2017-07-04 19:52:52 -04:00
|
|
|
'''
|
2017-07-06 12:40:03 -04:00
|
|
|
Home to the Core class which tracks program state. Used by GUI & commandline
|
2017-07-04 19:52:52 -04:00
|
|
|
'''
|
2017-06-06 11:14:39 -04:00
|
|
|
import sys
|
|
|
|
import os
|
2017-06-23 18:38:05 -04:00
|
|
|
from PyQt5 import QtCore, QtGui, uic
|
2015-03-02 16:47:52 -05:00
|
|
|
import subprocess as sp
|
|
|
|
import numpy
|
2017-06-07 12:59:59 -04:00
|
|
|
import json
|
2017-06-08 09:56:57 -04:00
|
|
|
from importlib import import_module
|
2017-06-23 18:38:05 -04:00
|
|
|
from PyQt5.QtCore import QStandardPaths
|
2015-03-02 16:47:52 -05:00
|
|
|
|
2017-07-04 19:52:52 -04:00
|
|
|
import toolkit
|
2017-07-13 00:05:11 -04:00
|
|
|
from frame import Frame
|
2017-07-15 01:00:03 -04:00
|
|
|
import video_thread
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2015-03-02 16:47:52 -05:00
|
|
|
|
2017-07-04 19:52:52 -04:00
|
|
|
class Core:
|
|
|
|
'''
|
|
|
|
MainWindow and Command module both use an instance of this class
|
|
|
|
to store the program state. This object tracks the components,
|
|
|
|
opens projects and presets, and stores settings/paths to data.
|
|
|
|
'''
|
2017-06-06 11:14:39 -04:00
|
|
|
def __init__(self):
|
2017-07-13 00:05:11 -04:00
|
|
|
Frame.core = self
|
2017-06-23 18:38:05 -04:00
|
|
|
self.dataDir = QStandardPaths.writableLocation(
|
|
|
|
QStandardPaths.AppConfigLocation
|
|
|
|
)
|
2017-06-08 16:50:48 -04:00
|
|
|
self.presetDir = os.path.join(self.dataDir, 'presets')
|
2017-06-23 03:39:56 -04:00
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
|
# frozen
|
|
|
|
self.wd = os.path.dirname(sys.executable)
|
|
|
|
else:
|
|
|
|
# unfrozen
|
|
|
|
self.wd = os.path.dirname(os.path.realpath(__file__))
|
2017-06-24 23:12:41 -04:00
|
|
|
self.componentsPath = os.path.join(self.wd, 'components')
|
2017-06-25 14:27:56 -04:00
|
|
|
self.settings = QtCore.QSettings(
|
|
|
|
os.path.join(self.dataDir, 'settings.ini'),
|
|
|
|
QtCore.QSettings.IniFormat
|
|
|
|
)
|
2017-06-23 03:39:56 -04:00
|
|
|
|
2017-06-07 12:59:59 -04:00
|
|
|
self.loadEncoderOptions()
|
2017-07-04 19:52:52 -04:00
|
|
|
self.videoFormats = toolkit.appendUppercase([
|
2017-06-15 22:15:03 -04:00
|
|
|
'*.mp4',
|
|
|
|
'*.mov',
|
|
|
|
'*.mkv',
|
|
|
|
'*.avi',
|
|
|
|
'*.webm',
|
|
|
|
'*.flv',
|
|
|
|
])
|
2017-07-04 19:52:52 -04:00
|
|
|
self.audioFormats = toolkit.appendUppercase([
|
2017-06-15 22:15:03 -04:00
|
|
|
'*.mp3',
|
|
|
|
'*.wav',
|
|
|
|
'*.ogg',
|
|
|
|
'*.fla',
|
2017-06-18 14:46:08 -04:00
|
|
|
'*.flac',
|
2017-06-15 22:15:03 -04:00
|
|
|
'*.aac',
|
|
|
|
])
|
2017-07-04 19:52:52 -04:00
|
|
|
self.imageFormats = toolkit.appendUppercase([
|
2017-06-15 22:15:03 -04:00
|
|
|
'*.png',
|
|
|
|
'*.jpg',
|
|
|
|
'*.tif',
|
|
|
|
'*.tiff',
|
|
|
|
'*.gif',
|
|
|
|
'*.bmp',
|
|
|
|
'*.ico',
|
|
|
|
'*.xbm',
|
|
|
|
'*.xpm',
|
|
|
|
])
|
2017-06-07 12:59:59 -04:00
|
|
|
|
2017-07-02 14:19:15 -04:00
|
|
|
self.FFMPEG_BIN = self.findFfmpeg()
|
2017-06-11 23:29:13 -04:00
|
|
|
self.findComponents()
|
2017-06-08 09:56:57 -04:00
|
|
|
self.selectedComponents = []
|
2017-06-13 22:47:18 -04:00
|
|
|
# copies of named presets to detect modification
|
|
|
|
self.savedPresets = {}
|
2017-06-08 09:56:57 -04:00
|
|
|
|
|
|
|
def findComponents(self):
|
|
|
|
def findComponents():
|
2017-06-24 23:12:41 -04:00
|
|
|
for f in sorted(os.listdir(self.componentsPath)):
|
|
|
|
name, ext = os.path.splitext(f)
|
|
|
|
if name.startswith("__"):
|
|
|
|
continue
|
|
|
|
elif ext == '.py':
|
|
|
|
yield name
|
2017-06-11 23:29:13 -04:00
|
|
|
self.modules = [
|
2017-06-08 09:56:57 -04:00
|
|
|
import_module('components.%s' % name)
|
2017-06-15 23:13:36 -04:00
|
|
|
for name in findComponents()
|
|
|
|
]
|
2017-06-25 18:12:16 -04:00
|
|
|
# store canonical module names and indexes
|
2017-06-11 23:29:13 -04:00
|
|
|
self.moduleIndexes = [i for i in range(len(self.modules))]
|
2017-06-18 21:49:00 -04:00
|
|
|
self.compNames = [mod.Component.__doc__ for mod in self.modules]
|
2017-06-25 18:12:16 -04:00
|
|
|
self.altCompNames = []
|
|
|
|
# store alternative names for modules
|
|
|
|
for i, mod in enumerate(self.modules):
|
|
|
|
if hasattr(mod.Component, 'names'):
|
|
|
|
for name in mod.Component.names():
|
|
|
|
self.altCompNames.append((name, i))
|
2017-06-08 09:56:57 -04:00
|
|
|
|
2017-06-12 22:34:37 -04:00
|
|
|
def componentListChanged(self):
|
|
|
|
for i, component in enumerate(self.selectedComponents):
|
|
|
|
component.compPos = i
|
|
|
|
|
2017-06-18 14:46:08 -04:00
|
|
|
def insertComponent(self, compPos, moduleIndex, loader):
|
|
|
|
'''Creates a new component'''
|
2017-06-22 19:59:31 -04:00
|
|
|
if compPos < 0 or compPos > len(self.selectedComponents):
|
2017-06-18 14:46:08 -04:00
|
|
|
compPos = len(self.selectedComponents)
|
2017-06-17 11:15:24 -04:00
|
|
|
if len(self.selectedComponents) > 50:
|
|
|
|
return None
|
2017-06-12 22:34:37 -04:00
|
|
|
|
|
|
|
component = self.modules[moduleIndex].Component(
|
2017-06-24 23:12:41 -04:00
|
|
|
moduleIndex, compPos, self
|
|
|
|
)
|
2017-06-08 09:56:57 -04:00
|
|
|
self.selectedComponents.insert(
|
|
|
|
compPos,
|
2017-06-24 23:12:41 -04:00
|
|
|
component
|
|
|
|
)
|
2017-06-12 22:34:37 -04:00
|
|
|
self.componentListChanged()
|
2017-06-18 14:46:08 -04:00
|
|
|
|
|
|
|
# init component's widget for loading/saving presets
|
|
|
|
self.selectedComponents[compPos].widget(loader)
|
|
|
|
self.updateComponent(compPos)
|
|
|
|
|
|
|
|
if hasattr(loader, 'insertComponent'):
|
|
|
|
loader.insertComponent(compPos)
|
2017-06-08 16:50:48 -04:00
|
|
|
return compPos
|
2017-06-08 09:56:57 -04:00
|
|
|
|
|
|
|
def moveComponent(self, startI, endI):
|
|
|
|
comp = self.selectedComponents.pop(startI)
|
2017-06-13 22:47:18 -04:00
|
|
|
self.selectedComponents.insert(endI, comp)
|
2017-06-12 22:34:37 -04:00
|
|
|
|
|
|
|
self.componentListChanged()
|
2017-06-08 16:50:48 -04:00
|
|
|
return endI
|
2017-06-08 09:56:57 -04:00
|
|
|
|
2017-06-12 22:34:37 -04:00
|
|
|
def removeComponent(self, i):
|
|
|
|
self.selectedComponents.pop(i)
|
|
|
|
self.componentListChanged()
|
|
|
|
|
2017-06-17 11:15:24 -04:00
|
|
|
def clearComponents(self):
|
|
|
|
self.selectedComponents = list()
|
|
|
|
self.componentListChanged()
|
|
|
|
|
2017-06-08 09:56:57 -04:00
|
|
|
def updateComponent(self, i):
|
2017-06-11 17:03:40 -04:00
|
|
|
# print('updating %s' % self.selectedComponents[i])
|
2017-06-08 09:56:57 -04:00
|
|
|
self.selectedComponents[i].update()
|
|
|
|
|
2017-06-11 23:29:13 -04:00
|
|
|
def moduleIndexFor(self, compName):
|
2017-06-25 18:12:16 -04:00
|
|
|
try:
|
|
|
|
index = self.compNames.index(compName)
|
|
|
|
return self.moduleIndexes[index]
|
|
|
|
except ValueError:
|
|
|
|
for altName, modI in self.altCompNames:
|
|
|
|
if altName == compName:
|
|
|
|
return self.moduleIndexes[modI]
|
2017-06-11 23:29:13 -04:00
|
|
|
|
2017-06-18 14:46:08 -04:00
|
|
|
def clearPreset(self, compIndex):
|
2017-06-15 11:36:26 -04:00
|
|
|
self.selectedComponents[compIndex].currentPreset = None
|
|
|
|
|
2017-06-13 22:47:18 -04:00
|
|
|
def openPreset(self, filepath, compIndex, presetName):
|
|
|
|
'''Applies a preset to a specific component'''
|
|
|
|
saveValueStore = self.getPreset(filepath)
|
|
|
|
if not saveValueStore:
|
|
|
|
return False
|
2017-06-15 23:13:36 -04:00
|
|
|
try:
|
|
|
|
self.selectedComponents[compIndex].loadPreset(
|
|
|
|
saveValueStore,
|
|
|
|
presetName
|
|
|
|
)
|
|
|
|
except KeyError as e:
|
|
|
|
print('preset missing value: %s' % e)
|
2017-06-13 22:47:18 -04:00
|
|
|
|
|
|
|
self.savedPresets[presetName] = dict(saveValueStore)
|
|
|
|
return True
|
|
|
|
|
2017-06-22 18:40:34 -04:00
|
|
|
def getPresetDir(self, comp):
|
|
|
|
return os.path.join(
|
|
|
|
self.presetDir, str(comp), str(comp.version()))
|
|
|
|
|
2017-06-13 22:47:18 -04:00
|
|
|
def getPreset(self, filepath):
|
|
|
|
'''Returns the preset dict stored at this filepath'''
|
|
|
|
if not os.path.exists(filepath):
|
|
|
|
return False
|
|
|
|
with open(filepath, 'r') as f:
|
|
|
|
for line in f:
|
2017-07-04 19:52:52 -04:00
|
|
|
saveValueStore = toolkit.presetFromString(line.strip())
|
2017-06-13 22:47:18 -04:00
|
|
|
break
|
|
|
|
return saveValueStore
|
|
|
|
|
2017-06-11 23:29:13 -04:00
|
|
|
def openProject(self, loader, filepath):
|
2017-06-22 18:40:34 -04:00
|
|
|
''' loader is the object calling this method which must have
|
|
|
|
its own showMessage(**kwargs) method for displaying errors.
|
|
|
|
'''
|
2017-06-22 20:31:04 -04:00
|
|
|
if not os.path.exists(filepath):
|
2017-06-25 14:27:56 -04:00
|
|
|
loader.showMessage(msg='Project file not found.')
|
2017-06-22 20:31:04 -04:00
|
|
|
return
|
|
|
|
|
2017-06-11 23:29:13 -04:00
|
|
|
errcode, data = self.parseAvFile(filepath)
|
|
|
|
if errcode == 0:
|
2017-06-15 22:15:03 -04:00
|
|
|
try:
|
2017-06-25 14:27:56 -04:00
|
|
|
if hasattr(loader, 'window'):
|
2017-06-25 15:50:31 -04:00
|
|
|
for widget, value in data['WindowFields']:
|
2017-06-25 14:27:56 -04:00
|
|
|
widget = eval('loader.window.%s' % widget)
|
2017-06-25 15:31:42 -04:00
|
|
|
widget.blockSignals(True)
|
2017-06-25 15:50:31 -04:00
|
|
|
widget.setText(value)
|
2017-06-25 15:31:42 -04:00
|
|
|
widget.blockSignals(False)
|
2017-06-25 14:27:56 -04:00
|
|
|
|
2017-06-25 15:50:31 -04:00
|
|
|
for key, value in data['Settings']:
|
|
|
|
self.settings.setValue(key, value)
|
2017-06-25 14:27:56 -04:00
|
|
|
|
|
|
|
for tup in data['Components']:
|
2017-06-15 22:15:03 -04:00
|
|
|
name, vers, preset = tup
|
|
|
|
clearThis = False
|
2017-06-24 23:40:32 -04:00
|
|
|
modified = False
|
2017-06-15 22:15:03 -04:00
|
|
|
|
|
|
|
# add loaded named presets to savedPresets dict
|
2017-06-23 23:00:24 -04:00
|
|
|
if 'preset' in preset and preset['preset'] is not None:
|
2017-06-15 22:15:03 -04:00
|
|
|
nam = preset['preset']
|
|
|
|
filepath2 = os.path.join(
|
|
|
|
self.presetDir, name, str(vers), nam)
|
|
|
|
origSaveValueStore = self.getPreset(filepath2)
|
|
|
|
if origSaveValueStore:
|
|
|
|
self.savedPresets[nam] = dict(origSaveValueStore)
|
2017-06-24 23:40:32 -04:00
|
|
|
modified = not origSaveValueStore == preset
|
2017-06-15 22:15:03 -04:00
|
|
|
else:
|
|
|
|
# saved preset was renamed or deleted
|
|
|
|
clearThis = True
|
|
|
|
|
2017-06-18 14:46:08 -04:00
|
|
|
# create the actual component object & get its index
|
|
|
|
i = self.insertComponent(
|
|
|
|
-1,
|
|
|
|
self.moduleIndexFor(name),
|
|
|
|
loader)
|
2017-06-23 23:00:24 -04:00
|
|
|
if i is None:
|
2017-06-18 14:46:08 -04:00
|
|
|
loader.showMessage(msg="Too many components!")
|
2017-06-17 11:15:24 -04:00
|
|
|
break
|
|
|
|
|
2017-06-15 22:15:03 -04:00
|
|
|
try:
|
2017-06-23 23:00:24 -04:00
|
|
|
if 'preset' in preset and preset['preset'] is not None:
|
2017-06-17 11:15:24 -04:00
|
|
|
self.selectedComponents[i].loadPreset(
|
2017-06-15 22:15:03 -04:00
|
|
|
preset
|
|
|
|
)
|
|
|
|
else:
|
2017-06-17 11:15:24 -04:00
|
|
|
self.selectedComponents[i].loadPreset(
|
2017-06-15 22:15:03 -04:00
|
|
|
preset,
|
|
|
|
preset['preset']
|
|
|
|
)
|
|
|
|
except KeyError as e:
|
2017-06-25 14:27:56 -04:00
|
|
|
print('%s missing value: %s' % (
|
2017-06-23 17:14:39 -04:00
|
|
|
self.selectedComponents[i], e)
|
|
|
|
)
|
2017-06-15 11:36:26 -04:00
|
|
|
|
2017-06-15 22:15:03 -04:00
|
|
|
if clearThis:
|
2017-06-18 14:46:08 -04:00
|
|
|
self.clearPreset(i)
|
|
|
|
if hasattr(loader, 'updateComponentTitle'):
|
2017-06-24 23:40:32 -04:00
|
|
|
loader.updateComponentTitle(i, modified)
|
2017-06-25 14:27:56 -04:00
|
|
|
|
2017-06-15 22:15:03 -04:00
|
|
|
except:
|
|
|
|
errcode = 1
|
|
|
|
data = sys.exc_info()
|
2017-06-15 11:36:26 -04:00
|
|
|
|
2017-06-15 22:15:03 -04:00
|
|
|
if errcode == 1:
|
2017-06-25 14:27:56 -04:00
|
|
|
typ, value, tb = data
|
|
|
|
if typ.__name__ == 'KeyError':
|
2017-06-11 23:29:13 -04:00
|
|
|
# probably just an old version, still loadable
|
|
|
|
print('file missing value: %s' % value)
|
|
|
|
return
|
2017-06-18 14:46:08 -04:00
|
|
|
if hasattr(loader, 'createNewProject'):
|
2017-06-25 18:12:16 -04:00
|
|
|
loader.createNewProject(prompt=False)
|
2017-06-25 14:27:56 -04:00
|
|
|
import traceback
|
|
|
|
msg = '%s: %s\n\nTraceback:\n' % (typ.__name__, value)
|
|
|
|
msg += "\n".join(traceback.format_tb(tb))
|
2017-06-11 23:29:13 -04:00
|
|
|
loader.showMessage(
|
|
|
|
msg="Project file '%s' is corrupted." % filepath,
|
|
|
|
showCancel=False,
|
2017-06-25 14:27:56 -04:00
|
|
|
icon='Warning',
|
2017-06-11 23:29:13 -04:00
|
|
|
detail=msg)
|
|
|
|
|
|
|
|
def parseAvFile(self, filepath):
|
|
|
|
'''Parses an avp (project) or avl (preset package) file.
|
2017-06-22 18:40:34 -04:00
|
|
|
Returns dictionary with section names as the keys, each one
|
|
|
|
contains a list of tuples: (compName, version, compPresetDict)
|
|
|
|
'''
|
2017-06-25 15:50:31 -04:00
|
|
|
validSections = (
|
|
|
|
'Components',
|
|
|
|
'Settings',
|
|
|
|
'WindowFields'
|
|
|
|
)
|
|
|
|
data = {sect: [] for sect in validSections}
|
2017-06-11 23:29:13 -04:00
|
|
|
try:
|
|
|
|
with open(filepath, 'r') as f:
|
|
|
|
def parseLine(line):
|
2017-06-22 18:40:34 -04:00
|
|
|
'''Decides if a file line is a section header'''
|
2017-06-11 23:29:13 -04:00
|
|
|
line = line.strip()
|
|
|
|
newSection = ''
|
|
|
|
|
|
|
|
if line.startswith('[') and line.endswith(']') \
|
|
|
|
and line[1:-1] in validSections:
|
|
|
|
newSection = line[1:-1]
|
|
|
|
|
|
|
|
return line, newSection
|
|
|
|
|
|
|
|
section = ''
|
|
|
|
i = 0
|
|
|
|
for line in f:
|
|
|
|
line, newSection = parseLine(line)
|
|
|
|
if newSection:
|
|
|
|
section = str(newSection)
|
|
|
|
continue
|
|
|
|
if line and section == 'Components':
|
|
|
|
if i == 0:
|
|
|
|
lastCompName = str(line)
|
|
|
|
i += 1
|
|
|
|
elif i == 1:
|
|
|
|
lastCompVers = str(line)
|
|
|
|
i += 1
|
|
|
|
elif i == 2:
|
2017-07-04 19:52:52 -04:00
|
|
|
lastCompPreset = toolkit.presetFromString(line)
|
2017-06-23 23:00:24 -04:00
|
|
|
data[section].append((
|
|
|
|
lastCompName,
|
2017-06-11 23:29:13 -04:00
|
|
|
lastCompVers,
|
2017-06-23 23:00:24 -04:00
|
|
|
lastCompPreset
|
|
|
|
))
|
2017-06-11 23:29:13 -04:00
|
|
|
i = 0
|
2017-06-25 14:27:56 -04:00
|
|
|
elif line and section:
|
2017-06-25 15:50:31 -04:00
|
|
|
key, value = line.split('=', 1)
|
|
|
|
data[section].append((key, value.strip()))
|
|
|
|
|
2017-06-11 23:29:13 -04:00
|
|
|
return 0, data
|
|
|
|
except:
|
|
|
|
return 1, sys.exc_info()
|
2017-06-08 16:50:48 -04:00
|
|
|
|
2017-06-08 22:56:33 -04:00
|
|
|
def importPreset(self, filepath):
|
2017-06-11 23:29:13 -04:00
|
|
|
errcode, data = self.parseAvFile(filepath)
|
|
|
|
returnList = []
|
|
|
|
if errcode == 0:
|
|
|
|
name, vers, preset = data['Components'][0]
|
|
|
|
presetName = preset['preset'] \
|
|
|
|
if preset['preset'] else os.path.basename(filepath)[:-4]
|
|
|
|
newPath = os.path.join(
|
|
|
|
self.presetDir,
|
|
|
|
name,
|
|
|
|
vers,
|
|
|
|
presetName
|
|
|
|
)
|
|
|
|
if os.path.exists(newPath):
|
|
|
|
return False, newPath
|
|
|
|
preset['preset'] = presetName
|
|
|
|
self.createPresetFile(
|
|
|
|
name, vers, presetName, preset
|
|
|
|
)
|
|
|
|
return True, presetName
|
|
|
|
elif errcode == 1:
|
|
|
|
# TODO: an error message
|
|
|
|
return False, ''
|
2017-06-08 22:56:33 -04:00
|
|
|
|
2017-06-10 14:52:01 -04:00
|
|
|
def exportPreset(self, exportPath, compName, vers, origName):
|
2017-06-23 23:00:24 -04:00
|
|
|
internalPath = os.path.join(
|
|
|
|
self.presetDir, compName, str(vers), origName
|
|
|
|
)
|
2017-06-10 14:52:01 -04:00
|
|
|
if not os.path.exists(internalPath):
|
|
|
|
return
|
|
|
|
if os.path.exists(exportPath):
|
|
|
|
os.remove(exportPath)
|
|
|
|
with open(internalPath, 'r') as f:
|
|
|
|
internalData = [line for line in f]
|
|
|
|
try:
|
2017-07-04 19:52:52 -04:00
|
|
|
saveValueStore = toolkit.presetFromString(internalData[0].strip())
|
2017-06-10 14:52:01 -04:00
|
|
|
self.createPresetFile(
|
|
|
|
compName, vers,
|
|
|
|
origName, saveValueStore,
|
|
|
|
exportPath
|
|
|
|
)
|
2017-06-11 12:52:29 -04:00
|
|
|
return True
|
2017-06-10 14:52:01 -04:00
|
|
|
except:
|
2017-06-11 12:52:29 -04:00
|
|
|
return False
|
2017-06-10 14:52:01 -04:00
|
|
|
|
|
|
|
def createPresetFile(
|
2017-06-23 23:00:24 -04:00
|
|
|
self, compName, vers, presetName, saveValueStore, filepath=''):
|
2017-06-10 14:52:01 -04:00
|
|
|
'''Create a preset file (.avl) at filepath using args.
|
2017-06-22 18:40:34 -04:00
|
|
|
Or if filepath is empty, create an internal preset using args'''
|
2017-06-10 14:52:01 -04:00
|
|
|
if not filepath:
|
|
|
|
dirname = os.path.join(self.presetDir, compName, str(vers))
|
|
|
|
if not os.path.exists(dirname):
|
|
|
|
os.makedirs(dirname)
|
|
|
|
filepath = os.path.join(dirname, presetName)
|
|
|
|
internal = True
|
|
|
|
else:
|
|
|
|
if not filepath.endswith('.avl'):
|
|
|
|
filepath += '.avl'
|
|
|
|
internal = False
|
|
|
|
|
|
|
|
with open(filepath, 'w') as f:
|
|
|
|
if not internal:
|
|
|
|
f.write('[Components]\n')
|
|
|
|
f.write('%s\n' % compName)
|
|
|
|
f.write('%s\n' % str(vers))
|
2017-07-04 19:52:52 -04:00
|
|
|
f.write(toolkit.presetToString(saveValueStore))
|
2017-06-10 14:52:01 -04:00
|
|
|
|
2017-06-25 14:27:56 -04:00
|
|
|
def createProjectFile(self, filepath, window=None):
|
2017-06-10 14:52:01 -04:00
|
|
|
'''Create a project file (.avp) using the current program state'''
|
2017-06-25 16:13:54 -04:00
|
|
|
settingsKeys = [
|
|
|
|
'componentDir',
|
|
|
|
'inputDir',
|
|
|
|
'outputDir',
|
|
|
|
'presetDir',
|
|
|
|
'projectDir',
|
2017-06-25 14:27:56 -04:00
|
|
|
]
|
2017-06-10 14:52:01 -04:00
|
|
|
try:
|
|
|
|
if not filepath.endswith(".avp"):
|
|
|
|
filepath += '.avp'
|
|
|
|
if os.path.exists(filepath):
|
|
|
|
os.remove(filepath)
|
|
|
|
with open(filepath, 'w') as f:
|
|
|
|
print('creating %s' % filepath)
|
2017-06-25 14:27:56 -04:00
|
|
|
|
2017-06-10 14:52:01 -04:00
|
|
|
f.write('[Components]\n')
|
|
|
|
for comp in self.selectedComponents:
|
|
|
|
saveValueStore = comp.savePreset()
|
2017-07-13 21:59:23 -04:00
|
|
|
saveValueStore['preset'] = comp.currentPreset
|
2017-06-10 14:52:01 -04:00
|
|
|
f.write('%s\n' % str(comp))
|
|
|
|
f.write('%s\n' % str(comp.version()))
|
2017-07-04 19:52:52 -04:00
|
|
|
f.write('%s\n' % toolkit.presetToString(saveValueStore))
|
2017-06-25 14:27:56 -04:00
|
|
|
|
2017-06-25 15:34:33 -04:00
|
|
|
f.write('\n[Settings]\n')
|
2017-06-25 14:27:56 -04:00
|
|
|
for key in self.settings.allKeys():
|
2017-06-25 16:13:54 -04:00
|
|
|
if key in settingsKeys:
|
2017-06-25 14:27:56 -04:00
|
|
|
f.write('%s=%s\n' % (key, self.settings.value(key)))
|
|
|
|
|
|
|
|
if window:
|
2017-06-25 15:34:33 -04:00
|
|
|
f.write('\n[WindowFields]\n')
|
2017-06-25 14:27:56 -04:00
|
|
|
f.write(
|
|
|
|
'lineEdit_audioFile=%s\n'
|
|
|
|
'lineEdit_outputFile=%s\n' % (
|
|
|
|
window.lineEdit_audioFile.text(),
|
|
|
|
window.lineEdit_outputFile.text()
|
|
|
|
)
|
|
|
|
)
|
2017-06-10 14:52:01 -04:00
|
|
|
return True
|
|
|
|
except:
|
|
|
|
return False
|
2017-06-08 22:56:33 -04:00
|
|
|
|
2017-06-07 12:59:59 -04:00
|
|
|
def loadEncoderOptions(self):
|
|
|
|
file_path = os.path.join(self.wd, 'encoder-options.json')
|
|
|
|
with open(file_path) as json_file:
|
|
|
|
self.encoder_options = json.load(json_file)
|
2017-06-06 11:14:39 -04:00
|
|
|
|
|
|
|
def findFfmpeg(self):
|
2017-07-02 14:19:15 -04:00
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
|
# The application is frozen
|
|
|
|
if sys.platform == "win32":
|
|
|
|
return os.path.join(self.wd, 'ffmpeg.exe')
|
|
|
|
else:
|
|
|
|
return os.path.join(self.wd, 'ffmpeg')
|
|
|
|
|
2017-06-06 11:14:39 -04:00
|
|
|
else:
|
2017-07-02 14:19:15 -04:00
|
|
|
if sys.platform == "win32":
|
2017-07-09 01:10:06 -04:00
|
|
|
return "ffmpeg"
|
2017-07-02 14:19:15 -04:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
with open(os.devnull, "w") as f:
|
2017-07-09 01:10:06 -04:00
|
|
|
toolkit.checkOutput(
|
|
|
|
['ffmpeg', '-version'], stderr=f
|
2017-07-04 19:52:52 -04:00
|
|
|
)
|
2017-07-02 14:19:15 -04:00
|
|
|
return "ffmpeg"
|
2017-07-09 01:10:06 -04:00
|
|
|
except sp.CalledProcessError:
|
2017-07-02 14:19:15 -04:00
|
|
|
return "avconv"
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-15 13:13:53 -04:00
|
|
|
def createFfmpegCommand(self, inputFile, outputFile, duration):
|
2017-07-09 21:27:29 -04:00
|
|
|
'''
|
|
|
|
Constructs the major ffmpeg command used to export the video
|
|
|
|
'''
|
2017-07-16 14:06:11 -04:00
|
|
|
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
|
|
|
|
duration = "{0:.3f}".format(duration + 0.1) # used by input sources
|
2017-07-09 21:27:29 -04:00
|
|
|
|
|
|
|
# Test if user has libfdk_aac
|
|
|
|
encoders = toolkit.checkOutput(
|
|
|
|
"%s -encoders -hide_banner" % self.FFMPEG_BIN, shell=True
|
|
|
|
)
|
|
|
|
encoders = encoders.decode("utf-8")
|
|
|
|
|
|
|
|
acodec = self.settings.value('outputAudioCodec')
|
|
|
|
|
|
|
|
options = self.encoder_options
|
|
|
|
containerName = self.settings.value('outputContainer')
|
|
|
|
vcodec = self.settings.value('outputVideoCodec')
|
|
|
|
vbitrate = str(self.settings.value('outputVideoBitrate'))+'k'
|
|
|
|
acodec = self.settings.value('outputAudioCodec')
|
|
|
|
abitrate = str(self.settings.value('outputAudioBitrate'))+'k'
|
|
|
|
|
|
|
|
for cont in options['containers']:
|
|
|
|
if cont['name'] == containerName:
|
|
|
|
container = cont['container']
|
|
|
|
break
|
|
|
|
|
|
|
|
vencoders = options['video-codecs'][vcodec]
|
|
|
|
aencoders = options['audio-codecs'][acodec]
|
|
|
|
|
|
|
|
for encoder in vencoders:
|
|
|
|
if encoder in encoders:
|
|
|
|
vencoder = encoder
|
|
|
|
break
|
|
|
|
|
|
|
|
for encoder in aencoders:
|
|
|
|
if encoder in encoders:
|
|
|
|
aencoder = encoder
|
|
|
|
break
|
|
|
|
|
|
|
|
ffmpegCommand = [
|
|
|
|
self.FFMPEG_BIN,
|
|
|
|
'-thread_queue_size', '512',
|
|
|
|
'-y', # overwrite the output file if it already exists.
|
|
|
|
|
|
|
|
# INPUT VIDEO
|
|
|
|
'-f', 'rawvideo',
|
|
|
|
'-vcodec', 'rawvideo',
|
|
|
|
'-s', '%sx%s' % (
|
|
|
|
self.settings.value('outputWidth'),
|
|
|
|
self.settings.value('outputHeight'),
|
|
|
|
),
|
|
|
|
'-pix_fmt', 'rgba',
|
|
|
|
'-r', self.settings.value('outputFrameRate'),
|
2017-07-15 13:13:53 -04:00
|
|
|
'-t', duration,
|
2017-07-09 21:27:29 -04:00
|
|
|
'-i', '-', # the video input comes from a pipe
|
|
|
|
'-an', # the video input has no sound
|
|
|
|
|
|
|
|
# INPUT SOUND
|
2017-07-15 13:13:53 -04:00
|
|
|
'-t', duration,
|
2017-07-09 21:27:29 -04:00
|
|
|
'-i', inputFile
|
|
|
|
]
|
|
|
|
|
2017-07-16 14:06:11 -04:00
|
|
|
# Add extra audio inputs and any needed avfilters
|
|
|
|
# NOTE: Global filters are currently hard-coded here for debugging use
|
|
|
|
globalFilters = 0 # increase to add global filters
|
2017-07-09 21:27:29 -04:00
|
|
|
extraAudio = [
|
|
|
|
comp.audio() for comp in self.selectedComponents
|
|
|
|
if 'audio' in comp.properties()
|
|
|
|
]
|
2017-07-16 14:06:11 -04:00
|
|
|
if extraAudio or globalFilters > 0:
|
|
|
|
# Add -i options for extra input files
|
|
|
|
extraFilters = {}
|
|
|
|
for streamNo, params in enumerate(reversed(extraAudio)):
|
2017-07-13 14:46:22 -04:00
|
|
|
extraInputFile, params = params
|
2017-07-09 21:27:29 -04:00
|
|
|
ffmpegCommand.extend([
|
2017-07-16 14:06:11 -04:00
|
|
|
'-t', safeDuration,
|
2017-07-09 21:27:29 -04:00
|
|
|
'-i', extraInputFile
|
|
|
|
])
|
2017-07-16 14:06:11 -04:00
|
|
|
# Construct dataset of extra filters we'll need to add later
|
|
|
|
for ffmpegFilter in params:
|
|
|
|
if streamNo + 2 not in extraFilters:
|
|
|
|
extraFilters[streamNo + 2] = []
|
|
|
|
extraFilters[streamNo + 2].append((
|
|
|
|
ffmpegFilter, params[ffmpegFilter]
|
|
|
|
))
|
|
|
|
|
|
|
|
# Start creating avfilters!
|
|
|
|
extraFilterCommand = []
|
|
|
|
|
|
|
|
if globalFilters <= 0:
|
|
|
|
# Dictionary of last-used tmp labels for a given stream number
|
|
|
|
tmpInputs = {streamNo: -1 for streamNo in extraFilters}
|
|
|
|
else:
|
|
|
|
# Insert blank entries for global filters into extraFilters
|
|
|
|
# so the per-stream filters know what input to source later
|
|
|
|
for streamNo in range(len(extraAudio), 0, -1):
|
|
|
|
if streamNo + 1 not in extraFilters:
|
|
|
|
extraFilters[streamNo + 1] = []
|
|
|
|
# Also filter the primary audio track
|
|
|
|
extraFilters[1] = []
|
|
|
|
tmpInputs = {
|
|
|
|
streamNo: globalFilters - 1
|
|
|
|
for streamNo in extraFilters
|
|
|
|
}
|
|
|
|
|
|
|
|
# Add the global filters!
|
|
|
|
# NOTE: list length must = globalFilters, currently hardcoded
|
|
|
|
if tmpInputs:
|
|
|
|
extraFilterCommand.extend([
|
|
|
|
'[%s:a] ashowinfo [%stmp0]' % (
|
|
|
|
str(streamNo),
|
|
|
|
str(streamNo)
|
|
|
|
)
|
|
|
|
for streamNo in tmpInputs
|
|
|
|
])
|
|
|
|
|
|
|
|
# Now add the per-stream filters!
|
|
|
|
for streamNo, paramList in extraFilters.items():
|
|
|
|
for param in paramList:
|
|
|
|
source = '[%s:a]' % str(streamNo) \
|
|
|
|
if tmpInputs[streamNo] == -1 else \
|
|
|
|
'[%stmp%s]' % (
|
|
|
|
str(streamNo), str(tmpInputs[streamNo])
|
|
|
|
)
|
|
|
|
tmpInputs[streamNo] = tmpInputs[streamNo] + 1
|
|
|
|
extraFilterCommand.append(
|
|
|
|
'%s %s%s [%stmp%s]' % (
|
|
|
|
source, param[0], param[1], str(streamNo),
|
|
|
|
str(tmpInputs[streamNo])
|
|
|
|
)
|
|
|
|
)
|
2017-07-13 14:46:22 -04:00
|
|
|
|
2017-07-16 14:06:11 -04:00
|
|
|
# Join all the filters together and combine into 1 stream
|
|
|
|
extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \
|
|
|
|
if tmpInputs else ''
|
2017-07-09 21:27:29 -04:00
|
|
|
ffmpegCommand.extend([
|
|
|
|
'-filter_complex',
|
2017-07-16 14:06:11 -04:00
|
|
|
extraFilterCommand +
|
|
|
|
'%s amix=inputs=%s:duration=first [a]'
|
|
|
|
% (
|
|
|
|
"".join([
|
|
|
|
'[%stmp%s]' % (str(i), tmpInputs[i])
|
|
|
|
if i in extraFilters else '[%s:a]' % str(i)
|
|
|
|
for i in range(1, len(extraAudio) + 2)
|
|
|
|
]),
|
|
|
|
str(len(extraAudio) + 1)
|
2017-07-11 06:06:22 -04:00
|
|
|
),
|
2017-07-09 21:27:29 -04:00
|
|
|
])
|
|
|
|
|
2017-07-16 14:06:11 -04:00
|
|
|
# Only map audio from the filters, and video from the pipe
|
|
|
|
ffmpegCommand.extend([
|
|
|
|
'-map', '0:v',
|
|
|
|
'-map', '[a]',
|
|
|
|
])
|
|
|
|
|
2017-07-09 21:27:29 -04:00
|
|
|
ffmpegCommand.extend([
|
|
|
|
# OUTPUT
|
|
|
|
'-vcodec', vencoder,
|
|
|
|
'-acodec', aencoder,
|
|
|
|
'-b:v', vbitrate,
|
|
|
|
'-b:a', abitrate,
|
|
|
|
'-pix_fmt', self.settings.value('outputVideoFormat'),
|
|
|
|
'-preset', self.settings.value('outputPreset'),
|
|
|
|
'-f', container
|
|
|
|
])
|
|
|
|
|
|
|
|
if acodec == 'aac':
|
|
|
|
ffmpegCommand.append('-strict')
|
|
|
|
ffmpegCommand.append('-2')
|
|
|
|
|
|
|
|
ffmpegCommand.append(outputFile)
|
|
|
|
return ffmpegCommand
|
|
|
|
|
2017-07-16 14:06:11 -04:00
|
|
|
def getAudioDuration(self, filename):
|
2017-06-06 11:14:39 -04:00
|
|
|
command = [self.FFMPEG_BIN, '-i', filename]
|
|
|
|
|
|
|
|
try:
|
2017-07-04 19:52:52 -04:00
|
|
|
fileInfo = toolkit.checkOutput(command, stderr=sp.STDOUT)
|
2017-06-06 11:14:39 -04:00
|
|
|
except sp.CalledProcessError as ex:
|
|
|
|
fileInfo = ex.output
|
|
|
|
|
|
|
|
info = fileInfo.decode("utf-8").split('\n')
|
|
|
|
for line in info:
|
|
|
|
if 'Duration' in line:
|
|
|
|
d = line.split(',')[0]
|
|
|
|
d = d.split(' ')[3]
|
|
|
|
d = d.split(':')
|
|
|
|
duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
|
2017-07-16 14:06:11 -04:00
|
|
|
return duration
|
|
|
|
|
|
|
|
def readAudioFile(self, filename, parent):
|
|
|
|
duration = self.getAudioDuration(filename)
|
2017-06-06 11:14:39 -04:00
|
|
|
|
|
|
|
command = [
|
|
|
|
self.FFMPEG_BIN,
|
|
|
|
'-i', filename,
|
|
|
|
'-f', 's16le',
|
|
|
|
'-acodec', 'pcm_s16le',
|
|
|
|
'-ar', '44100', # ouput will have 44100 Hz
|
|
|
|
'-ac', '1', # mono (set to '2' for stereo)
|
|
|
|
'-']
|
2017-07-04 19:52:52 -04:00
|
|
|
in_pipe = toolkit.openPipe(
|
2017-07-09 14:31:19 -04:00
|
|
|
command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
|
|
|
|
)
|
2017-06-06 11:14:39 -04:00
|
|
|
|
|
|
|
completeAudioArray = numpy.empty(0, dtype="int16")
|
|
|
|
|
|
|
|
progress = 0
|
|
|
|
lastPercent = None
|
|
|
|
while True:
|
|
|
|
if self.canceled:
|
|
|
|
break
|
|
|
|
# read 2 seconds of audio
|
2017-07-09 14:31:19 -04:00
|
|
|
progress += 4
|
2017-06-06 11:14:39 -04:00
|
|
|
raw_audio = in_pipe.stdout.read(88200*4)
|
|
|
|
if len(raw_audio) == 0:
|
|
|
|
break
|
|
|
|
audio_array = numpy.fromstring(raw_audio, dtype="int16")
|
|
|
|
completeAudioArray = numpy.append(completeAudioArray, audio_array)
|
|
|
|
|
|
|
|
percent = int(100*(progress/duration))
|
|
|
|
if percent >= 100:
|
|
|
|
percent = 100
|
|
|
|
|
|
|
|
if lastPercent != percent:
|
|
|
|
string = 'Loading audio file: '+str(percent)+'%'
|
|
|
|
parent.progressBarSetText.emit(string)
|
|
|
|
parent.progressBarUpdate.emit(percent)
|
|
|
|
|
|
|
|
lastPercent = percent
|
|
|
|
|
|
|
|
in_pipe.kill()
|
|
|
|
in_pipe.wait()
|
|
|
|
|
|
|
|
# add 0s the end
|
|
|
|
completeAudioArrayCopy = numpy.zeros(
|
|
|
|
len(completeAudioArray) + 44100, dtype="int16")
|
|
|
|
completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
|
|
|
|
completeAudioArray = completeAudioArrayCopy
|
|
|
|
|
2017-07-15 13:13:53 -04:00
|
|
|
return (completeAudioArray, duration)
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-15 01:00:03 -04:00
|
|
|
def newVideoWorker(self, loader, audioFile, outputPath):
|
|
|
|
self.videoThread = QtCore.QThread(loader)
|
|
|
|
videoWorker = video_thread.Worker(
|
|
|
|
loader, audioFile, outputPath, self.selectedComponents
|
|
|
|
)
|
|
|
|
videoWorker.moveToThread(self.videoThread)
|
|
|
|
videoWorker.videoCreated.connect(self.videoCreated)
|
|
|
|
|
|
|
|
self.videoThread.start()
|
|
|
|
return videoWorker
|
|
|
|
|
|
|
|
def videoCreated(self):
|
|
|
|
self.videoThread.quit()
|
|
|
|
self.videoThread.wait()
|
|
|
|
|
2017-06-06 11:14:39 -04:00
|
|
|
def cancel(self):
|
|
|
|
self.canceled = True
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
self.canceled = False
|