aboutsummaryrefslogtreecommitdiff
path: root/src/toolkit
diff options
context:
space:
mode:
Diffstat (limited to 'src/toolkit')
-rw-r--r--src/toolkit/common.py86
-rw-r--r--src/toolkit/ffmpeg.py140
-rw-r--r--src/toolkit/frame.py12
3 files changed, 204 insertions, 34 deletions
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 251a2c1..eba57d9 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -34,30 +34,28 @@ def appendUppercase(lst):
lst.append(form.upper())
return lst
-
-def hideCmdWin(func):
- ''' Stops CMD window from appearing on Windows.
- Adapted from here: http://code.activestate.com/recipes/409002/
- '''
- def decorator(commandList, **kwargs):
+def pipeWrapper(func):
+ '''A decorator to insert proper kwargs into Popen objects.'''
+ def pipeWrapper(commandList, **kwargs):
if sys.platform == 'win32':
+ # Stop CMD window from appearing on Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
kwargs['startupinfo'] = startupinfo
+
+ if 'bufsize' not in kwargs:
+ kwargs['bufsize'] = 10**8
+ if 'stdin' not in kwargs:
+ kwargs['stdin'] = subprocess.DEVNULL
return func(commandList, **kwargs)
- return decorator
+ return pipeWrapper
-@hideCmdWin
+@pipeWrapper
def checkOutput(commandList, **kwargs):
return subprocess.check_output(commandList, **kwargs)
-@hideCmdWin
-def openPipe(commandList, **kwargs):
- return subprocess.Popen(commandList, **kwargs)
-
-
def disableWhenEncoding(func):
def decorator(self, *args, **kwargs):
if self.encoding:
@@ -76,25 +74,6 @@ def disableWhenOpeningProject(func):
return decorator
-def pickColor():
- '''
- Use color picker to get color input from the user,
- and return this as an RGB string and QPushButton stylesheet.
- In a subclass apply stylesheet to any color selection widgets
- '''
- dialog = QtWidgets.QColorDialog()
- dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
- color = dialog.getColor()
- if color.isValid():
- RGBstring = '%s,%s,%s' % (
- str(color.red()), str(color.green()), str(color.blue()))
- btnStyle = "QPushButton{background-color: %s; outline: none;}" \
- % color.name()
- return RGBstring, btnStyle
- else:
- return None, None
-
-
def rgbFromString(string):
'''Turns an RGB string like "255, 255, 255" into a tuple'''
try:
@@ -115,3 +94,46 @@ def formatTraceback(tb=None):
import sys
tb = sys.exc_info()[2]
return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb))
+
+
+def connectWidget(widget, func):
+ if type(widget) == QtWidgets.QLineEdit:
+ widget.textChanged.connect(func)
+ elif type(widget) == QtWidgets.QSpinBox \
+ or type(widget) == QtWidgets.QDoubleSpinBox:
+ widget.valueChanged.connect(func)
+ elif type(widget) == QtWidgets.QCheckBox:
+ widget.stateChanged.connect(func)
+ elif type(widget) == QtWidgets.QComboBox:
+ widget.currentIndexChanged.connect(func)
+ else:
+ return False
+ return True
+
+
+def setWidgetValue(widget, val):
+ '''Generic setValue method for use with any typical QtWidget'''
+ if type(widget) == QtWidgets.QLineEdit:
+ widget.setText(val)
+ elif type(widget) == QtWidgets.QSpinBox \
+ or type(widget) == QtWidgets.QDoubleSpinBox:
+ widget.setValue(val)
+ elif type(widget) == QtWidgets.QCheckBox:
+ widget.setChecked(val)
+ elif type(widget) == QtWidgets.QComboBox:
+ widget.setCurrentIndex(val)
+ else:
+ return False
+ return True
+
+
+def getWidgetValue(widget):
+ if type(widget) == QtWidgets.QLineEdit:
+ return widget.text()
+ elif type(widget) == QtWidgets.QSpinBox \
+ or type(widget) == QtWidgets.QDoubleSpinBox:
+ return widget.value()
+ elif type(widget) == QtWidgets.QCheckBox:
+ return widget.isChecked()
+ elif type(widget) == QtWidgets.QComboBox:
+ return widget.currentIndex()
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index b8bc679..3421049 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -5,9 +5,133 @@ import numpy
import sys
import os
import subprocess
+import threading
+import signal
+from queue import PriorityQueue
import core
-from toolkit.common import checkOutput, openPipe
+from toolkit.common import checkOutput, pipeWrapper
+from component import ComponentError
+
+
+class FfmpegVideo:
+ '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
+
+ # error from the thread used to fill the buffer
+ threadError = None
+
+ def __init__(self, **kwargs):
+ mandatoryArgs = [
+ 'inputPath',
+ 'filter_',
+ 'width',
+ 'height',
+ 'frameRate', # frames per second
+ 'chunkSize', # number of bytes in one frame
+ 'parent', # mainwindow object
+ 'component', # component object
+ ]
+ for arg in mandatoryArgs:
+ setattr(self, arg, kwargs[arg])
+
+ self.frameNo = -1
+ self.currentFrame = 'None'
+ self.map_ = None
+
+ if 'loopVideo' in kwargs and kwargs['loopVideo']:
+ self.loopValue = '-1'
+ else:
+ self.loopValue = '0'
+ if 'filter_' in kwargs:
+ if kwargs['filter_'][0] != '-filter_complex':
+ kwargs['filter_'].insert(0, '-filter_complex')
+ else:
+ kwargs['filter_'] = None
+
+ self.command = [
+ core.Core.FFMPEG_BIN,
+ '-thread_queue_size', '512',
+ '-r', str(self.frameRate),
+ '-stream_loop', self.loopValue,
+ '-i', self.inputPath,
+ '-f', 'image2pipe',
+ '-pix_fmt', 'rgba',
+ ]
+ if type(kwargs['filter_']) is list:
+ self.command.extend(
+ kwargs['filter_']
+ )
+ self.command.extend([
+ '-codec:v', 'rawvideo', '-',
+ ])
+
+ self.frameBuffer = PriorityQueue()
+ self.frameBuffer.maxsize = self.frameRate
+ self.finishedFrames = {}
+
+ self.thread = threading.Thread(
+ target=self.fillBuffer,
+ name='FFmpeg Frame-Fetcher'
+ )
+ self.thread.daemon = True
+ self.thread.start()
+
+ def frame(self, num):
+ while True:
+ if num in self.finishedFrames:
+ image = self.finishedFrames.pop(num)
+ return image
+
+ i, image = self.frameBuffer.get()
+ self.finishedFrames[i] = image
+ self.frameBuffer.task_done()
+
+ def fillBuffer(self):
+ logFilename = os.path.join(
+ core.Core.dataDir, 'extra_%s.log' % str(self.component.compPos))
+ with open(logFilename, 'w') as log:
+ log.write(" ".join(self.command) + '\n\n')
+ with open(logFilename, 'a') as log:
+ self.pipe = openPipe(
+ self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
+ stderr=log, bufsize=10**8
+ )
+ while True:
+ if self.parent.canceled:
+ break
+ self.frameNo += 1
+
+ # If we run out of frames, use the last good frame and loop.
+ try:
+ if len(self.currentFrame) == 0:
+ self.frameBuffer.put((self.frameNo-1, self.lastFrame))
+ continue
+ except AttributeError:
+ FfmpegVideo.threadError = ComponentError(
+ self.component, 'video',
+ "Video seemed playable but wasn't."
+ )
+ break
+
+ try:
+ self.currentFrame = self.pipe.stdout.read(self.chunkSize)
+ except ValueError:
+ FfmpegVideo.threadError = ComponentError(
+ self.component, 'video')
+
+ if len(self.currentFrame) != 0:
+ self.frameBuffer.put((self.frameNo, self.currentFrame))
+ self.lastFrame = self.currentFrame
+
+
+@pipeWrapper
+def openPipe(commandList, **kwargs):
+ return subprocess.Popen(commandList, **kwargs)
+
+
+def closePipe(pipe):
+ pipe.stdout.close()
+ pipe.send_signal(signal.SIGINT)
def findFfmpeg():
@@ -248,7 +372,12 @@ def getAudioDuration(filename):
except subprocess.CalledProcessError as ex:
fileInfo = ex.output
- info = fileInfo.decode("utf-8").split('\n')
+ try:
+ info = fileInfo.decode("utf-8").split('\n')
+ except UnicodeDecodeError as e:
+ print('Unicode error:', str(e))
+ return False
+
for line in info:
if 'Duration' in line:
d = line.split(',')[0]
@@ -321,3 +450,10 @@ def readAudioFile(filename, videoWorker):
completeAudioArray = completeAudioArrayCopy
return (completeAudioArray, duration)
+
+
+def exampleSound():
+ return (
+ 'aevalsrc=tan(random(1)*PI*t)*sin(random(0)*2*PI*t),'
+ 'apulsator=offset_l=0.5:offset_r=0.5,'
+ )
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index b66e037..c007188 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -6,6 +6,7 @@ from PIL import Image
from PIL.ImageQt import ImageQt
import sys
import os
+import math
import core
@@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor):
super().__init__(b, g, r, a)
+def scale(scalePercent, width, height, returntype=None):
+ width = (float(width) / 100.0) * float(scalePercent)
+ height = (float(height) / 100.0) * float(scalePercent)
+ if returntype == str:
+ return (str(math.ceil(width)), str(math.ceil(height)))
+ elif returntype == int:
+ return (math.ceil(width), math.ceil(height))
+ else:
+ return (width, height)
+
+
def defaultSize(framefunc):
'''Makes width/height arguments optional'''
def decorator(*args):