aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/video.py198
-rw-r--r--src/components/waveform.py139
-rw-r--r--src/components/waveform.ui283
3 files changed, 473 insertions, 147 deletions
diff --git a/src/components/video.py b/src/components/video.py
index b2487c1..d3460ff 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -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)
+
+ except ValueError:
+ print(
+ '### BAD VIDEO SELECTED ###\n'
+ 'Video will not export with these settings'
+ )
+ self.badVideo = True
+ return BlankFrame(self.width, self.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):
- 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
diff --git a/src/components/waveform.py b/src/components/waveform.py
new file mode 100644
index 0000000..487a3bb
--- /dev/null
+++ b/src/components/waveform.py
@@ -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
diff --git a/src/components/waveform.ui b/src/components/waveform.ui
new file mode 100644
index 0000000..5d62150
--- /dev/null
+++ b/src/components/waveform.ui
@@ -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>