FFmpeg: nicer process report #99912

Open
opened 2022-07-22 14:31:11 +02:00 by Sybren A. Stüvel · 6 comments

FFmpeg can report its progress in a machine-readable way (see Stack Overflow). It would be nice to include this in Flamenco Worker, so that it can send progress info to the Manager.

FFmpeg can report its progress in a machine-readable way (see [Stack Overflow](https://stackoverflow.com/a/43980180/875379)). It would be nice to include this in Flamenco Worker, so that it can send progress info to the Manager.
Author
Owner

Changed status from 'Needs Triage' to: 'Confirmed'

Changed status from 'Needs Triage' to: 'Confirmed'
Author
Owner

Added subscriber: @dr.sybren

Added subscriber: @dr.sybren
Sybren A. Stüvel added
Type
To Do
and removed
Type
Report
labels 2023-02-17 11:10:58 +01:00

Hey all. I stumbled on this looking for something else and just wanted to give a little back.

This is not specific to Flamenco - I use a highly modified version of the Pandora Render Farm Manager. It is open source and I have added/modified it to suite our needs. One of the things I did was write a "Preview Maker" that uses FFMPEG to create a vid clip from the rendered image sequences from the UI. I wanted it to be fast and simple with a nice GUI. One of the things is to have FFMPEG progress in the GUI.

So here is the code, maybe it will help. Obviously it will not work on it's own, but maybe you can use some of the code for Flamenco. I am sure this could be written cleaner and/or better, but it works for me.

J.


# -*- coding: utf-8 -*-
#
####################################################
#
# Pandora - Renderfarm Manager
#
# https://prism-pipeline.com/pandora/
#
# contact: contact@prism-pipeline.com
#
####################################################
#
#
# Copyright (C) 2016-2020 Richard Frangenberg
#
# Licensed under GNU GPL-3.0-or-later
#
# This file is part of Pandora.
#
# Pandora is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pandora is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Pandora.  If not, see <https://www.gnu.org/licenses/>.
#
#
#
#   MODIFIED BY JOSHUA BRECKEEN
#       ALTA ARTS
#       ver. 1.3
#       2/07/23


import sys, os, platform, subprocess, traceback, time, threading, re
from functools import wraps

if sys.version[0] == "3":
    pyLibs = "Python37"
    pVersion = 3
else:
    pyLibs = "Python27"
    pVersion = 2

pandoraRoot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))

scriptPath = os.path.abspath(os.path.dirname(__file__))
if scriptPath not in sys.path:
    sys.path.append(scriptPath)

sys.path.append(os.path.join(pandoraRoot, "PythonLibs"))

pyLibPath = os.path.join(pandoraRoot, "PythonLibs", pyLibs)
cpLibs = os.path.join(pandoraRoot, "PythonLibs", "CrossPlatform")

if cpLibs not in sys.path:
    sys.path.append(cpLibs)

if pyLibPath not in sys.path:
    sys.path.append(pyLibPath)

if platform.system() == "Windows":
    sys.path.insert(0, os.path.join(pyLibPath, "win32"))
    sys.path.insert(0, os.path.join(pyLibPath, "win32", "lib"))
    os.environ['PATH'] = os.path.join(pyLibPath, "pywin32_system32") + os.pathsep + os.environ['PATH']


try:
    from PySide2 import QtWidgets
    from PySide2.QtUiTools import QUiLoader
    from PySide2 import QtCore

    from PySide2.QtCore import *
    from PySide2.QtGui import *
    from PySide2.QtWidgets import *

    psVersion = 2

except:
    from PySide.QtCore import *
    from PySide.QtGui import *

    psVersion = 1

sys.path.append('./UserInterfacesPandora')

from UserInterfacesPandora import qdarkstyle


class RenderThread(threading.Thread):
    def __init__(self, command, totalFrames, updateCallback):
        threading.Thread.__init__(self)

        self.command = command
        self.totalFrames = totalFrames
        self.updateCallback = updateCallback
        self.proc = None

    def run(self):

        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE

        self.proc = subprocess.Popen(
            self.command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            bufsize=1,
            universal_newlines=True,
            startupinfo=startupinfo
        )

        self.updateCallback(5)

                                                                #   Reading the output of FFPEG
        while True:
            output = self.proc.stderr.readline()

            if self.proc.poll() is not None:
                break

            if output:
                if isinstance(output, bytes):
                    output = output.decode()
                match = re.search(r"frame=\s*(\d+)", output)    #   Looking for frame num's
                if match:
                    frame_num = int(match.group(1))
                    progress = round((frame_num / self.totalFrames) * 100, 2)   #   Getting a percentage
                    self.updateCallback(progress)              #    Updated the Prog Bar         

        stdout, stderr = self.proc.communicate()

        if stdout:
            ffmpgOutput = stdout.decode('utf-8') if isinstance(stdout, bytes) else stdout
        else:
            ffmpgOutput = stderr.decode('utf-8') if isinstance(stderr, bytes) else stderr

        message = ffmpgOutput

        self.updateCallback(None, message)
#        self.updateCallback(100)           #   Disabled becasue sometimes it would read 100% before finished
#        self.updateCallback(None, ffmpgOutput)


    def terminate(self):

        if self.proc is not None:
            self.proc.terminate()



class PreviewMaker(QtWidgets.QMainWindow):
    def __init__(self, path, jData, FFMPG, core):
        super().__init__()

        self.core = core
        self.core.parentWindow(self)

        self.timer = QTimer()
        self.renderThread = None
        self.jData = jData
        self.inputPath = path
        self.FFMPG = FFMPG

        self.errorCode = 0

        self.padding = self.jData["information"]["padding"]
        self.paddingNum = len(self.padding)

        self.inputColList = {}
        self.qualList = {}
        self.scaleList = {}
        self.FPSlist = ()
        self.containerList = ()
        self.codecList = ()
        self.outputColList = {}
        
        self.loadUi()
        self.connectEvents()
        self.setCentralWidget(self.ui)
        self.populateCombos()
        self.loadInfo()


    def err_decorator(func):
        @wraps(func)
        def func_wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                exc_type, exc_obj, exc_tb = sys.exc_info()
                erStr = "%s ERROR - Renderhandler %s:\n%s\n\n%s" % (
                    time.strftime("%d/%m/%y %X"),
                    args[0].core.version,
                    "".join(traceback.format_stack()),
                    traceback.format_exc(),
                )
                args[0].core.writeErrorLog(erStr)

        return func_wrapper


    @err_decorator
    def loadUi(self):

        self.setWindowTitle("Preview Maker")
#        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)                   #   DISABLED SINCE IT COVERS ERRORS

        current_dir = os.path.dirname(os.path.abspath(__file__))
        ui_path = os.path.join(current_dir, "UserInterfacesPandora/PreviewMaker.ui")

        loader = QUiLoader()
        ui_file = QFile(ui_path)
        ui_file.open(QFile.ReadOnly)
        self.ui = loader.load(ui_file)
        ui_file.close()

        #   OVERRIDE STYLESHEET
            #   READ-ONLY:  TRANSPARENT
        self.ui.e_seqName.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)")
        self.ui.e_jobFrames.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)")
        self.ui.e_renderFrameRate.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)")
        self.ui.e_renderedFrames.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)")
        self.ui.e_renderRez.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)")
        self.ui.e_renderColorMgt.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)")


            #   SELECTABLE COMBO's: DARK GREY
        self.ui.cb_outputQual.setStyleSheet("background-color: rgb(40, 40, 40)")
        self.ui.cb_inputColSpace.setStyleSheet("background-color: rgb(40, 40, 40)")
        self.ui.cb_outputScale.setStyleSheet("background-color: rgb(40, 40, 40)")

        self.ui.cb_outputContain.setStyleSheet("background-color: rgb(40, 40, 40)")
        self.ui.cb_outputFPS.setStyleSheet("background-color: rgb(40, 40, 40)")
        self.ui.cb_outputCodec.setStyleSheet("background-color: rgb(40, 40, 40)")
        self.ui.cb_outputColSpace.setStyleSheet("background-color: rgb(40, 40, 40)")

        self.ui.e_outputFilePath.setStyleSheet("background-color: rgb(50, 50, 50)")

    @err_decorator
    def connectEvents(self):
        self.ui.but_execute.clicked.connect(self.execute)
        self.ui.but_cancel.clicked.connect(self.cancel)
        self.ui.but_exit.clicked.connect(self.exit)
        self.ui.cb_outputScale.highlighted.connect(self.resetProgBar)
        self.ui.but_outputFilePath.clicked.connect(self.selOutputPath)
        self.ui.e_outputFileName.editingFinished.connect(self.makeSaveFileName)
        self.ui.cb_outputContain.currentIndexChanged.connect(self.makeSaveFileName)


    @err_decorator
    def writeStatus(self, statusText):

        self.ui.e_console.append(statusText)


    @err_decorator
    def populateCombos(self):

        folder, fileName = os.path.split(self.inputPath)
        fileNameNoExt, ext = os.path.splitext(fileName)
        fileNameNoExt = fileNameNoExt[:-self.paddingNum]

        fileExt = ext
                                                        #   FFMPEG cannot handle ACES AFAIK
        self.ui.cb_inputColSpace.clear()
        self.inputColList = {"rec709" : "-gamma 2.0",           #   Through testing, these gave me the closest results,
                            "sRGB" : "",                        #   atleast good enough for a rough preview.
                            "Linear" : "-gamma 2.2",                    #   CORRECT
                            "Raw" : "-gamma 2.2",                       #   CORRECT
                            "Linear ACES" : "",
                            "ACEScg" : "-gamma 2.2",                    #   CORRECT GAMMA, COLSPACE TODO
                            "None" : ""
                            }
        
        self.ui.cb_inputColSpace.addItems(self.inputColList.keys())

        index = 0
                                #   These are the only types we use
        if fileExt == ".exr":           #   Preselects combo based on file ext - assumes linear             
            index = self.ui.cb_inputColSpace.findText("Linear", QtCore.Qt.MatchFixedString)            
        
        if fileExt == ".png":           #   same
            index = self.ui.cb_inputColSpace.findText("sRGB", QtCore.Qt.MatchFixedString)

        self.ui.cb_inputColSpace.setCurrentIndex(index)

        self.ui.cb_outputQual.clear()
        self.qualList = {"High" : "-crf 18",        #   To Keep simple, chose only three levels of Qual
                         "Medium" : "-crf 23",
                         "Low" : "-crf 28"
                        }
        self.ui.cb_outputQual.addItems(self.qualList.keys())
        self.ui.cb_outputQual.setCurrentIndex(1)

        self.ui.cb_outputScale.clear()
        self.scaleList = {r"25%" : '-vf "scale=iw*.25:ih*.25"',     #   Again, to keep it simple.
                          r"50%" : '-vf "scale=iw*.5:ih*.5"',
                          r"75%" : '-vf "scale=iw*.75:ih*.75"',
                          r"100%" : '-vf "scale=iw*1:ih*1"',
                          r"150%" : '-vf "scale=iw*1.5:ih*1.5"',
                          r"200%" : '-vf "scale=iw*2:ih*2"'
                          }

        self.ui.cb_outputScale.addItems(self.scaleList.keys())
        self.ui.cb_outputScale.setCurrentIndex(3)

        self.ui.cb_outputFPS.clear()
        self.FPSlist = ("10", 
                   "12",
                   "15",
                   "23.97",
                   "24",
                   "25",
                   "29.97",
                   "30",
                   "48",
                   "50",
                   "59.94",
                   "60",
                   "90",
                   "120",
                   "180")
        self.ui.cb_outputFPS.addItems(self.FPSlist)

        try:
            inputFPS = self.jData["information"]["frameRate"]
            index = self.ui.cb_outputFPS.findText(inputFPS)
        except:
            index = 7

        self.ui.cb_outputFPS.setCurrentIndex(index)

        self.ui.cb_outputContain.clear()
        self.containerList = (".mp4",           #   TODO - Hardcoded to .mp4
                            ".mov",             #   thats all we use for previews
                            ".mkv",
                            ".avi")
        self.ui.cb_outputContain.addItems(self.containerList)
        self.ui.cb_outputContain.setCurrentIndex(0)

        self.ui.cb_outputCodec.clear()
        self.codecList = ("h264",               #   TODO - Hardcoded to h264
                        "h265")
        self.ui.cb_outputCodec.addItems(self.codecList)
        self.ui.cb_outputCodec.setCurrentIndex(0)

        self.ui.cb_outputColSpace.clear()       #   Chose average formats
        self.outputColList = {"rec709" : "-pix_fmt yuv420p -color_primaries bt709 -color_trc bt709 -colorspace bt709",
                            "rec2020" : "-c:v libx265 -crf 20 -vf eq=gamma=1.15:saturation=0.925:brightness=0.05:contrast=1.15 -color_primaries bt2020",
                            "sRGB" : "-c:v libx264rgb",
                            "None" : ""
                            }
        self.ui.cb_outputColSpace.addItems(self.outputColList.keys())
        self.ui.cb_outputColSpace.setCurrentIndex(0)


    @err_decorator
    def loadInfo(self):

        folder, fileName = os.path.split(self.inputPath)
        fileNameNoExt, ext = os.path.splitext(fileName)
        fileNameNoExt = fileNameNoExt[:-self.paddingNum]
        saveFolder = os.path.join(folder, "")

        renderedFrames = len([f for f in os.listdir(folder)
                            if os.path.isfile(os.path.join(folder, f))]) 

        self.ui.e_seqName.setText(fileNameNoExt + self.padding + ext)

        pattern = r'^.*?(\d{1,4})(?=\.\w+$)'        #   Looks for sequence with padding at the end
        files = [f for f in os.listdir(folder)
                if os.path.isfile(os.path.join(folder, f)) and re.search(pattern, f)]

        numbers = [int(re.search(pattern, f).group(1)) for f in files]
        self.firstFile = min(numbers) if numbers else None              #   Finds the lowest sequence num
                                                                        #   for FFMPEG start-frame
        if "frameRange" in self.jData["information"]:
            jobFrames = self.jData["information"]["frameRange"]     #   Populates the GUI from metadata
            self.ui.e_jobFrames.setText(jobFrames)
        else:
            self.ui.e_jobFrames.setText("Unknown")

        self.ui.e_renderedFrames.setText(str(renderedFrames))

        if "frameRate" in self.jData["information"]:
            jobFrameRate = self.jData["information"]["frameRate"]
            self.ui.e_renderFrameRate.setText(jobFrameRate)
        else:
            self.ui.e_renderFrameRate.setText("Unknown")
        
        if "render resolution" in self.jData["information"]:
            renderRez = self.jData["information"]["render resolution"]
            self.ui.e_renderRez.setText(renderRez)
        else:
            self.ui.e_renderRez.setText("Unknown")

        if "Render Color Management" in self.jData["information"]:
            renderColorMgt = self.jData["information"]["Render Color Management"]
            self.ui.e_renderColorMgt.setText(renderColorMgt)
        else:
            self.ui.e_renderColorMgt.setText("Unknown")

        saveFileName = f"{fileNameNoExt}Preview"

        self.ui.e_outputFileName.setText(saveFileName)
        self.makeSaveFileName()

        self.ui.e_outputFilePath.setText(saveFolder)

        self.resetProgBar()
        self.writeStatus("Loaded Info")


    @err_decorator
    def selOutputPath(self):

        initialDir = self.ui.e_outputFilePath.text()
 
        self.outputPath = QFileDialog(self, "Open file", initialDir)
        self.outputPath.setFileMode(QFileDialog.Directory)

        if self.outputPath.exec_() == QFileDialog.Accepted:
            selDir = self.outputPath.selectedFiles()[0]
            selDir = os.path.abspath(selDir)
            self.ui.e_outputFilePath.setText(selDir)

        self.resetProgBar()
        self.writeStatus("Changed Output Folder")


    @err_decorator
    def makeSaveFileName(self, *ags):

        fileExt = self.ui.cb_outputContain.currentText()
        fileNameNoExt, ext = os.path.splitext(self.ui.e_outputFileName.text())

        self.outputPath = fileNameNoExt + fileExt
        self.ui.e_outputFileName.setText(self.outputPath)

        self.resetProgBar()


    @err_decorator
    def getArgs(self):
                                                                #   Builds the FFMPEG command
        selOverwrite = self.ui.chb_overwrite.isChecked()
        if selOverwrite:
            overWrite = "-y"
        else:
            overWrite = ""

        inColor = self.inputColList[self.ui.cb_inputColSpace.currentText()]

        selFrameRate = self.ui.cb_outputFPS.currentText()
        frameRate = "-r " + selFrameRate

        firstFile = self.firstFile                                # Needed if start frame is not 1
        startNumber = "-start_number " + str(firstFile)

        scale = self.scaleList[self.ui.cb_outputScale.currentText()]    
        outQual = self.qualList[self.ui.cb_outputQual.currentText()]
        outColor = self.outputColList[self.ui.cb_outputColSpace.currentText()]
        
        inputArgs = f'{overWrite} {inColor} {frameRate} {startNumber}'
        outputArgs = f'{scale} {outQual} {outColor}'

        return inputArgs,outputArgs
    

    @err_decorator
    def getMetaData(self):        

        metaDataList = []                                   #   Builds Metadata for info

        if "program" in self.jData["information"]:
            dcc = self.jData["information"]["program"]
            metaDataList.append(rf' -metadata "DCC"="{dcc}"')

        if "projectName" in self.jData["information"]:        
            project = self.jData["information"]["projectName"]
            metaDataList.append(rf' -metadata "Project"="{project}"')

        if "frameRate" in self.jData["information"]:        
            projFrameRate = self.jData["information"]["frameRate"]
            metaDataList.append(rf' -metadata "Project Frame Rate"="{projFrameRate}"')

        if "jobName" in self.jData["information"]:        
            shot = self.jData["information"]["jobName"]
            metaDataList.append(rf' -metadata "Shot"={shot}')

        if "Render Layer" in self.jData["information"]:        
            renderLayer = self.jData["information"]["Render Layer"]
            metaDataList.append(rf' -metadata "Render Layer"="{renderLayer}"')

        if "camera" in self.jData["information"]:        
            renderCam = self.jData["information"]["camera"]
            metaDataList.append(rf' -metadata "Render Camera"="{renderCam}"')  

        if "render resolution" in self.jData["information"]:        
            renderRez = self.jData["information"]["render resolution"]
            metaDataList.append(rf' -metadata "Render Resolution"="{renderRez}"')         

        if "frameRange" in self.jData["information"]:        
            renderRange = self.jData["information"]["frameRange"]
            metaDataList.append(rf' -metadata "Render Frame Range"="{renderRange}"')         

        if "renderSamples" in self.jData["information"]:        
            renderSamples = self.jData["information"]["renderSamples"]
            metaDataList.append(rf' -metadata "Render Samples"="{renderSamples}"')  

        if "Render Color Management" in self.jData["information"]:        
            renderColMgt = self.jData["information"]["Render Color Management"]
            metaDataList.append(rf' -metadata "Render Color Management"="{renderColMgt}"')

        if "File Format" in self.jData["information"]:        
            fileFormat = self.jData["information"]["File Format"]
            metaDataList.append(rf' -metadata "Render Image Format"="{fileFormat}"')

        if "alpha" in self.jData["information"]:        
            alpha = self.jData["information"]["alpha"]
            metaDataList.append(rf' -metadata "Render Image - Alpha"="{alpha}"')               

        if self.ui.e_metaNote.text() != "":
            metaDataList.append(rf' -metadata "Note"="{self.ui.e_metaNote.text()}"')

        metaDataList =  " ".join(metaDataList)
        metaDataSuffix = " -movflags +use_metadata_tags"
        metaData = metaDataList + metaDataSuffix

        return metaData


    @err_decorator
    def execute(self):

        self.resetProgBar()

        ffmpg = f'"{self.FFMPG}"'           #   Gets FFMPEG path from Pandora settings file

        inputArgs, outputArgs = self.getArgs()
        metaData = self.getMetaData()
        totalFrames = int(self.ui.e_renderedFrames.text())

        folder, fileName = os.path.split(self.inputPath)
        fileNameNoExt, ext = os.path.splitext(fileName)
        fileNameNoExt = fileNameNoExt[:-self.paddingNum]

        sequenceExt = rf"%0{self.paddingNum}d"          #   Makes the output filename with the sequence padding num
        sourceFile = (os.path.join(folder, fileNameNoExt) + sequenceExt + ext)
        sourceFile = f'"{sourceFile}"'

        outputPath = self.ui.e_outputFilePath.text()
        outputFileName = self.ui.e_outputFileName.text()

        outputFile = os.path.join(outputPath, outputFileName)
        checkFile = outputFile
        outputFile = f'"{outputFile}"'

        command = f'{ffmpg} {inputArgs} -i {sourceFile} {metaData} {outputArgs} {outputFile}'

        if os.path.isfile(checkFile) and not self.ui.chb_overwrite.isChecked():

            self.writeStatus("")
            self.writeStatus("File Exists and Overwrite NOT Enabled")
            self.ui.progressBar.setStyleSheet("background-color: red;")
        else:
            self.writeStatus("")                    #   TESTING
            self.writeStatus(command)               #   TESTING
            self.writeStatus("")                    #   TESTING

            self.renderThread = RenderThread(command, totalFrames, self.updateProgressBar)  #   Calls FFMPEG thread
            self.renderThread.start()


    @err_decorator
    def resetProgBar(self, *ags):                   #   Reset Progbar to Zero
        self.ui.progressBar.setStyleSheet("background-color: black;")
        self.ui.progressBar.setValue(0)


    @err_decorator                                  #   Update Progbar with FFMPEG output
    def updateProgressBar(self, progress, message=None):
        self.errorCode = 0

        if progress is not None:
            if progress < 5:
                progress = 5
            self.ui.progressBar.setValue(progress)

        if message:
            self.writeStatus(message)

        if progress == 100:
            self.errorCode = 1

        if self.errorCode == 1:

            self.writeStatus("Encode Complete")
            self.ui.progressBar.setStyleSheet("""
                                                QProgressBar::chunk {
                                                background-color: #0BDA51;
                                                }
                                                """)
            

    @err_decorator
    def cancel(self):
        if self.renderThread is None:
            pass

        else:
            reply = QMessageBox.question(
                                        self,
                                        "Cancel Encode",
                                        "Are you sure?",
                                        QMessageBox.Yes | QMessageBox.No, QMessageBox.No
                                        )
            
            if reply == QMessageBox.Yes:

                if self.renderThread is not None:

                    self.renderThread.terminate()
                    self.renderThread = None

                    self.ui.progressBar.setStyleSheet("background-color: red;")

                    self.writeStatus("")
                    self.writeStatus("Encode Canceled by User")

            else:
                pass

    @err_decorator
    def exit(self):

        self.close()
        

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    try:
        app.setStyleSheet(qdarkstyle.load_stylesheet(pyside=True))

    except NameError:
        pass
    
    previewMaker = PreviewMaker()
    previewMaker.show()
    sys.exit(app.exec_())
Hey all. I stumbled on this looking for something else and just wanted to give a little back. This is not specific to Flamenco - I use a highly modified version of the Pandora Render Farm Manager. It is open source and I have added/modified it to suite our needs. One of the things I did was write a "Preview Maker" that uses FFMPEG to create a vid clip from the rendered image sequences from the UI. I wanted it to be fast and simple with a nice GUI. One of the things is to have FFMPEG progress in the GUI. So here is the code, maybe it will help. Obviously it will not work on it's own, but maybe you can use some of the code for Flamenco. I am sure this could be written cleaner and/or better, but it works for me. J. ----- ``` # -*- coding: utf-8 -*- # #################################################### # # Pandora - Renderfarm Manager # # https://prism-pipeline.com/pandora/ # # contact: contact@prism-pipeline.com # #################################################### # # # Copyright (C) 2016-2020 Richard Frangenberg # # Licensed under GNU GPL-3.0-or-later # # This file is part of Pandora. # # Pandora is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Pandora is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Pandora. If not, see <https://www.gnu.org/licenses/>. # # # # MODIFIED BY JOSHUA BRECKEEN # ALTA ARTS # ver. 1.3 # 2/07/23 import sys, os, platform, subprocess, traceback, time, threading, re from functools import wraps if sys.version[0] == "3": pyLibs = "Python37" pVersion = 3 else: pyLibs = "Python27" pVersion = 2 pandoraRoot = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) scriptPath = os.path.abspath(os.path.dirname(__file__)) if scriptPath not in sys.path: sys.path.append(scriptPath) sys.path.append(os.path.join(pandoraRoot, "PythonLibs")) pyLibPath = os.path.join(pandoraRoot, "PythonLibs", pyLibs) cpLibs = os.path.join(pandoraRoot, "PythonLibs", "CrossPlatform") if cpLibs not in sys.path: sys.path.append(cpLibs) if pyLibPath not in sys.path: sys.path.append(pyLibPath) if platform.system() == "Windows": sys.path.insert(0, os.path.join(pyLibPath, "win32")) sys.path.insert(0, os.path.join(pyLibPath, "win32", "lib")) os.environ['PATH'] = os.path.join(pyLibPath, "pywin32_system32") + os.pathsep + os.environ['PATH'] try: from PySide2 import QtWidgets from PySide2.QtUiTools import QUiLoader from PySide2 import QtCore from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import * psVersion = 2 except: from PySide.QtCore import * from PySide.QtGui import * psVersion = 1 sys.path.append('./UserInterfacesPandora') from UserInterfacesPandora import qdarkstyle class RenderThread(threading.Thread): def __init__(self, command, totalFrames, updateCallback): threading.Thread.__init__(self) self.command = command self.totalFrames = totalFrames self.updateCallback = updateCallback self.proc = None def run(self): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE self.proc = subprocess.Popen( self.command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True, startupinfo=startupinfo ) self.updateCallback(5) # Reading the output of FFPEG while True: output = self.proc.stderr.readline() if self.proc.poll() is not None: break if output: if isinstance(output, bytes): output = output.decode() match = re.search(r"frame=\s*(\d+)", output) # Looking for frame num's if match: frame_num = int(match.group(1)) progress = round((frame_num / self.totalFrames) * 100, 2) # Getting a percentage self.updateCallback(progress) # Updated the Prog Bar stdout, stderr = self.proc.communicate() if stdout: ffmpgOutput = stdout.decode('utf-8') if isinstance(stdout, bytes) else stdout else: ffmpgOutput = stderr.decode('utf-8') if isinstance(stderr, bytes) else stderr message = ffmpgOutput self.updateCallback(None, message) # self.updateCallback(100) # Disabled becasue sometimes it would read 100% before finished # self.updateCallback(None, ffmpgOutput) def terminate(self): if self.proc is not None: self.proc.terminate() class PreviewMaker(QtWidgets.QMainWindow): def __init__(self, path, jData, FFMPG, core): super().__init__() self.core = core self.core.parentWindow(self) self.timer = QTimer() self.renderThread = None self.jData = jData self.inputPath = path self.FFMPG = FFMPG self.errorCode = 0 self.padding = self.jData["information"]["padding"] self.paddingNum = len(self.padding) self.inputColList = {} self.qualList = {} self.scaleList = {} self.FPSlist = () self.containerList = () self.codecList = () self.outputColList = {} self.loadUi() self.connectEvents() self.setCentralWidget(self.ui) self.populateCombos() self.loadInfo() def err_decorator(func): @wraps(func) def func_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: exc_type, exc_obj, exc_tb = sys.exc_info() erStr = "%s ERROR - Renderhandler %s:\n%s\n\n%s" % ( time.strftime("%d/%m/%y %X"), args[0].core.version, "".join(traceback.format_stack()), traceback.format_exc(), ) args[0].core.writeErrorLog(erStr) return func_wrapper @err_decorator def loadUi(self): self.setWindowTitle("Preview Maker") # self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) # DISABLED SINCE IT COVERS ERRORS current_dir = os.path.dirname(os.path.abspath(__file__)) ui_path = os.path.join(current_dir, "UserInterfacesPandora/PreviewMaker.ui") loader = QUiLoader() ui_file = QFile(ui_path) ui_file.open(QFile.ReadOnly) self.ui = loader.load(ui_file) ui_file.close() # OVERRIDE STYLESHEET # READ-ONLY: TRANSPARENT self.ui.e_seqName.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)") self.ui.e_jobFrames.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)") self.ui.e_renderFrameRate.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)") self.ui.e_renderedFrames.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)") self.ui.e_renderRez.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)") self.ui.e_renderColorMgt.setStyleSheet("background-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0)") # SELECTABLE COMBO's: DARK GREY self.ui.cb_outputQual.setStyleSheet("background-color: rgb(40, 40, 40)") self.ui.cb_inputColSpace.setStyleSheet("background-color: rgb(40, 40, 40)") self.ui.cb_outputScale.setStyleSheet("background-color: rgb(40, 40, 40)") self.ui.cb_outputContain.setStyleSheet("background-color: rgb(40, 40, 40)") self.ui.cb_outputFPS.setStyleSheet("background-color: rgb(40, 40, 40)") self.ui.cb_outputCodec.setStyleSheet("background-color: rgb(40, 40, 40)") self.ui.cb_outputColSpace.setStyleSheet("background-color: rgb(40, 40, 40)") self.ui.e_outputFilePath.setStyleSheet("background-color: rgb(50, 50, 50)") @err_decorator def connectEvents(self): self.ui.but_execute.clicked.connect(self.execute) self.ui.but_cancel.clicked.connect(self.cancel) self.ui.but_exit.clicked.connect(self.exit) self.ui.cb_outputScale.highlighted.connect(self.resetProgBar) self.ui.but_outputFilePath.clicked.connect(self.selOutputPath) self.ui.e_outputFileName.editingFinished.connect(self.makeSaveFileName) self.ui.cb_outputContain.currentIndexChanged.connect(self.makeSaveFileName) @err_decorator def writeStatus(self, statusText): self.ui.e_console.append(statusText) @err_decorator def populateCombos(self): folder, fileName = os.path.split(self.inputPath) fileNameNoExt, ext = os.path.splitext(fileName) fileNameNoExt = fileNameNoExt[:-self.paddingNum] fileExt = ext # FFMPEG cannot handle ACES AFAIK self.ui.cb_inputColSpace.clear() self.inputColList = {"rec709" : "-gamma 2.0", # Through testing, these gave me the closest results, "sRGB" : "", # atleast good enough for a rough preview. "Linear" : "-gamma 2.2", # CORRECT "Raw" : "-gamma 2.2", # CORRECT "Linear ACES" : "", "ACEScg" : "-gamma 2.2", # CORRECT GAMMA, COLSPACE TODO "None" : "" } self.ui.cb_inputColSpace.addItems(self.inputColList.keys()) index = 0 # These are the only types we use if fileExt == ".exr": # Preselects combo based on file ext - assumes linear index = self.ui.cb_inputColSpace.findText("Linear", QtCore.Qt.MatchFixedString) if fileExt == ".png": # same index = self.ui.cb_inputColSpace.findText("sRGB", QtCore.Qt.MatchFixedString) self.ui.cb_inputColSpace.setCurrentIndex(index) self.ui.cb_outputQual.clear() self.qualList = {"High" : "-crf 18", # To Keep simple, chose only three levels of Qual "Medium" : "-crf 23", "Low" : "-crf 28" } self.ui.cb_outputQual.addItems(self.qualList.keys()) self.ui.cb_outputQual.setCurrentIndex(1) self.ui.cb_outputScale.clear() self.scaleList = {r"25%" : '-vf "scale=iw*.25:ih*.25"', # Again, to keep it simple. r"50%" : '-vf "scale=iw*.5:ih*.5"', r"75%" : '-vf "scale=iw*.75:ih*.75"', r"100%" : '-vf "scale=iw*1:ih*1"', r"150%" : '-vf "scale=iw*1.5:ih*1.5"', r"200%" : '-vf "scale=iw*2:ih*2"' } self.ui.cb_outputScale.addItems(self.scaleList.keys()) self.ui.cb_outputScale.setCurrentIndex(3) self.ui.cb_outputFPS.clear() self.FPSlist = ("10", "12", "15", "23.97", "24", "25", "29.97", "30", "48", "50", "59.94", "60", "90", "120", "180") self.ui.cb_outputFPS.addItems(self.FPSlist) try: inputFPS = self.jData["information"]["frameRate"] index = self.ui.cb_outputFPS.findText(inputFPS) except: index = 7 self.ui.cb_outputFPS.setCurrentIndex(index) self.ui.cb_outputContain.clear() self.containerList = (".mp4", # TODO - Hardcoded to .mp4 ".mov", # thats all we use for previews ".mkv", ".avi") self.ui.cb_outputContain.addItems(self.containerList) self.ui.cb_outputContain.setCurrentIndex(0) self.ui.cb_outputCodec.clear() self.codecList = ("h264", # TODO - Hardcoded to h264 "h265") self.ui.cb_outputCodec.addItems(self.codecList) self.ui.cb_outputCodec.setCurrentIndex(0) self.ui.cb_outputColSpace.clear() # Chose average formats self.outputColList = {"rec709" : "-pix_fmt yuv420p -color_primaries bt709 -color_trc bt709 -colorspace bt709", "rec2020" : "-c:v libx265 -crf 20 -vf eq=gamma=1.15:saturation=0.925:brightness=0.05:contrast=1.15 -color_primaries bt2020", "sRGB" : "-c:v libx264rgb", "None" : "" } self.ui.cb_outputColSpace.addItems(self.outputColList.keys()) self.ui.cb_outputColSpace.setCurrentIndex(0) @err_decorator def loadInfo(self): folder, fileName = os.path.split(self.inputPath) fileNameNoExt, ext = os.path.splitext(fileName) fileNameNoExt = fileNameNoExt[:-self.paddingNum] saveFolder = os.path.join(folder, "") renderedFrames = len([f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))]) self.ui.e_seqName.setText(fileNameNoExt + self.padding + ext) pattern = r'^.*?(\d{1,4})(?=\.\w+$)' # Looks for sequence with padding at the end files = [f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)) and re.search(pattern, f)] numbers = [int(re.search(pattern, f).group(1)) for f in files] self.firstFile = min(numbers) if numbers else None # Finds the lowest sequence num # for FFMPEG start-frame if "frameRange" in self.jData["information"]: jobFrames = self.jData["information"]["frameRange"] # Populates the GUI from metadata self.ui.e_jobFrames.setText(jobFrames) else: self.ui.e_jobFrames.setText("Unknown") self.ui.e_renderedFrames.setText(str(renderedFrames)) if "frameRate" in self.jData["information"]: jobFrameRate = self.jData["information"]["frameRate"] self.ui.e_renderFrameRate.setText(jobFrameRate) else: self.ui.e_renderFrameRate.setText("Unknown") if "render resolution" in self.jData["information"]: renderRez = self.jData["information"]["render resolution"] self.ui.e_renderRez.setText(renderRez) else: self.ui.e_renderRez.setText("Unknown") if "Render Color Management" in self.jData["information"]: renderColorMgt = self.jData["information"]["Render Color Management"] self.ui.e_renderColorMgt.setText(renderColorMgt) else: self.ui.e_renderColorMgt.setText("Unknown") saveFileName = f"{fileNameNoExt}Preview" self.ui.e_outputFileName.setText(saveFileName) self.makeSaveFileName() self.ui.e_outputFilePath.setText(saveFolder) self.resetProgBar() self.writeStatus("Loaded Info") @err_decorator def selOutputPath(self): initialDir = self.ui.e_outputFilePath.text() self.outputPath = QFileDialog(self, "Open file", initialDir) self.outputPath.setFileMode(QFileDialog.Directory) if self.outputPath.exec_() == QFileDialog.Accepted: selDir = self.outputPath.selectedFiles()[0] selDir = os.path.abspath(selDir) self.ui.e_outputFilePath.setText(selDir) self.resetProgBar() self.writeStatus("Changed Output Folder") @err_decorator def makeSaveFileName(self, *ags): fileExt = self.ui.cb_outputContain.currentText() fileNameNoExt, ext = os.path.splitext(self.ui.e_outputFileName.text()) self.outputPath = fileNameNoExt + fileExt self.ui.e_outputFileName.setText(self.outputPath) self.resetProgBar() @err_decorator def getArgs(self): # Builds the FFMPEG command selOverwrite = self.ui.chb_overwrite.isChecked() if selOverwrite: overWrite = "-y" else: overWrite = "" inColor = self.inputColList[self.ui.cb_inputColSpace.currentText()] selFrameRate = self.ui.cb_outputFPS.currentText() frameRate = "-r " + selFrameRate firstFile = self.firstFile # Needed if start frame is not 1 startNumber = "-start_number " + str(firstFile) scale = self.scaleList[self.ui.cb_outputScale.currentText()] outQual = self.qualList[self.ui.cb_outputQual.currentText()] outColor = self.outputColList[self.ui.cb_outputColSpace.currentText()] inputArgs = f'{overWrite} {inColor} {frameRate} {startNumber}' outputArgs = f'{scale} {outQual} {outColor}' return inputArgs,outputArgs @err_decorator def getMetaData(self): metaDataList = [] # Builds Metadata for info if "program" in self.jData["information"]: dcc = self.jData["information"]["program"] metaDataList.append(rf' -metadata "DCC"="{dcc}"') if "projectName" in self.jData["information"]: project = self.jData["information"]["projectName"] metaDataList.append(rf' -metadata "Project"="{project}"') if "frameRate" in self.jData["information"]: projFrameRate = self.jData["information"]["frameRate"] metaDataList.append(rf' -metadata "Project Frame Rate"="{projFrameRate}"') if "jobName" in self.jData["information"]: shot = self.jData["information"]["jobName"] metaDataList.append(rf' -metadata "Shot"={shot}') if "Render Layer" in self.jData["information"]: renderLayer = self.jData["information"]["Render Layer"] metaDataList.append(rf' -metadata "Render Layer"="{renderLayer}"') if "camera" in self.jData["information"]: renderCam = self.jData["information"]["camera"] metaDataList.append(rf' -metadata "Render Camera"="{renderCam}"') if "render resolution" in self.jData["information"]: renderRez = self.jData["information"]["render resolution"] metaDataList.append(rf' -metadata "Render Resolution"="{renderRez}"') if "frameRange" in self.jData["information"]: renderRange = self.jData["information"]["frameRange"] metaDataList.append(rf' -metadata "Render Frame Range"="{renderRange}"') if "renderSamples" in self.jData["information"]: renderSamples = self.jData["information"]["renderSamples"] metaDataList.append(rf' -metadata "Render Samples"="{renderSamples}"') if "Render Color Management" in self.jData["information"]: renderColMgt = self.jData["information"]["Render Color Management"] metaDataList.append(rf' -metadata "Render Color Management"="{renderColMgt}"') if "File Format" in self.jData["information"]: fileFormat = self.jData["information"]["File Format"] metaDataList.append(rf' -metadata "Render Image Format"="{fileFormat}"') if "alpha" in self.jData["information"]: alpha = self.jData["information"]["alpha"] metaDataList.append(rf' -metadata "Render Image - Alpha"="{alpha}"') if self.ui.e_metaNote.text() != "": metaDataList.append(rf' -metadata "Note"="{self.ui.e_metaNote.text()}"') metaDataList = " ".join(metaDataList) metaDataSuffix = " -movflags +use_metadata_tags" metaData = metaDataList + metaDataSuffix return metaData @err_decorator def execute(self): self.resetProgBar() ffmpg = f'"{self.FFMPG}"' # Gets FFMPEG path from Pandora settings file inputArgs, outputArgs = self.getArgs() metaData = self.getMetaData() totalFrames = int(self.ui.e_renderedFrames.text()) folder, fileName = os.path.split(self.inputPath) fileNameNoExt, ext = os.path.splitext(fileName) fileNameNoExt = fileNameNoExt[:-self.paddingNum] sequenceExt = rf"%0{self.paddingNum}d" # Makes the output filename with the sequence padding num sourceFile = (os.path.join(folder, fileNameNoExt) + sequenceExt + ext) sourceFile = f'"{sourceFile}"' outputPath = self.ui.e_outputFilePath.text() outputFileName = self.ui.e_outputFileName.text() outputFile = os.path.join(outputPath, outputFileName) checkFile = outputFile outputFile = f'"{outputFile}"' command = f'{ffmpg} {inputArgs} -i {sourceFile} {metaData} {outputArgs} {outputFile}' if os.path.isfile(checkFile) and not self.ui.chb_overwrite.isChecked(): self.writeStatus("") self.writeStatus("File Exists and Overwrite NOT Enabled") self.ui.progressBar.setStyleSheet("background-color: red;") else: self.writeStatus("") # TESTING self.writeStatus(command) # TESTING self.writeStatus("") # TESTING self.renderThread = RenderThread(command, totalFrames, self.updateProgressBar) # Calls FFMPEG thread self.renderThread.start() @err_decorator def resetProgBar(self, *ags): # Reset Progbar to Zero self.ui.progressBar.setStyleSheet("background-color: black;") self.ui.progressBar.setValue(0) @err_decorator # Update Progbar with FFMPEG output def updateProgressBar(self, progress, message=None): self.errorCode = 0 if progress is not None: if progress < 5: progress = 5 self.ui.progressBar.setValue(progress) if message: self.writeStatus(message) if progress == 100: self.errorCode = 1 if self.errorCode == 1: self.writeStatus("Encode Complete") self.ui.progressBar.setStyleSheet(""" QProgressBar::chunk { background-color: #0BDA51; } """) @err_decorator def cancel(self): if self.renderThread is None: pass else: reply = QMessageBox.question( self, "Cancel Encode", "Are you sure?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: if self.renderThread is not None: self.renderThread.terminate() self.renderThread = None self.ui.progressBar.setStyleSheet("background-color: red;") self.writeStatus("") self.writeStatus("Encode Canceled by User") else: pass @err_decorator def exit(self): self.close() if __name__ == "__main__": app = QtWidgets.QApplication([]) try: app.setStyleSheet(qdarkstyle.load_stylesheet(pyside=True)) except NameError: pass previewMaker = PreviewMaker() previewMaker.show() sys.exit(app.exec_()) ```
Author
Owner

Thanks for the script @JBreckeen. Am I right that it's parsing FFmpeg's regular output, and is not using its -progress argument to send progress reports?

(I'm asking because I couldn't find this, but the script you sent is large so I might have missed something)

Thanks for the script @JBreckeen. Am I right that it's parsing FFmpeg's regular output, and is not using its `-progress` argument to send progress reports? (I'm asking because I couldn't find this, but the script you sent is large so I might have missed something)

My pleasure.

And correct, it is just reading the "output = self.proc.stderr.readline()" and then:

            if output:
                if isinstance(output, bytes):
                    output = output.decode()
                match = re.search(r"frame=\s*(\d+)", output)
                if match:
                    frame_num = int(match.group(1))
                    progress = round((frame_num / self.totalFrames) * 100, 2)
                    self.updateCallback(progress)

And example command is:

"C:\Program Files\ffmpeg-2023-02-27-git-891ed24f77-full_build\bin\ffmpeg.exe"  -gamma 2.2 -r 30 -start_number 1 -i "N:\Data\Renders\Modern House with Attached Garage\Rough Orbit\v02\Rough Orbit - v02 - %03d.exr"  -metadata "DCC"="Blender"  -metadata "Project"="Modern House with Attached Garage"  -metadata "Project Frame Rate"="30"  -metadata "Shot"="Rough Orbit - v02"  -metadata "Render Layer"="All"  -metadata "Render Camera"="Camera"  -metadata "Render Resolution"="1920 x 1080"  -metadata "Render Frame Range"="1-250"  -metadata "Render Samples"="200"  -metadata "Render Color Management"="Device: sRGB,   View: Filmic,   Look: None"  -metadata "Render Image Format"="EXR"  -metadata "Render Image - Alpha"="False" -movflags +use_metadata_tags -vf "scale=iw*1:ih*1" -crf 23 -pix_fmt yuv420p -color_primaries bt709 -color_trc bt709 -colorspace bt709 "C:\Users\Alta Arts\Desktop\Rough Orbit - v02 - Preview.mp4"

And just calling it using subprocess in a thread with a hidden window:

        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE

        self.proc = subprocess.Popen(PROC
            self.command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            bufsize=1,
            universal_newlines=True,
            startupinfo=startupinfo
        )

It seems to be working without errors now. I did keep finding failures such as when the first frame was not "1", but solved it. Have not had an error in awhile now.

But again, I am not coder and there might be better ways to do it . . .

J.

My pleasure. And correct, it is just reading the "output = self.proc.stderr.readline()" and then: ``` if output: if isinstance(output, bytes): output = output.decode() match = re.search(r"frame=\s*(\d+)", output) if match: frame_num = int(match.group(1)) progress = round((frame_num / self.totalFrames) * 100, 2) self.updateCallback(progress) ``` And example command is: ``` "C:\Program Files\ffmpeg-2023-02-27-git-891ed24f77-full_build\bin\ffmpeg.exe" -gamma 2.2 -r 30 -start_number 1 -i "N:\Data\Renders\Modern House with Attached Garage\Rough Orbit\v02\Rough Orbit - v02 - %03d.exr" -metadata "DCC"="Blender" -metadata "Project"="Modern House with Attached Garage" -metadata "Project Frame Rate"="30" -metadata "Shot"="Rough Orbit - v02" -metadata "Render Layer"="All" -metadata "Render Camera"="Camera" -metadata "Render Resolution"="1920 x 1080" -metadata "Render Frame Range"="1-250" -metadata "Render Samples"="200" -metadata "Render Color Management"="Device: sRGB, View: Filmic, Look: None" -metadata "Render Image Format"="EXR" -metadata "Render Image - Alpha"="False" -movflags +use_metadata_tags -vf "scale=iw*1:ih*1" -crf 23 -pix_fmt yuv420p -color_primaries bt709 -color_trc bt709 -colorspace bt709 "C:\Users\Alta Arts\Desktop\Rough Orbit - v02 - Preview.mp4" ``` And just calling it using subprocess in a thread with a hidden window: ``` startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE self.proc = subprocess.Popen(PROC self.command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True, startupinfo=startupinfo ) ``` It seems to be working without errors now. I did keep finding failures such as when the first frame was not "1", but solved it. Have not had an error in awhile now. But again, I am not coder and there might be better ways to do it . . . J.
Author
Owner

Running FFmpeg is handled, we already have code for that :)

The question I have mostly is what would be a better approach in the long run:

  • Using -progress will give us output from FFmpeg that's intended to be read by code. It is likely more stable / future-proof, but also requires a bit more effort on the Flamenco side (it needs to open a network socket to receive the info).
  • Using your approach is easier on the short term, as it can just use FFmpeg's standard output. I'm a little hesitant to parse that, though, as it's surrounded by other output that could cause confusion on the Flamenco side.
Running FFmpeg is handled, we already have code for that :) The question I have mostly is what would be a better approach in the long run: - Using `-progress` will give us output from FFmpeg that's intended to be read by code. It is likely more stable / future-proof, but also requires a bit more effort on the Flamenco side (it needs to open a network socket to receive the info). - Using your approach is easier on the short term, as it can just use FFmpeg's standard output. I'm a little hesitant to parse that, though, as it's surrounded by other output that could cause confusion on the Flamenco side.
Sign in to join this conversation.
No Milestone
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: studio/flamenco#99912
No description provided.