从 ffmpeg 获取实时输出以在进度条中使用(PyQt4,stdout)

Posted

技术标签:

【中文标题】从 ffmpeg 获取实时输出以在进度条中使用(PyQt4,stdout)【英文标题】:Getting realtime output from ffmpeg to be used in progress bar (PyQt4, stdout) 【发布时间】:2011-11-29 18:16:52 【问题描述】:

我已经查看了许多问题,但仍然无法完全弄清楚这一点。我正在使用 PyQt,并希望运行 ffmpeg -i file.mp4 file.avi 并在流式传输时获取输出,以便创建进度条。

我查看了以下问题: Can ffmpeg show a progress bar? catching stdout in realtime from subprocess

我可以看到 rsync 命令的输出,使用以下代码:

import subprocess, time, os, sys

cmd = "rsync -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print("OUTPUT>>> " + str(line.rstrip()))
    p.stdout.flush()

但是当我将命令更改为 ffmpeg -i file.mp4 file.avi 时,我没有收到任何输出。我猜这与标准输出/输出缓冲有关,但我不知道如何读取看起来像

的行
frame=   51 fps= 27 q=31.0 Lsize=     769kB time=2.04 bitrate=3092.8kbits/s

我可以用它来计算进度。

有人可以告诉我一个例子,说明如何使用或不使用 PyQt(如果可能的话)将这些信息从 ffmpeg 获取到 python 中


编辑: 我最终选择了 jlp 的解决方案,我的代码如下所示:

#!/usr/bin/python
import pexpect

cmd = 'ffmpeg -i file.MTS file.avi'
thread = pexpect.spawn(cmd)
print "started %s" % cmd
cpl = thread.compile_pattern_list([
    pexpect.EOF,
    "frame= *\d+",
    '(.+)'
])
while True:
    i = thread.expect_list(cpl, timeout=None)
    if i == 0: # EOF
        print "the sub process exited"
        break
    elif i == 1:
        frame_number = thread.match.group(0)
        print frame_number
        thread.close
    elif i == 2:
        #unknown_line = thread.match.group(0)
        #print unknown_line
        pass

这给出了这个输出:

started ffmpeg -i file.MTS file.avi
frame=   13
frame=   31
frame=   48
frame=   64
frame=   80
frame=   97
frame=  115
frame=  133
frame=  152
frame=  170
frame=  188
frame=  205
frame=  220
frame=  226
the sub process exited

完美!

【问题讨论】:

您在 edit 中的代码看起来不正确(并且对我不起作用)...我认为您不想捕获通配符模式并且什么都不做(您只需要捕获您关心的模式),更重要的是 - 您希望 thread.close 在 while 循环之外,而不是在您第一次捕获您感兴趣的模式时调用。 @jlp 的代码似乎更正确,一旦适应了 ffmpeg 输出,就可以为我工作。 如果是Python3,应该是:frame_number = thread.match.group(0).decode('utf-8') 对于捕获的错误,您应该在 while 后面加上:thread.close()if thread.exitstatus:print(thread.before)else:print('Ok') 这部分'(.+)'在代码中做了什么?另外,我正在使用的程序,我需要检测输出失败,有没有办法做多种模式?谢谢。 【参考方案1】:

我写了一个专门的包,它为你在 Python 中的 ffmpeg 进度提供了一个生成器函数:ffmpeg-progress-yield

简单运行:

pip3 install ffmpeg-progress-yield

然后,只需这样做:

from ffmpeg_progress_yield import FfmpegProgress

cmd = [
    "ffmpeg", "-i", "test/test.mp4", "-c:v", "libx264", "-vf", "scale=1920x1080", "-preset", "fast", "-f", "null", "/dev/null",
]

ff = FfmpegProgress(cmd)
for progress in ff.run_command_with_progress():
    print(f"progress/100")

请注意,这只适用于预先知道持续时间的输入文件。

【讨论】:

【参考方案2】:

您也可以通过将 QProcess 中的插槽连接到 QTextEdit 或其他任何东西,使用 PyQt4 的 QProcess(如原始问题中所问)非常清楚地做到这一点。我对 python 和 pyqt 还是很陌生,但我就是这样做的:

import sys
from PyQt4 import QtCore, QtGui

class ffmpegBatch(QtGui.QWidget):
    def __init__(self):
        super(ffmpegBatch, self).__init__()
        self.initUI()

    def initUI(self):
        layout = QtGui.QVBoxLayout()
        self.edit = QtGui.QTextEdit()
        self.edit.setGeometry(300, 300, 300, 300)
        run = QtGui.QPushButton("Run process")

        layout.addWidget(self.edit)
        layout.addWidget(run)

        self.setLayout(layout)

        run.clicked.connect(self.run)

    def run(self):
        # your commandline whatnot here, I just used this for demonstration
        cmd = "systeminfo"

        proc = QtCore.QProcess(self)
        proc.setProcessChannelMode(proc.MergedChannels)
        proc.start(cmd)
        proc.readyReadStandardOutput.connect(lambda: self.readStdOutput(proc))


    def readStdOutput(self, proc):
        self.edit.append(QtCore.QString(proc.readAllStandardOutput()))

def main():
    app = QtGui.QApplication(sys.argv)
    ex = ffmpegBatch()
    ex.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

【讨论】:

【参考方案3】:

在这种捕获 ffmpeg 状态输出(发送到 STDERR)的特定情况下,这个 SO 问题为我解决了它:FFMPEG and Pythons subprocess

诀窍是将universal_newlines=True 添加到subprocess.Popen() 调用中,因为ffmpeg 的输出实际上是无缓冲的,但带有换行符。

cmd = "ffmpeg -i in.mp4 -y out.avi"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True)
for line in process.stdout:
    print(line)

另请注意,在此代码示例中,STDERR 状态输出直接重定向到subprocess.STDOUT

【讨论】:

很高兴看到有人提供链接仅供参考。很多人都将它们用作整个答案。 对我不起作用,至少在 python 3.9.0b5、ffmpeg 4.3-2、WSL2 Ubuntu 上 谢谢,这正是我需要的。【参考方案4】:

这个答案对我不起作用:/这是我做的方式。

它来自我的项目KoalaBeatzHunter。

享受吧!

def convertMp4ToMp3(mp4f, mp3f, odir, kbps, callback=None, efsize=None):
    """
    mp4f:     mp4 file
    mp3f:     mp3 file
    odir:     output directory
    kbps:     quality in kbps, ex: 320000
    callback: callback() to recieve progress
    efsize:   estimated file size, if there is will callback() with %
    Important:
    communicate() blocks until the child process returns, so the rest of the lines 
    in your loop will only get executed after the child process has finished running. 
    Reading from stderr will block too, unless you read character by character like here.
    """
    cmdf = "ffmpeg -i "+ odir+mp4f +" -f mp3 -ab "+ str(kbps) +" -vn "+ odir+mp3f
    lineAfterCarriage = ''

    print deleteFile(odir + mp3f)

    child = subprocess.Popen(cmdf, shell=True, stderr=subprocess.PIPE)

    while True:
        char = child.stderr.read(1)
        if char == '' and child.poll() != None:
            break
        if char != '':
            # simple print to console
#             sys.stdout.write(char)
#             sys.stdout.flush()
            lineAfterCarriage += char
            if char == '\r':
                if callback:
                    size = int(extractFFmpegFileSize(lineAfterCarriage)[0])
                    # kb to bytes
                    size *= 1024
                    if efsize:
                        callback(size, efsize)
                lineAfterCarriage = ''

接下来,你还需要 3 个函数来实现它。

def executeShellCommand(cmd):
    p = Popen(cmd , shell=True, stdout=PIPE, stderr=PIPE)
    out, err = p.communicate()
    return out.rstrip(), err.rstrip(), p.returncode

def getFFmpegFileDurationInSeconds(filename):
    cmd = "ffmpeg -i "+ filename +" 2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//"
    time = executeShellCommand(cmd)[0]
    h = int(time[0:2])
    m = int(time[3:5])
    s = int(time[6:8])
    ms = int(time[9:11])
    ts = (h * 60 * 60) + (m * 60) + s + (ms/60)
    return ts

def estimateFFmpegMp4toMp3NewFileSizeInBytes(duration, kbps):
    """
    * Very close but not exact.
    duration: current file duration in seconds
    kbps: quality in kbps, ex: 320000
    Ex:
        estim.:    12,200,000
        real:      12,215,118
    """
    return ((kbps * duration) / 8)

最后你做到了:

# get new mp3 estimated size
secs = utls.getFFmpegFileDurationInSeconds(filename)
efsize = utls.estimateFFmpegMp4toMp3NewFileSizeInBytes(secs, 320000)
print efsize

utls.convertMp4ToMp3("AwesomeKoalaBeat.mp4", "AwesomeKoalaBeat.mp3",
                "../../tmp/", 320000, utls.callbackPrint, efsize)

希望这会有所帮助!

【讨论】:

【参考方案5】:

我发现从子进程获取动态反馈/输出的唯一方法是使用 pexpect 之类的东西:

#! /usr/bin/python

import pexpect

cmd = "foo.sh"
thread = pexpect.spawn(cmd)
print "started %s" % cmd
cpl = thread.compile_pattern_list([pexpect.EOF,
                                   'waited (\d+)'])
while True:
    i = thread.expect_list(cpl, timeout=None)
    if i == 0: # EOF
        print "the sub process exited"
        break
    elif i == 1:
        waited_time = thread.match.group(1)
        print "the sub process waited %d seconds" % int(waited_time)
thread.close()

被调用的子进程 foo.sh 只是等待 10 到 20 秒之间的随机时间,这里是它的代码:

#! /bin/sh

n=5
while [ $n -gt 0 ]; do
    ns=`date +%N`
    p=`expr $ns % 10 + 10`
    sleep $p
    echo waited $p
    n=`expr $n - 1`
done

您需要使用一些与您从 ffmpeg 获得的输出相匹配的正则表达式,并对其进行某种计算以显示进度条,但这至少可以让您获得 ffmpeg 的无缓冲输出。

【讨论】:

正是我想要的,谢谢。我见过 pexpect 但还没有弄清楚如何使用它,您的示例非常清楚地展示了它。对不起,我是新手,没有积分可以投票给你的答案! 没有汗水。我也是新手。其他人会在某个时候投票赞成。很高兴它有帮助。 我相信你是对的,@Anentropic 编辑了我的帖子以反映正确的调用约定。 @jlp 很酷,谢谢你的回答帮助我开始使用 pexpect 在我的情况下,python 脚本,以下行不起作用。 thread = pexpect.spawn(cmd) 我将上一行 cmd = "foo.sh" 改为 cmd = "./foo.sh" 希望对某人有所帮助:)【参考方案6】:

如果您有持续时间(您也可以从 FFMPEG 输出中获得),您可以通过读取编码时的经过时间(时间)输出来计算进度。

一个简单的例子:

  pipe = subprocess.Popen(
        cmd,
        stderr=subprocess.PIPE,
        close_fds=True
  )
  fcntl.fcntl(
        pipe.stderr.fileno(),
        fcntl.F_SETFL,
        fcntl.fcntl(pipe.stderr.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK,
  )
   while True:
            readx = select.select([pipe.stderr.fileno()], [], [])[0]

            if readx: 
                chunk = pipe.stderr.read()

                if not chunk:
                    break

                result = re.search(r'\stime=(?P<time>\S+) ', chunk)
                elapsed_time = float(result.groupdict()['time'])

                # Assuming you have the duration in seconds
                progress = (elapsed_time / duration) * 100

                # Do something with progress here
                callback(progress)

        time.sleep(10)

【讨论】:

不幸的是,选择不适用于不是从 Windows 上的 WinSock 创建的文件 :(【参考方案7】:
    通常不需要从 shell 调用。 根据经验,我知道部分 ffmpeg 输出来自 stderr,而不是 stdout

如果您只想打印输出行,就像上面的示例一样,那么只需这样做:

import subprocess

cmd = 'ffmpeg -i file.mp4 file.avi'
args = cmd.split()

p = subprocess.Popen(args)

注意,ffmpeg 聊天的那一行是以\r 结束的,所以会覆盖在同一行!我认为这意味着您不能像处理 rsync 示例那样遍历 p.stderr 中的行。那么,要构建自己的进度条,您可能需要自己处理阅读,这应该可以帮助您开始:

p = subprocess.Popen(args, stderr=subprocess.PIPE)

while True:
  chatter = p.stderr.read(1024)
  print("OUTPUT>>> " + chatter.rstrip())

【讨论】:

以上是关于从 ffmpeg 获取实时输出以在进度条中使用(PyQt4,stdout)的主要内容,如果未能解决你的问题,请参考以下文章

获取 ASP.NET GridView 单元格值并在 JQuery 进度条中显示

长度在进度条中显示减号

Android 集成 FFmpeg 获取 FFmpeg 执行进度

使用 ffmpeg 获取帧数

网页载入进度条中的javascript

无法从从 Firebase 实时数据库中检索数据以在以下代码行中使用它的方法中获取返回值 [重复]