2017-07-06 12:40:03 -04:00
|
|
|
'''
|
|
|
|
Thread created to export a video. It has a slot to begin export using
|
|
|
|
an input file, output path, and component list. During export multiple
|
|
|
|
threads are created to render the video as quickly as possible. Signals
|
|
|
|
are emitted to update MainWindow's progress bar, detail text, and preview.
|
2017-07-09 01:10:06 -04:00
|
|
|
Export can be cancelled with cancel()
|
2017-07-06 12:40:03 -04:00
|
|
|
'''
|
2017-07-20 20:31:38 -04:00
|
|
|
from PyQt5 import QtCore, QtGui
|
2017-06-23 18:38:05 -04:00
|
|
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
2017-07-20 20:31:38 -04:00
|
|
|
from PIL import Image
|
2015-03-03 14:11:55 -05:00
|
|
|
from PIL.ImageQt import ImageQt
|
|
|
|
import numpy
|
|
|
|
import subprocess as sp
|
|
|
|
import sys
|
2017-06-02 00:24:13 -04:00
|
|
|
import os
|
2017-05-31 05:01:18 -04:00
|
|
|
from queue import Queue, PriorityQueue
|
2017-06-02 09:14:04 -04:00
|
|
|
from threading import Thread, Event
|
2017-05-31 03:15:09 -04:00
|
|
|
import time
|
2017-06-02 00:24:13 -04:00
|
|
|
import signal
|
2015-03-03 14:11:55 -05:00
|
|
|
|
2017-07-15 01:00:03 -04:00
|
|
|
from toolkit import openPipe
|
2017-07-20 20:31:38 -04:00
|
|
|
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
|
2017-07-17 22:07:33 -04:00
|
|
|
from toolkit.frame import Checkerboard
|
2017-06-25 10:36:32 -04:00
|
|
|
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2015-03-03 14:11:55 -05:00
|
|
|
class Worker(QtCore.QObject):
|
|
|
|
|
2017-05-31 03:15:09 -04:00
|
|
|
imageCreated = pyqtSignal(['QImage'])
|
|
|
|
videoCreated = pyqtSignal()
|
|
|
|
progressBarUpdate = pyqtSignal(int)
|
|
|
|
progressBarSetText = pyqtSignal(str)
|
2017-06-03 01:07:30 -04:00
|
|
|
encoding = pyqtSignal(bool)
|
2017-05-31 03:15:09 -04:00
|
|
|
|
2017-07-15 01:00:03 -04:00
|
|
|
def __init__(self, parent, inputFile, outputFile, components):
|
2017-05-31 03:15:09 -04:00
|
|
|
QtCore.QObject.__init__(self)
|
2017-07-09 21:27:29 -04:00
|
|
|
self.core = parent.core
|
2017-07-20 20:31:38 -04:00
|
|
|
self.settings = parent.settings
|
2017-06-08 22:31:02 -04:00
|
|
|
self.modules = parent.core.modules
|
2017-07-15 01:00:03 -04:00
|
|
|
parent.createVideo.connect(self.createVideo)
|
|
|
|
|
2017-05-31 03:15:09 -04:00
|
|
|
self.parent = parent
|
2017-07-15 01:00:03 -04:00
|
|
|
self.components = components
|
|
|
|
self.outputFile = outputFile
|
|
|
|
self.inputFile = inputFile
|
|
|
|
|
2017-06-22 18:40:34 -04:00
|
|
|
self.sampleSize = 1470 # 44100 / 30 = 1470
|
2017-06-02 00:24:13 -04:00
|
|
|
self.canceled = False
|
|
|
|
self.error = False
|
2017-06-02 09:14:04 -04:00
|
|
|
self.stopped = False
|
2017-05-31 03:15:09 -04:00
|
|
|
|
|
|
|
def renderNode(self):
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
|
|
|
Grabs audio data indices at frames to export, from compositeQueue.
|
|
|
|
Sends it to the components' frameRender methods in layer order
|
|
|
|
to create subframes & composite them into the final frame.
|
|
|
|
The resulting frames are collected in the renderQueue
|
|
|
|
'''
|
2017-06-02 09:14:04 -04:00
|
|
|
while not self.stopped:
|
2017-07-09 01:10:06 -04:00
|
|
|
audioI = self.compositeQueue.get()
|
|
|
|
bgI = int(audioI / self.sampleSize)
|
2017-06-05 05:54:58 -04:00
|
|
|
frame = None
|
2017-06-03 20:29:25 -04:00
|
|
|
for compNo, comp in reversed(list(enumerate(self.components))):
|
2017-07-15 01:00:03 -04:00
|
|
|
layerNo = len(self.components) - compNo - 1
|
2017-07-13 19:31:00 -04:00
|
|
|
if layerNo in self.staticComponents:
|
|
|
|
if self.staticComponents[layerNo] is None:
|
2017-07-13 00:05:11 -04:00
|
|
|
# this layer was merged into a following layer
|
|
|
|
continue
|
2017-07-09 01:10:06 -04:00
|
|
|
# static component
|
|
|
|
if frame is None: # bottom-most layer
|
2017-07-13 19:31:00 -04:00
|
|
|
frame = self.staticComponents[layerNo]
|
2017-06-05 05:54:58 -04:00
|
|
|
else:
|
2017-06-06 11:14:39 -04:00
|
|
|
frame = Image.alpha_composite(
|
2017-07-13 19:31:00 -04:00
|
|
|
frame, self.staticComponents[layerNo]
|
2017-07-09 01:10:06 -04:00
|
|
|
)
|
2017-05-31 03:15:09 -04:00
|
|
|
else:
|
2017-07-09 01:10:06 -04:00
|
|
|
# animated component
|
|
|
|
if frame is None: # bottom-most layer
|
|
|
|
frame = comp.frameRender(compNo, bgI)
|
2017-06-05 05:54:58 -04:00
|
|
|
else:
|
2017-06-06 11:14:39 -04:00
|
|
|
frame = Image.alpha_composite(
|
2017-07-09 01:10:06 -04:00
|
|
|
frame, comp.frameRender(compNo, bgI)
|
|
|
|
)
|
2015-03-03 14:11:55 -05:00
|
|
|
|
2017-07-09 01:10:06 -04:00
|
|
|
self.renderQueue.put([audioI, frame])
|
2017-05-31 03:15:09 -04:00
|
|
|
self.compositeQueue.task_done()
|
|
|
|
|
|
|
|
def renderDispatch(self):
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
|
|
|
Places audio data indices in the compositeQueue, to be used
|
|
|
|
by a renderNode later. All indices are multiples of self.sampleSize
|
|
|
|
sampleSize * frameNo = audioI, AKA audio data starting at frameNo
|
|
|
|
'''
|
2017-05-31 03:15:09 -04:00
|
|
|
print('Dispatching Frames for Compositing...')
|
|
|
|
|
2017-07-09 01:10:06 -04:00
|
|
|
for audioI in range(0, len(self.completeAudioArray), self.sampleSize):
|
|
|
|
self.compositeQueue.put(audioI)
|
2017-05-31 03:15:09 -04:00
|
|
|
|
|
|
|
def previewDispatch(self):
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
|
|
|
Grabs frames from the previewQueue, adds them to the checkerboard
|
|
|
|
and emits a final QImage to the MainWindow for the live preview
|
|
|
|
'''
|
2017-07-13 00:05:11 -04:00
|
|
|
background = Checkerboard(self.width, self.height)
|
2017-06-06 05:04:42 -04:00
|
|
|
|
2017-06-02 09:14:04 -04:00
|
|
|
while not self.stopped:
|
2017-07-09 01:10:06 -04:00
|
|
|
audioI, frame = self.previewQueue.get()
|
|
|
|
if time.time() - self.lastPreview >= 0.06 or audioI == 0:
|
|
|
|
image = Image.alpha_composite(background.copy(), frame)
|
2017-07-09 14:31:19 -04:00
|
|
|
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
|
2017-06-01 09:05:20 -04:00
|
|
|
self.lastPreview = time.time()
|
2017-05-31 03:15:09 -04:00
|
|
|
|
|
|
|
self.previewQueue.task_done()
|
|
|
|
|
2017-07-15 01:00:03 -04:00
|
|
|
@pyqtSlot()
|
|
|
|
def createVideo(self):
|
2017-07-09 14:31:19 -04:00
|
|
|
numpy.seterr(divide='ignore')
|
2017-06-03 01:07:30 -04:00
|
|
|
self.encoding.emit(True)
|
2017-07-09 14:31:19 -04:00
|
|
|
self.extraAudio = []
|
2017-07-09 21:27:29 -04:00
|
|
|
self.width = int(self.settings.value('outputWidth'))
|
|
|
|
self.height = int(self.settings.value('outputHeight'))
|
2017-07-09 14:31:19 -04:00
|
|
|
|
|
|
|
self.compositeQueue = Queue()
|
|
|
|
self.compositeQueue.maxsize = 20
|
|
|
|
self.renderQueue = PriorityQueue()
|
|
|
|
self.renderQueue.maxsize = 20
|
|
|
|
self.previewQueue = PriorityQueue()
|
|
|
|
|
|
|
|
self.reset()
|
2017-05-31 03:15:09 -04:00
|
|
|
progressBarValue = 0
|
|
|
|
self.progressBarUpdate.emit(progressBarValue)
|
|
|
|
|
2017-07-09 14:31:19 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
2017-07-09 21:27:29 -04:00
|
|
|
# READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
|
2017-07-09 14:31:19 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
|
|
|
|
self.progressBarSetText.emit("Loading audio file...")
|
2017-07-20 20:31:38 -04:00
|
|
|
audioFileTraits = readAudioFile(
|
2017-07-15 13:13:53 -04:00
|
|
|
self.inputFile, self
|
|
|
|
)
|
2017-07-20 20:31:38 -04:00
|
|
|
if audioFileTraits is None:
|
|
|
|
self.cancelExport()
|
|
|
|
return
|
|
|
|
self.completeAudioArray, duration = audioFileTraits
|
2017-05-31 03:15:09 -04:00
|
|
|
|
2017-07-09 14:31:19 -04:00
|
|
|
self.progressBarUpdate.emit(0)
|
|
|
|
self.progressBarSetText.emit("Starting components...")
|
2017-07-20 20:31:38 -04:00
|
|
|
canceledByComponent = False
|
2017-07-09 14:31:19 -04:00
|
|
|
print('Loaded Components:', ", ".join([
|
|
|
|
"%s) %s" % (num, str(component))
|
|
|
|
for num, component in enumerate(reversed(self.components))
|
|
|
|
]))
|
|
|
|
self.staticComponents = {}
|
2017-07-13 17:03:25 -04:00
|
|
|
for compNo, comp in enumerate(reversed(self.components)):
|
2017-07-09 21:27:29 -04:00
|
|
|
comp.preFrameRender(
|
2017-07-09 14:31:19 -04:00
|
|
|
worker=self,
|
|
|
|
completeAudioArray=self.completeAudioArray,
|
|
|
|
sampleSize=self.sampleSize,
|
|
|
|
progressBarUpdate=self.progressBarUpdate,
|
|
|
|
progressBarSetText=self.progressBarSetText
|
|
|
|
)
|
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
if 'error' in comp.properties:
|
2017-07-13 17:03:25 -04:00
|
|
|
self.cancel()
|
2017-07-11 06:06:22 -04:00
|
|
|
self.canceled = True
|
2017-07-20 20:31:38 -04:00
|
|
|
canceledByComponent = True
|
2017-07-11 06:06:22 -04:00
|
|
|
errMsg = "Component #%s encountered an error!" % compNo \
|
2017-07-20 20:31:38 -04:00
|
|
|
if comp.error is None else 'Component #%s (%s): %s' % (
|
2017-07-13 17:03:25 -04:00
|
|
|
str(compNo),
|
|
|
|
str(comp),
|
2017-07-20 20:31:38 -04:00
|
|
|
comp.error
|
2017-07-13 17:03:25 -04:00
|
|
|
)
|
2017-07-11 06:06:22 -04:00
|
|
|
self.parent.showMessage(
|
|
|
|
msg=errMsg,
|
|
|
|
icon='Warning',
|
|
|
|
parent=None # MainWindow is in a different thread
|
|
|
|
)
|
2017-07-13 17:03:25 -04:00
|
|
|
break
|
2017-07-20 20:31:38 -04:00
|
|
|
if 'static' in comp.properties:
|
2017-07-09 21:27:29 -04:00
|
|
|
self.staticComponents[compNo] = \
|
|
|
|
comp.frameRender(compNo, 0).copy()
|
2017-07-09 14:31:19 -04:00
|
|
|
|
2017-07-13 17:03:25 -04:00
|
|
|
if self.canceled:
|
2017-07-20 20:31:38 -04:00
|
|
|
if canceledByComponent:
|
|
|
|
print('Export cancelled by component #%s (%s): %s' % (
|
|
|
|
compNo, str(comp), comp.error
|
|
|
|
))
|
|
|
|
self.cancelExport()
|
2017-07-13 17:03:25 -04:00
|
|
|
return
|
|
|
|
|
2017-07-13 00:05:11 -04:00
|
|
|
# Merge consecutive static component frames together
|
2017-07-13 17:03:25 -04:00
|
|
|
for compNo in range(len(self.components)):
|
2017-07-13 00:05:11 -04:00
|
|
|
if compNo not in self.staticComponents \
|
2017-07-13 17:03:25 -04:00
|
|
|
or compNo + 1 not in self.staticComponents:
|
2017-07-13 00:05:11 -04:00
|
|
|
continue
|
2017-07-13 17:03:25 -04:00
|
|
|
self.staticComponents[compNo + 1] = Image.alpha_composite(
|
2017-07-13 00:05:11 -04:00
|
|
|
self.staticComponents.pop(compNo),
|
2017-07-13 17:03:25 -04:00
|
|
|
self.staticComponents[compNo + 1]
|
2017-07-13 00:05:11 -04:00
|
|
|
)
|
|
|
|
self.staticComponents[compNo] = None
|
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
ffmpegCommand = createFfmpegCommand(
|
|
|
|
self.inputFile, self.outputFile, self.components, duration
|
2017-07-15 01:00:03 -04:00
|
|
|
)
|
2017-07-13 14:46:22 -04:00
|
|
|
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
|
|
|
|
print('############################')
|
2017-07-04 19:52:52 -04:00
|
|
|
self.out_pipe = openPipe(
|
2017-07-09 14:31:19 -04:00
|
|
|
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
|
|
|
|
)
|
2017-05-31 03:15:09 -04:00
|
|
|
|
2017-07-09 14:31:19 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
# START CREATING THE VIDEO
|
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
|
2017-07-15 01:00:03 -04:00
|
|
|
# Make 2 or 3 renderNodes in new threads to create the frames
|
2017-06-22 18:40:34 -04:00
|
|
|
self.renderThreads = []
|
2017-07-15 01:00:03 -04:00
|
|
|
try:
|
|
|
|
numCpus = len(os.sched_getaffinity(0))
|
|
|
|
except:
|
|
|
|
numCpus = os.cpu_count()
|
|
|
|
|
|
|
|
for i in range(2 if numCpus <= 2 else 3):
|
2017-06-06 11:14:39 -04:00
|
|
|
self.renderThreads.append(
|
|
|
|
Thread(target=self.renderNode, name="Render Thread"))
|
2017-06-02 09:14:04 -04:00
|
|
|
self.renderThreads[i].daemon = True
|
|
|
|
self.renderThreads[i].start()
|
2017-05-31 03:15:09 -04:00
|
|
|
|
2017-06-06 11:14:39 -04:00
|
|
|
self.dispatchThread = Thread(
|
|
|
|
target=self.renderDispatch, name="Render Dispatch Thread")
|
2017-05-31 03:15:09 -04:00
|
|
|
self.dispatchThread.daemon = True
|
|
|
|
self.dispatchThread.start()
|
|
|
|
|
2017-07-09 14:31:19 -04:00
|
|
|
self.lastPreview = 0.0
|
2017-06-06 11:14:39 -04:00
|
|
|
self.previewDispatch = Thread(
|
|
|
|
target=self.previewDispatch, name="Render Dispatch Thread")
|
2017-05-31 03:15:09 -04:00
|
|
|
self.previewDispatch.daemon = True
|
|
|
|
self.previewDispatch.start()
|
|
|
|
|
2017-07-09 14:31:19 -04:00
|
|
|
# Begin piping into ffmpeg!
|
2017-05-31 03:15:09 -04:00
|
|
|
frameBuffer = {}
|
2017-07-09 14:31:19 -04:00
|
|
|
progressBarValue = 0
|
|
|
|
self.progressBarUpdate.emit(progressBarValue)
|
|
|
|
self.progressBarSetText.emit("Exporting video...")
|
2017-06-02 01:30:44 -04:00
|
|
|
if not self.canceled:
|
2017-07-09 01:10:06 -04:00
|
|
|
for audioI in range(
|
|
|
|
0, len(self.completeAudioArray), self.sampleSize):
|
2017-06-02 01:30:44 -04:00
|
|
|
while True:
|
2017-07-09 01:10:06 -04:00
|
|
|
if audioI in frameBuffer or self.canceled:
|
2017-06-02 01:30:44 -04:00
|
|
|
# if frame's in buffer, pipe it to ffmpeg
|
|
|
|
break
|
|
|
|
# else fetch the next frame & add to the buffer
|
2017-07-09 14:31:19 -04:00
|
|
|
audioI_, frame = self.renderQueue.get()
|
|
|
|
frameBuffer[audioI_] = frame
|
2017-06-02 01:30:44 -04:00
|
|
|
self.renderQueue.task_done()
|
2017-06-17 19:08:18 -04:00
|
|
|
if self.canceled:
|
|
|
|
break
|
2017-06-02 01:30:44 -04:00
|
|
|
|
|
|
|
try:
|
2017-07-09 01:10:06 -04:00
|
|
|
self.out_pipe.stdin.write(frameBuffer[audioI].tobytes())
|
2017-07-09 14:31:19 -04:00
|
|
|
self.previewQueue.put([audioI, frameBuffer.pop(audioI)])
|
2017-06-02 01:30:44 -04:00
|
|
|
except:
|
2017-05-31 05:01:18 -04:00
|
|
|
break
|
2017-05-31 03:15:09 -04:00
|
|
|
|
2017-06-02 01:30:44 -04:00
|
|
|
# increase progress bar value
|
2017-07-09 14:31:19 -04:00
|
|
|
completion = (audioI / len(self.completeAudioArray)) * 100
|
|
|
|
if progressBarValue + 1 <= completion:
|
|
|
|
progressBarValue = numpy.floor(completion)
|
2017-06-02 01:30:44 -04:00
|
|
|
self.progressBarUpdate.emit(progressBarValue)
|
2017-07-09 14:31:19 -04:00
|
|
|
self.progressBarSetText.emit(
|
|
|
|
"Exporting video: %s%%" % str(int(progressBarValue))
|
|
|
|
)
|
2017-05-31 03:15:09 -04:00
|
|
|
|
|
|
|
numpy.seterr(all='print')
|
|
|
|
|
2017-06-02 00:24:13 -04:00
|
|
|
self.out_pipe.stdin.close()
|
|
|
|
if self.out_pipe.stderr is not None:
|
|
|
|
print(self.out_pipe.stderr.read())
|
|
|
|
self.out_pipe.stderr.close()
|
|
|
|
self.error = True
|
2017-05-31 03:15:09 -04:00
|
|
|
# out_pipe.terminate() # don't terminate ffmpeg too early
|
2017-06-02 00:24:13 -04:00
|
|
|
self.out_pipe.wait()
|
|
|
|
if self.canceled:
|
|
|
|
print("Export Canceled")
|
2017-06-02 01:30:44 -04:00
|
|
|
try:
|
|
|
|
os.remove(self.outputFile)
|
|
|
|
except:
|
|
|
|
pass
|
2017-06-02 00:24:13 -04:00
|
|
|
self.progressBarUpdate.emit(0)
|
|
|
|
self.progressBarSetText.emit('Export Canceled')
|
|
|
|
else:
|
|
|
|
if self.error:
|
|
|
|
print("Export Failed")
|
|
|
|
self.progressBarUpdate.emit(0)
|
|
|
|
self.progressBarSetText.emit('Export Failed')
|
|
|
|
else:
|
|
|
|
print("Export Complete")
|
|
|
|
self.progressBarUpdate.emit(100)
|
|
|
|
self.progressBarSetText.emit('Export Complete')
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-06-02 00:24:13 -04:00
|
|
|
self.error = False
|
|
|
|
self.canceled = False
|
2017-06-02 09:14:04 -04:00
|
|
|
self.stopped = True
|
2017-06-03 01:07:30 -04:00
|
|
|
self.encoding.emit(False)
|
2017-05-31 03:15:09 -04:00
|
|
|
self.videoCreated.emit()
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
def cancelExport(self):
|
|
|
|
self.progressBarUpdate.emit(0)
|
|
|
|
self.progressBarSetText.emit('Export Canceled')
|
|
|
|
self.encoding.emit(False)
|
|
|
|
self.videoCreated.emit()
|
|
|
|
|
2017-06-02 04:30:51 -04:00
|
|
|
def updateProgress(self, pStr, pVal):
|
|
|
|
self.progressBarValue.emit(pVal)
|
|
|
|
self.progressBarSetText.emit(pStr)
|
|
|
|
|
2017-06-02 01:30:44 -04:00
|
|
|
def cancel(self):
|
|
|
|
self.canceled = True
|
2017-07-13 17:03:25 -04:00
|
|
|
self.stopped = True
|
2017-06-02 01:30:44 -04:00
|
|
|
self.core.cancel()
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-06-02 01:30:44 -04:00
|
|
|
for comp in self.components:
|
|
|
|
comp.cancel()
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-06-02 01:30:44 -04:00
|
|
|
try:
|
|
|
|
self.out_pipe.send_signal(signal.SIGINT)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
self.core.reset()
|
|
|
|
self.canceled = False
|
|
|
|
for comp in self.components:
|
|
|
|
comp.reset()
|