diff options
| author | Aeliton G. Silva | 2026-01-12 22:39:55 -0300 |
|---|---|---|
| committer | Aeliton G. Silva | 2026-01-13 04:22:25 -0300 |
| commit | f975144f25d34f97329b2d4e52891061573cea08 (patch) | |
| tree | 226fe223b31af6f217b1dd413629ab2cf26964d4 /src/video_thread.py | |
| parent | b8703752ffc7768b0275897b3c2a869ff41504e5 (diff) | |
Use pyproject.toml + uv_build
This replaces setup.py by a modern pyproject.toml using uv_build
backend.
Dependencies are being also managed by uv, so to install dependencies
and run the project one can execute:
```
uv sync
uv run pytest # optional
python -m avp
```
To build the both source and binary (wheel) distribution package run:
```
uv build
```
Uv can be installed with `pip install uv`.
The directory structure has been changed to reflect best practices.
- src/* -> src/avp/
- src/tests -> ../tests
Diffstat (limited to 'src/video_thread.py')
| -rw-r--r-- | src/video_thread.py | 417 |
1 files changed, 0 insertions, 417 deletions
diff --git a/src/video_thread.py b/src/video_thread.py deleted file mode 100644 index 5d72409..0000000 --- a/src/video_thread.py +++ /dev/null @@ -1,417 +0,0 @@ -""" -Worker thread created to export a video. It has a slot to begin export using -an input file, output path, and component list. - -Signals are emitted to update MainWindow's progress bar, detail text, and preview. -A Command object takes the place of MainWindow while in commandline mode. - -Export can be cancelled with cancel() -""" - -from PyQt6 import QtCore, QtGui -from PyQt6.QtCore import pyqtSignal, pyqtSlot -from PIL import Image -from PIL.ImageQt import ImageQt -import numpy -import subprocess as sp -import sys -import os -import time -import signal -import logging - -from .component import ComponentError -from .toolkit.frame import Checkerboard -from .toolkit.ffmpeg import ( - openPipe, - readAudioFile, - getAudioDuration, - createFfmpegCommand, -) - - -log = logging.getLogger("AVP.VideoThread") - - -class Worker(QtCore.QObject): - - imageCreated = pyqtSignal("QImage") - videoCreated = pyqtSignal() - progressBarUpdate = pyqtSignal(int) - progressBarSetText = pyqtSignal(str) - encoding = pyqtSignal(bool) - - def __init__(self, parent, inputFile, outputFile, components): - super().__init__() - self.core = parent.core - self.settings = parent.settings - self.modules = parent.core.modules - parent.createVideo.connect(self.createVideo) - self.previewEnabled = type(parent.core).previewEnabled - - self.components = components - self.outputFile = outputFile - self.inputFile = inputFile - - self.hertz = 44100 - self.sampleSize = 1470 # 44100 / 30 = 1470 - self.canceled = False - self.error = False - - def createFfmpegCommand(self, duration): - try: - ffmpegCommand = createFfmpegCommand( - self.inputFile, self.outputFile, self.components, duration - ) - except sp.CalledProcessError as e: - # FIXME video_thread should own this error signal, not components - self.components[0]._error.emit( - "Ffmpeg could not be found. Is it installed?", str(e) - ) - self.error = True - return - - if not ffmpegCommand: - # FIXME video_thread should own this error signal, not components - self.components[0]._error.emit( - "The FFmpeg command could not be generated.", "" - ) - log.critical( - "Cancelling render process due to failure while generating the ffmpeg command." - ) - self.failExport() - return - return ffmpegCommand - - def determineAudioLength(self): - """ - Returns audio length which determines length of final video, or False if failure occurs - """ - if any( - [True if "pcm" in comp.properties() else False for comp in self.components] - ): - self.progressBarSetText.emit("Loading audio file...") - audioFileTraits = readAudioFile(self.inputFile, self) - if audioFileTraits is None: - self.cancelExport() - return False - self.completeAudioArray, duration = audioFileTraits - self.audioArrayLen = len(self.completeAudioArray) - else: - duration = getAudioDuration(self.inputFile) - self.completeAudioArray = [] - self.audioArrayLen = int( - ((duration * self.hertz) + self.hertz) - self.sampleSize - ) - return duration - - def preFrameRender(self): - """ - Initializes components that need to pre-compute stuff. - Also prerenders "static" components like text and merges them if possible - """ - self.staticComponents = {} - - # Call preFrameRender on each component - canceledByComponent = False - initText = ", ".join( - [ - "%s) %s" % (num, str(component)) - for num, component in enumerate(reversed(self.components)) - ] - ) - print("Loaded Components:", initText) - log.info("Calling preFrameRender for %s", initText) - for compNo, comp in enumerate(reversed(self.components)): - try: - comp.preFrameRender( - audioFile=self.inputFile, - completeAudioArray=self.completeAudioArray, - audioArrayLen=self.audioArrayLen, - sampleSize=self.sampleSize, - progressBarUpdate=self.progressBarUpdate, - progressBarSetText=self.progressBarSetText, - ) - except ComponentError: - log.warning( - "#%s %s encountered an error in its preFrameRender method", - compNo, - comp, - ) - - compProps = comp.properties() - if "error" in compProps or comp._lockedError is not None: - self.cancel() - self.canceled = True - canceledByComponent = True - compError = ( - comp.error() if type(comp.error()) is tuple else (comp.error(), "") - ) - errMsg = ( - "Component #%s (%s) encountered an error!" - % (str(compNo), comp.name) - if comp.error() is None - else "Export cancelled by component #%s (%s): %s" - % (str(compNo), comp.name, compError[0]) - ) - log.error(errMsg) - comp._error.emit(errMsg, compError[1]) - break - if "static" in compProps: - log.info("Saving static frame from #%s %s", compNo, comp) - self.staticComponents[compNo] = comp.frameRender(0).copy() - - # Check if any errors occured - log.debug("Checking if a component wishes to cancel the export...") - if self.canceled: - if canceledByComponent: - log.error( - "Export cancelled by component #%s (%s): %s", - compNo, - comp.name, - ( - "No message." - if comp.error() is None - else ( - comp.error() - if type(comp.error()) is str - else comp.error()[0] - ) - ), - ) - self.cancelExport() - - # Merge static frames that can be merged to reduce workload - def mergeConsecutiveStaticComponentFrames(self): - log.info("Merging consecutive static component frames") - for compNo in range(len(self.components)): - if ( - compNo not in self.staticComponents - or compNo + 1 not in self.staticComponents - ): - continue - self.staticComponents[compNo + 1] = Image.alpha_composite( - self.staticComponents.pop(compNo), - self.staticComponents[compNo + 1], - ) - self.staticComponents[compNo] = None - - mergeConsecutiveStaticComponentFrames(self) - - def frameRender(self, audioI): - """ - Renders a frame composited together from the frames returned by each component - audioI is a multiple of self.sampleSize, which can be divided to determine frameNo - """ - - def err(): - self.closePipe() - self.cancelExport() - self.error = True - msg = "A call to renderFrame in the video thread failed critically." - log.critical(msg) - comp._error.emit(msg, str(e)) - - bgI = int(audioI / self.sampleSize) - frame = None - for layerNo, comp in enumerate(reversed((self.components))): - if self.canceled: - break - try: - if layerNo in self.staticComponents: - if self.staticComponents[layerNo] is None: - # this layer was merged into a following layer - continue - # static component - if frame is None: # bottom-most layer - frame = self.staticComponents[layerNo] - else: - frame = Image.alpha_composite( - frame, self.staticComponents[layerNo] - ) - - else: - # animated component - if frame is None: # bottom-most layer - frame = comp.frameRender(bgI) - else: - frame = Image.alpha_composite(frame, comp.frameRender(bgI)) - except Exception as e: - err() - return frame - - def showPreview(self, frame): - """ - Receives a final frame that will be piped to FFmpeg, - adds it to the MainWindow for the live preview - """ - # We must store a reference to this QImage - # or else Qt will garbage-collect it on the C++ side - self.latestPreview = ImageQt(frame) - self.imageCreated.emit(QtGui.QImage(self.latestPreview)) - - @pyqtSlot() - def createVideo(self): - """ - 1. Numpy is set to ignore division errors during this method - 2. Determine length of final video - 3. Call preFrameRender on each component - 4. Create the main FFmpeg command - 5. Open the out_pipe to FFmpeg process - 6. Iterate over the audio data array and call frameRender on the components to get frames - 7. Close the out_pipe - 8. Call postFrameRender on each component - """ - log.debug("Video worker received signal to createVideo") - log.debug("Video thread id: {}".format(int(QtCore.QThread.currentThreadId()))) - numpy.seterr(divide="ignore") - self.encoding.emit(True) - self.extraAudio = [] - self.width = int(self.settings.value("outputWidth")) - self.height = int(self.settings.value("outputHeight")) - - # Set core.Core.canceled to False and call .reset() on each component - self.reset() - # Initialize progress bar to 0 - progressBarValue = 0 - self.progressBarUpdate.emit(progressBarValue) - - # Determine longest length of audio which will be the final video's duration - log.debug("Determining length of audio...") - duration = self.determineAudioLength() - if not duration: - return - - # Call preFrameRender on each component to perform initialization - self.progressBarUpdate.emit(0) - self.progressBarSetText.emit("Starting components...") - self.preFrameRender() - if self.canceled: - return - - # Create FFmpeg command - ffmpegCommand = self.createFfmpegCommand(duration) - if not ffmpegCommand: - return - cmd = " ".join(ffmpegCommand) - print("###### FFMPEG COMMAND ######\n%s" % cmd) - print("############################") - log.info(cmd) - - # Open pipe to FFmpeg - log.info("Opening pipe to FFmpeg") - try: - self.out_pipe = openPipe( - ffmpegCommand, - stdin=sp.PIPE, - stdout=sys.stdout, - stderr=sys.stdout, - ) - except sp.CalledProcessError: - log.critical("Out_Pipe to FFmpeg couldn't be created!", exc_info=True) - raise - - # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - # START CREATING THE VIDEO - # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - progressBarValue = 0 - self.progressBarUpdate.emit(progressBarValue) - # Begin piping into ffmpeg! - self.progressBarSetText.emit("Exporting video...") - for audioI in range(0, self.audioArrayLen, self.sampleSize): - if self.canceled: - break - # fetch the next frame & add to the FFmpeg pipe - frame = self.frameRender(audioI) - - # Update live preview - if self.previewEnabled: - self.showPreview(frame) - - try: - self.out_pipe.stdin.write(frame.tobytes()) - except Exception: - break - - # increase progress bar value - completion = (audioI / self.audioArrayLen) * 100 - if progressBarValue + 1 <= completion: - progressBarValue = numpy.floor(completion).astype(int) - self.progressBarUpdate.emit(progressBarValue) - self.progressBarSetText.emit( - "Exporting video: %s%%" % str(int(progressBarValue)) - ) - - # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - # Finished creating the video! - # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - - numpy.seterr(all="print") - - self.closePipe() - - for comp in reversed(self.components): - comp.postFrameRender() - - if self.canceled: - print("Export Canceled") - try: - os.remove(self.outputFile) - except Exception: - pass - self.progressBarUpdate.emit(0) - self.progressBarSetText.emit("Export Canceled") - else: - if self.error: - self.failExport() - else: - print("Export Complete") - self.progressBarUpdate.emit(100) - self.progressBarSetText.emit("Export Complete") - - self.error = False - self.canceled = False - self.encoding.emit(False) - self.videoCreated.emit() - - def closePipe(self): - try: - self.out_pipe.stdin.close() - except (BrokenPipeError, OSError): - log.debug("Broken pipe to FFmpeg!") - if self.out_pipe.stderr is not None: - log.error(self.out_pipe.stderr.read()) - self.out_pipe.stderr.close() - self.error = True - self.out_pipe.wait() - - def cancelExport(self, message="Export Canceled"): - self.progressBarUpdate.emit(0) - self.progressBarSetText.emit(message) - self.encoding.emit(False) - self.videoCreated.emit() - - def failExport(self): - self.cancelExport("Export Failed") - - def updateProgress(self, pStr, pVal): - self.progressBarValue.emit(pVal) - self.progressBarSetText.emit(pStr) - - def cancel(self): - self.canceled = True - self.core.cancel() - - for comp in self.components: - comp.cancel() - - try: - self.out_pipe.send_signal(signal.SIGTERM) - except Exception: - pass - - def reset(self): - self.core.reset() - self.canceled = False - for comp in self.components: - comp.reset() |
