starting work on Waveform component
split Video class out of Video component for reuse in Waveform
This commit is contained in:
parent
6f8f178778
commit
c1457b6dad
|
@ -11,3 +11,5 @@ env/*
|
|||
*.tar.*
|
||||
*.exe
|
||||
ffmpeg
|
||||
*.bak
|
||||
*~
|
||||
|
|
|
@ -197,7 +197,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
|||
'''
|
||||
Must call super() when subclassing
|
||||
Triggered only before a video is exported (video_thread.py)
|
||||
self.worker = the video thread worker
|
||||
self.audioFile = filepath to the main input audio file
|
||||
self.completeAudioArray = a list of audio samples
|
||||
self.sampleSize = number of audio samples per video frame
|
||||
self.progressBarUpdate = signal to set progress bar number
|
||||
|
@ -436,7 +436,7 @@ class ComponentError(RuntimeError):
|
|||
import sys
|
||||
if sys.exc_info()[0] is not None:
|
||||
string = (
|
||||
"%s component's %s encountered %s %s." % (
|
||||
"%s component's %s encountered %s %s: %s" % (
|
||||
caller.__class__.name,
|
||||
name,
|
||||
'an' if any([
|
||||
|
@ -444,12 +444,13 @@ class ComponentError(RuntimeError):
|
|||
for vowel in ('A', 'I')
|
||||
]) else 'a',
|
||||
sys.exc_info()[0].__name__,
|
||||
str(sys.exc_info()[1])
|
||||
)
|
||||
)
|
||||
detail = formatTraceback(sys.exc_info()[2])
|
||||
else:
|
||||
string = name
|
||||
detail = "Methods:\n%s" % (
|
||||
detail = "Attributes:\n%s" % (
|
||||
"\n".join(
|
||||
[m for m in dir(caller) if not m.startswith('_')]
|
||||
)
|
||||
|
|
|
@ -1,103 +1,13 @@
|
|||
from PIL import Image, ImageDraw
|
||||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
import signal
|
||||
import threading
|
||||
from queue import PriorityQueue
|
||||
|
||||
from component import Component, ComponentError
|
||||
from toolkit.frame import BlankFrame
|
||||
from toolkit.ffmpeg import testAudioStream
|
||||
from toolkit import openPipe, checkOutput
|
||||
|
||||
|
||||
class Video:
|
||||
'''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 = [
|
||||
'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
|
||||
'videoPath',
|
||||
'width',
|
||||
'height',
|
||||
'scale', # percentage scale
|
||||
'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'
|
||||
if 'loopVideo' in kwargs and kwargs['loopVideo']:
|
||||
self.loopValue = '-1'
|
||||
else:
|
||||
self.loopValue = '0'
|
||||
self.command = [
|
||||
self.ffmpeg,
|
||||
'-thread_queue_size', '512',
|
||||
'-r', str(self.frameRate),
|
||||
'-stream_loop', self.loopValue,
|
||||
'-i', self.videoPath,
|
||||
'-f', 'image2pipe',
|
||||
'-pix_fmt', 'rgba',
|
||||
'-filter_complex', '[0:v] scale=%s:%s' % scale(
|
||||
self.scale, self.width, self.height, str),
|
||||
'-vcodec', 'rawvideo', '-',
|
||||
]
|
||||
|
||||
self.frameBuffer = PriorityQueue()
|
||||
self.frameBuffer.maxsize = self.frameRate
|
||||
self.finishedFrames = {}
|
||||
|
||||
self.thread = threading.Thread(
|
||||
target=self.fillBuffer,
|
||||
name='Video 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 finalizeFrame(
|
||||
self.component, image, self.width, self.height)
|
||||
|
||||
i, image = self.frameBuffer.get()
|
||||
self.finishedFrames[i] = image
|
||||
self.frameBuffer.task_done()
|
||||
|
||||
def fillBuffer(self):
|
||||
self.pipe = openPipe(
|
||||
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, 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:
|
||||
Video.threadError = ComponentError(self.component, 'video')
|
||||
break
|
||||
|
||||
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
|
||||
if len(self.currentFrame) != 0:
|
||||
self.frameBuffer.put((self.frameNo, self.currentFrame))
|
||||
self.lastFrame = self.currentFrame
|
||||
from toolkit.ffmpeg import testAudioStream, FfmpegVideo
|
||||
from toolkit import openPipe, closePipe, checkOutput, scale
|
||||
|
||||
|
||||
class Component(Component):
|
||||
|
@ -182,22 +92,21 @@ class Component(Component):
|
|||
def preFrameRender(self, **kwargs):
|
||||
super().preFrameRender(**kwargs)
|
||||
self.updateChunksize()
|
||||
self.video = Video(
|
||||
ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
|
||||
self.video = FfmpegVideo(
|
||||
inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
|
||||
width=self.width, height=self.height, chunkSize=self.chunkSize,
|
||||
frameRate=int(self.settings.value("outputFrameRate")),
|
||||
parent=self.parent, loopVideo=self.loopVideo,
|
||||
component=self, scale=self.scale
|
||||
component=self
|
||||
) if os.path.exists(self.videoPath) else None
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
if Video.threadError is not None:
|
||||
raise Video.threadError
|
||||
return self.video.frame(frameNo)
|
||||
if FfmpegVideo.threadError is not None:
|
||||
raise FfmpegVideo.threadError
|
||||
return self.finalizeFrame(self.video.frame(frameNo))
|
||||
|
||||
def postFrameRender(self):
|
||||
self.video.pipe.stdout.close()
|
||||
self.video.pipe.send_signal(signal.SIGINT)
|
||||
closePipe(self.video.pipe)
|
||||
|
||||
def pickVideo(self):
|
||||
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
||||
|
@ -220,23 +129,30 @@ class Component(Component):
|
|||
'-i', self.videoPath,
|
||||
'-f', 'image2pipe',
|
||||
'-pix_fmt', 'rgba',
|
||||
'-filter_complex', '[0:v] scale=%s:%s' % scale(
|
||||
self.scale, width, height, str),
|
||||
]
|
||||
command.extend(self.makeFfmpegFilter())
|
||||
command.extend([
|
||||
'-vcodec', 'rawvideo', '-',
|
||||
'-ss', '90',
|
||||
'-vframes', '1',
|
||||
]
|
||||
'-frames:v', '1',
|
||||
])
|
||||
pipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||
)
|
||||
byteFrame = pipe.stdout.read(self.chunkSize)
|
||||
pipe.stdout.close()
|
||||
pipe.send_signal(signal.SIGINT)
|
||||
closePipe(pipe)
|
||||
|
||||
frame = finalizeFrame(self, byteFrame, width, height)
|
||||
frame = self.finalizeFrame(byteFrame)
|
||||
return frame
|
||||
|
||||
def makeFfmpegFilter(self):
|
||||
return [
|
||||
'-filter_complex',
|
||||
'[0:v] scale=%s:%s' % scale(
|
||||
self.scale, self.width, self.height, str),
|
||||
]
|
||||
|
||||
def updateChunksize(self):
|
||||
if self.scale != 100 and not self.distort:
|
||||
width, height = scale(self.scale, self.width, self.height, int)
|
||||
|
@ -268,44 +184,32 @@ class Component(Component):
|
|||
print('Load a video:\n path=/filepath/to/video.mp4')
|
||||
print('Using audio:\n path=/filepath/to/video.mp4 audio')
|
||||
|
||||
def finalizeFrame(self, imageData):
|
||||
try:
|
||||
if self.distort:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
(self.width, self.height),
|
||||
imageData)
|
||||
else:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
scale(self.scale, self.width, self.height, int),
|
||||
imageData)
|
||||
|
||||
def scale(scale, width, height, returntype=None):
|
||||
width = (float(width) / 100.0) * float(scale)
|
||||
height = (float(height) / 100.0) * float(scale)
|
||||
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)
|
||||
except ValueError:
|
||||
print(
|
||||
'### BAD VIDEO SELECTED ###\n'
|
||||
'Video will not export with these settings'
|
||||
)
|
||||
self.badVideo = True
|
||||
return BlankFrame(self.width, self.height)
|
||||
|
||||
|
||||
def finalizeFrame(self, imageData, width, height):
|
||||
try:
|
||||
if self.distort:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
(width, height),
|
||||
imageData)
|
||||
if self.scale != 100 \
|
||||
or self.xPosition != 0 or self.yPosition != 0:
|
||||
frame = BlankFrame(self.width, self.height)
|
||||
frame.paste(image, box=(self.xPosition, self.yPosition))
|
||||
else:
|
||||
image = Image.frombytes(
|
||||
'RGBA',
|
||||
scale(self.scale, width, height, int),
|
||||
imageData)
|
||||
|
||||
except ValueError:
|
||||
print(
|
||||
'### BAD VIDEO SELECTED ###\n'
|
||||
'Video will not export with these settings'
|
||||
)
|
||||
self.badVideo = True
|
||||
return BlankFrame(width, height)
|
||||
|
||||
if self.scale != 100 \
|
||||
or self.xPosition != 0 or self.yPosition != 0:
|
||||
frame = BlankFrame(width, height)
|
||||
frame.paste(image, box=(self.xPosition, self.yPosition))
|
||||
else:
|
||||
frame = image
|
||||
self.badVideo = False
|
||||
return frame
|
||||
frame = image
|
||||
self.badVideo = False
|
||||
return frame
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
from PIL import Image
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
from PyQt5.QtGui import QColor
|
||||
import os
|
||||
import math
|
||||
import subprocess
|
||||
|
||||
from component import Component, ComponentError
|
||||
from toolkit.frame import BlankFrame
|
||||
from toolkit import openPipe, checkOutput, rgbFromString
|
||||
from toolkit.ffmpeg import FfmpegVideo
|
||||
|
||||
|
||||
class Component(Component):
|
||||
name = 'Waveform'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, *args):
|
||||
self.color = (255, 255, 255)
|
||||
super().widget(*args)
|
||||
|
||||
self.page.lineEdit_color.setText('%s,%s,%s' % self.color)
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*self.color1).name()
|
||||
self.page.lineEdit_color.setStylesheet(btnStyle)
|
||||
self.page.pushButton_color.clicked.connect(lambda: self.pickColor())
|
||||
|
||||
self.trackWidgets(
|
||||
{
|
||||
'mode': self.page.comboBox_mode,
|
||||
'x': self.page.spinBox_x,
|
||||
'y': self.page.spinBox_y,
|
||||
'mirror': self.page.checkBox_mirror,
|
||||
'scale': self.page.spinBox_scale,
|
||||
}
|
||||
)
|
||||
|
||||
def update(self):
|
||||
self.color = rgbFromString(self.page.lineEdit_color.text())
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*self.color).name()
|
||||
self.page.pushButton_color.setStyleSheet(btnStyle)
|
||||
super().update()
|
||||
|
||||
def previewRender(self):
|
||||
self.updateChunksize()
|
||||
frame = self.getPreviewFrame(self.width, self.height)
|
||||
if not frame:
|
||||
return BlankFrame(self.width, self.height)
|
||||
else:
|
||||
return frame
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
super().preFrameRender(**kwargs)
|
||||
self.updateChunksize()
|
||||
self.video = FfmpegVideo(
|
||||
inputPath=self.audioFile,
|
||||
filter_=makeFfmpegFilter(),
|
||||
width=self.width, height=self.height,
|
||||
chunkSize=self.chunkSize,
|
||||
frameRate=int(self.settings.value("outputFrameRate")),
|
||||
parent=self.parent, component=self,
|
||||
)
|
||||
|
||||
def frameRender(self, frameNo):
|
||||
if FfmpegVideo.threadError is not None:
|
||||
raise FfmpegVideo.threadError
|
||||
return finalizeFrame(self.video.frame(frameNo))
|
||||
|
||||
def postFrameRender(self):
|
||||
closePipe(self.video.pipe)
|
||||
|
||||
def getPreviewFrame(self, width, height):
|
||||
inputFile = self.parent.window.lineEdit_audioFile.text()
|
||||
if not inputFile or not os.path.exists(inputFile):
|
||||
return
|
||||
|
||||
command = [
|
||||
self.core.FFMPEG_BIN,
|
||||
'-thread_queue_size', '512',
|
||||
'-i', inputFile,
|
||||
'-f', 'image2pipe',
|
||||
'-pix_fmt', 'rgba',
|
||||
]
|
||||
command.extend(self.makeFfmpegFilter())
|
||||
command.extend([
|
||||
'-vcodec', 'rawvideo', '-',
|
||||
'-ss', '90',
|
||||
'-frames:v', '1',
|
||||
])
|
||||
pipe = openPipe(
|
||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||
)
|
||||
byteFrame = pipe.stdout.read(self.chunkSize)
|
||||
closePipe(pipe)
|
||||
|
||||
frame = finalizeFrame(self, byteFrame, width, height)
|
||||
return frame
|
||||
|
||||
def makeFfmpegFilter(self):
|
||||
w, h = scale(self.scale, self.width, self.height, str)
|
||||
return [
|
||||
'-filter_complex',
|
||||
'[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % (
|
||||
w, h, self.mode,
|
||||
),
|
||||
'-map', '[v]',
|
||||
'-map', '0:a',
|
||||
]
|
||||
|
||||
def updateChunksize(self):
|
||||
if self.scale != 100:
|
||||
width, height = scale(self.scale, self.width, self.height, int)
|
||||
else:
|
||||
width, height = self.width, self.height
|
||||
self.chunkSize = 4 * width * height
|
||||
|
||||
|
||||
def scale(scale, width, height, returntype=None):
|
||||
width = (float(width) / 100.0) * float(scale)
|
||||
height = (float(height) / 100.0) * float(scale)
|
||||
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 finalizeFrame(self, imageData, width, height):
|
||||
# frombytes goes here
|
||||
if self.scale != 100 \
|
||||
or self.x != 0 or self.y != 0:
|
||||
frame = BlankFrame(width, height)
|
||||
frame.paste(image, box=(self.x, self.y))
|
||||
else:
|
||||
frame = image
|
||||
return frame
|
|
@ -0,0 +1,283 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>586</width>
|
||||
<height>197</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>197</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_textColor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>31</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_mode">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Cline</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Line</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>P2p</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Point</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_9">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>5</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_xTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_x">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_yTitleAlign">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_y">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-10000</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Wave Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="lineEdit_color"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_color">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_mirror">
|
||||
<property name="text">
|
||||
<string>Mirror</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="spinBox_scale">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::UpDownArrows</enum>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string>%</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>400</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -6,9 +6,22 @@ import string
|
|||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
import math
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def scale(scale, width, height, returntype=None):
|
||||
width = (float(width) / 100.0) * float(scale)
|
||||
height = (float(height) / 100.0) * float(scale)
|
||||
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 badName(name):
|
||||
'''Returns whether a name contains non-alphanumeric chars'''
|
||||
return any([letter in string.punctuation for letter in name])
|
||||
|
@ -34,29 +47,35 @@ 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
|
||||
@pipeWrapper
|
||||
def openPipe(commandList, **kwargs):
|
||||
return subprocess.Popen(commandList, **kwargs)
|
||||
|
||||
def closePipe(pipe):
|
||||
pipe.stdout.close()
|
||||
pipe.send_signal(signal.SIGINT)
|
||||
|
||||
def disableWhenEncoding(func):
|
||||
def decorator(self, *args, **kwargs):
|
||||
|
|
|
@ -5,11 +5,110 @@ import numpy
|
|||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
from queue import PriorityQueue
|
||||
|
||||
import core
|
||||
from toolkit.common import checkOutput, openPipe
|
||||
|
||||
|
||||
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([
|
||||
'-vcodec', '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):
|
||||
self.pipe = openPipe(
|
||||
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, 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:
|
||||
Video.threadError = ComponentError(self.component, 'video')
|
||||
break
|
||||
|
||||
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
|
||||
if len(self.currentFrame) != 0:
|
||||
self.frameBuffer.put((self.frameNo, self.currentFrame))
|
||||
self.lastFrame = self.currentFrame
|
||||
|
||||
|
||||
def findFfmpeg():
|
||||
if getattr(sys, 'frozen', False):
|
||||
# The application is frozen
|
||||
|
|
|
@ -153,7 +153,7 @@ class Worker(QtCore.QObject):
|
|||
for compNo, comp in enumerate(reversed(self.components)):
|
||||
try:
|
||||
comp.preFrameRender(
|
||||
worker=self,
|
||||
audioFile=self.inputFile,
|
||||
completeAudioArray=self.completeAudioArray,
|
||||
sampleSize=self.sampleSize,
|
||||
progressBarUpdate=self.progressBarUpdate,
|
||||
|
|
Reference in New Issue