2017-07-29 13:08:28 -04:00
|
|
|
from PIL import Image
|
2017-06-24 23:12:41 -04:00
|
|
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
2017-06-06 11:14:39 -04:00
|
|
|
import os
|
2017-06-24 23:12:41 -04:00
|
|
|
import math
|
2017-06-06 11:14:39 -04:00
|
|
|
import subprocess
|
2017-07-02 20:46:48 -04:00
|
|
|
|
2017-07-29 20:27:46 -04:00
|
|
|
from component import Component
|
|
|
|
from toolkit.frame import BlankFrame, scale
|
|
|
|
from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
|
|
|
|
from toolkit import checkOutput
|
2017-06-06 01:40:26 -04:00
|
|
|
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-02 20:46:48 -04:00
|
|
|
class Component(Component):
|
2017-07-20 20:31:38 -04:00
|
|
|
name = 'Video'
|
|
|
|
version = '1.0.0'
|
2017-06-12 22:34:37 -04:00
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
def widget(self, *args):
|
2017-06-03 22:58:40 -04:00
|
|
|
self.videoPath = ''
|
2017-07-13 17:03:25 -04:00
|
|
|
self.badVideo = False
|
2017-07-15 13:13:53 -04:00
|
|
|
self.badAudio = False
|
2017-06-03 22:58:40 -04:00
|
|
|
self.x = 0
|
|
|
|
self.y = 0
|
2017-06-06 02:57:48 -04:00
|
|
|
self.loopVideo = False
|
2017-07-23 01:53:54 -04:00
|
|
|
super().widget(*args)
|
|
|
|
self.page.pushButton_video.clicked.connect(self.pickVideo)
|
|
|
|
self.trackWidgets(
|
|
|
|
{
|
|
|
|
'videoPath': self.page.lineEdit_video,
|
|
|
|
'loopVideo': self.page.checkBox_loop,
|
|
|
|
'useAudio': self.page.checkBox_useAudio,
|
|
|
|
'distort': self.page.checkBox_distort,
|
|
|
|
'scale': self.page.spinBox_scale,
|
|
|
|
'volume': self.page.spinBox_volume,
|
|
|
|
'xPosition': self.page.spinBox_x,
|
|
|
|
'yPosition': self.page.spinBox_y,
|
|
|
|
}, presetNames={
|
|
|
|
'videoPath': 'video',
|
|
|
|
'loopVideo': 'loop',
|
|
|
|
'xPosition': 'x',
|
|
|
|
'yPosition': 'y',
|
2017-08-01 21:57:36 -04:00
|
|
|
}, relativeWidgets={
|
|
|
|
'xPosition': 'x',
|
|
|
|
'yPosition': 'y',
|
2017-07-23 01:53:54 -04:00
|
|
|
}
|
|
|
|
)
|
2017-06-03 20:39:32 -04:00
|
|
|
|
|
|
|
def update(self):
|
2017-07-23 01:53:54 -04:00
|
|
|
if self.page.checkBox_useAudio.isChecked():
|
2017-07-16 14:06:11 -04:00
|
|
|
self.page.label_volume.setEnabled(True)
|
|
|
|
self.page.spinBox_volume.setEnabled(True)
|
|
|
|
else:
|
|
|
|
self.page.label_volume.setEnabled(False)
|
|
|
|
self.page.spinBox_volume.setEnabled(False)
|
2017-06-13 22:47:18 -04:00
|
|
|
super().update()
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
def previewRender(self):
|
|
|
|
self.updateChunksize()
|
|
|
|
frame = self.getPreviewFrame(self.width, self.height)
|
2017-06-06 08:55:22 -04:00
|
|
|
if not frame:
|
2017-07-27 17:49:08 -04:00
|
|
|
return BlankFrame(self.width, self.height)
|
2017-06-06 08:55:22 -04:00
|
|
|
else:
|
|
|
|
return frame
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-11 06:06:22 -04:00
|
|
|
def properties(self):
|
|
|
|
props = []
|
2017-07-27 22:15:41 -04:00
|
|
|
if hasattr(self.parent, 'window'):
|
|
|
|
outputFile = self.parent.window.lineEdit_outputFile.text()
|
|
|
|
else:
|
|
|
|
outputFile = str(self.parent.args.output)
|
2017-07-25 22:02:47 -04:00
|
|
|
|
|
|
|
if not self.videoPath:
|
|
|
|
self.lockError("There is no video selected.")
|
|
|
|
elif self.badVideo:
|
|
|
|
self.lockError("Could not identify an audio stream in this video.")
|
|
|
|
elif not os.path.exists(self.videoPath):
|
|
|
|
self.lockError("The video selected does not exist!")
|
2017-07-27 22:15:41 -04:00
|
|
|
elif os.path.realpath(self.videoPath) == os.path.realpath(outputFile):
|
2017-07-25 22:02:47 -04:00
|
|
|
self.lockError("Input and output paths match.")
|
2017-07-15 01:00:03 -04:00
|
|
|
|
|
|
|
if self.useAudio:
|
|
|
|
props.append('audio')
|
2017-07-25 22:02:47 -04:00
|
|
|
if not testAudioStream(self.videoPath) \
|
|
|
|
and self.error() is None:
|
|
|
|
self.lockError(
|
|
|
|
"Could not identify an audio stream in this video.")
|
2017-07-15 01:00:03 -04:00
|
|
|
|
2017-07-11 06:06:22 -04:00
|
|
|
return props
|
|
|
|
|
|
|
|
def audio(self):
|
2017-07-16 14:06:11 -04:00
|
|
|
params = {}
|
|
|
|
if self.volume != 1.0:
|
|
|
|
params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume)
|
|
|
|
return (self.videoPath, params)
|
2017-07-11 06:06:22 -04:00
|
|
|
|
2017-06-04 00:19:10 -04:00
|
|
|
def preFrameRender(self, **kwargs):
|
2017-06-04 13:00:36 -04:00
|
|
|
super().preFrameRender(**kwargs)
|
2017-07-27 17:49:08 -04:00
|
|
|
self.updateChunksize()
|
2017-07-29 13:08:28 -04:00
|
|
|
self.video = FfmpegVideo(
|
|
|
|
inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
|
2017-07-27 17:49:08 -04:00
|
|
|
width=self.width, height=self.height, chunkSize=self.chunkSize,
|
2017-07-24 21:22:04 -04:00
|
|
|
frameRate=int(self.settings.value("outputFrameRate")),
|
|
|
|
parent=self.parent, loopVideo=self.loopVideo,
|
2017-07-29 13:08:28 -04:00
|
|
|
component=self
|
2017-07-24 21:22:04 -04:00
|
|
|
) if os.path.exists(self.videoPath) else None
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
def frameRender(self, frameNo):
|
2017-07-29 13:08:28 -04:00
|
|
|
if FfmpegVideo.threadError is not None:
|
|
|
|
raise FfmpegVideo.threadError
|
|
|
|
return self.finalizeFrame(self.video.frame(frameNo))
|
2017-07-27 17:49:08 -04:00
|
|
|
|
2017-07-27 22:15:41 -04:00
|
|
|
def postFrameRender(self):
|
2017-07-29 13:08:28 -04:00
|
|
|
closePipe(self.video.pipe)
|
2017-06-03 20:39:32 -04:00
|
|
|
|
2017-06-03 22:58:40 -04:00
|
|
|
def pickVideo(self):
|
2017-06-25 14:27:56 -04:00
|
|
|
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
2017-06-23 23:00:24 -04:00
|
|
|
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
2017-06-06 11:14:39 -04:00
|
|
|
self.page, "Choose Video",
|
2017-07-23 01:53:54 -04:00
|
|
|
imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
|
2017-06-06 11:14:39 -04:00
|
|
|
)
|
2017-06-07 23:22:55 -04:00
|
|
|
if filename:
|
2017-06-25 14:27:56 -04:00
|
|
|
self.settings.setValue("componentDir", os.path.dirname(filename))
|
2017-06-03 22:58:40 -04:00
|
|
|
self.page.lineEdit_video.setText(filename)
|
|
|
|
self.update()
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-06-06 01:40:26 -04:00
|
|
|
def getPreviewFrame(self, width, height):
|
2017-06-06 08:55:22 -04:00
|
|
|
if not self.videoPath or not os.path.exists(self.videoPath):
|
|
|
|
return
|
2017-06-15 15:09:45 -04:00
|
|
|
|
2017-06-04 20:27:43 -04:00
|
|
|
command = [
|
2017-07-23 01:53:54 -04:00
|
|
|
self.core.FFMPEG_BIN,
|
2017-06-05 05:54:58 -04:00
|
|
|
'-thread_queue_size', '512',
|
2017-06-04 20:27:43 -04:00
|
|
|
'-i', self.videoPath,
|
|
|
|
'-f', 'image2pipe',
|
|
|
|
'-pix_fmt', 'rgba',
|
2017-07-29 13:08:28 -04:00
|
|
|
]
|
|
|
|
command.extend(self.makeFfmpegFilter())
|
|
|
|
command.extend([
|
2017-07-29 20:27:46 -04:00
|
|
|
'-codec:v', 'rawvideo', '-',
|
2017-06-06 01:40:26 -04:00
|
|
|
'-ss', '90',
|
2017-07-29 13:08:28 -04:00
|
|
|
'-frames:v', '1',
|
|
|
|
])
|
2017-07-04 19:52:52 -04:00
|
|
|
pipe = openPipe(
|
2017-07-27 17:49:08 -04:00
|
|
|
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
2017-06-06 11:14:39 -04:00
|
|
|
stderr=subprocess.DEVNULL, bufsize=10**8
|
|
|
|
)
|
2017-06-06 01:40:26 -04:00
|
|
|
byteFrame = pipe.stdout.read(self.chunkSize)
|
2017-07-29 13:08:28 -04:00
|
|
|
closePipe(pipe)
|
2017-06-15 15:09:45 -04:00
|
|
|
|
2017-07-29 13:08:28 -04:00
|
|
|
frame = self.finalizeFrame(byteFrame)
|
2017-06-15 15:09:45 -04:00
|
|
|
return frame
|
|
|
|
|
2017-07-29 13:08:28 -04:00
|
|
|
def makeFfmpegFilter(self):
|
|
|
|
return [
|
|
|
|
'-filter_complex',
|
|
|
|
'[0:v] scale=%s:%s' % scale(
|
|
|
|
self.scale, self.width, self.height, str),
|
|
|
|
]
|
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
def updateChunksize(self):
|
2017-06-15 15:09:45 -04:00
|
|
|
if self.scale != 100 and not self.distort:
|
2017-07-27 17:49:08 -04:00
|
|
|
width, height = scale(self.scale, self.width, self.height, int)
|
2017-07-27 22:15:41 -04:00
|
|
|
else:
|
|
|
|
width, height = self.width, self.height
|
2017-07-27 17:49:08 -04:00
|
|
|
self.chunkSize = 4 * width * height
|
2017-06-15 15:09:45 -04:00
|
|
|
|
2017-06-22 18:40:34 -04:00
|
|
|
def command(self, arg):
|
2017-07-23 01:53:54 -04:00
|
|
|
if '=' in arg:
|
2017-06-22 22:23:04 -04:00
|
|
|
key, arg = arg.split('=', 1)
|
|
|
|
if key == 'path' and os.path.exists(arg):
|
2017-07-23 01:53:54 -04:00
|
|
|
if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats:
|
2017-06-22 22:23:04 -04:00
|
|
|
self.page.lineEdit_video.setText(arg)
|
|
|
|
self.page.spinBox_scale.setValue(100)
|
|
|
|
self.page.checkBox_loop.setChecked(True)
|
|
|
|
return
|
2017-06-22 18:40:34 -04:00
|
|
|
else:
|
|
|
|
print("Not a supported video format")
|
|
|
|
quit(1)
|
2017-07-15 13:13:53 -04:00
|
|
|
elif arg == 'audio':
|
|
|
|
if not self.page.lineEdit_video.text():
|
|
|
|
print("'audio' option must follow a video selection")
|
|
|
|
quit(1)
|
|
|
|
self.page.checkBox_useAudio.setChecked(True)
|
|
|
|
return
|
2017-06-22 18:40:34 -04:00
|
|
|
super().command(arg)
|
|
|
|
|
|
|
|
def commandHelp(self):
|
2017-06-22 22:23:04 -04:00
|
|
|
print('Load a video:\n path=/filepath/to/video.mp4')
|
2017-07-15 13:13:53 -04:00
|
|
|
print('Using audio:\n path=/filepath/to/video.mp4 audio')
|
2017-06-22 18:40:34 -04:00
|
|
|
|
2017-07-29 13:08:28 -04:00
|
|
|
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)
|
2017-06-23 23:00:24 -04:00
|
|
|
|
2017-07-29 13:08:28 -04:00
|
|
|
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))
|
2017-06-25 10:36:32 -04:00
|
|
|
else:
|
2017-07-29 13:08:28 -04:00
|
|
|
frame = image
|
|
|
|
self.badVideo = False
|
|
|
|
return frame
|