如何在进度条中实时显示 ffmpeg 输出内容(使用 PyQt4 和 stdout)?

31

我看了很多问题,但还是不能完全弄清楚。 我正在使用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时,我没有收到任何输出。我猜这与stdout / output buffering有关,但是我不知道如何读取看起来像的那一行
frame=   51 fps= 27 q=31.0 Lsize=     769kB time=2.04 bitrate=3092.8kbits/s

我希望能够使用它来确定进度。

有人可以向我展示如何将此信息从ffmpeg获取到Python中,无论是否使用PyQt(如果可能)


编辑:最终我采用了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

完美!


1
你在 edit 中的代码看起来不对(而且对我也不起作用)... 我认为你不想捕获通配符模式并什么都不做(你只需要捕获你关心的模式),更重要的是 - 你希望 thread.close 在 while 循环之外而不是在第一次捕获到你感兴趣的模式时调用。@jlp 的代码似乎更正确,一旦适应 ffmpeg 输出,它对我也有效。 - Anentropic
在Python3中,应该是:frame_number = thread.match.group(0).decode('utf-8') - o_ren
为了捕获错误,你应该在 while 循环后面加上:thread.close()if thread.exitstatus: print(thread.before) else: print('Ok') - Artem
这段代码中的 '(.+)' 是做什么用的?另外,我正在处理的程序中,我需要检测输出中的故障,有没有一种方法可以使用多个模式来实现?谢谢。 - ScipioAfricanus
9个回答

23
在这个特定的情况下,为了捕获ffmpeg的状态输出(其输出到STDERR),这个stackoverflow问题为我解决了这个问题:FFMPEG and Pythons subprocess
关键是在subprocess.Popen()调用中添加universal_newlines=True,因为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上是这样。 - user10685
谢谢,这正是我所需要的。 - franchyze923

17

我发现从子进程获取动态反馈/输出的唯一方法是使用类似 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,但不知道如何使用,你的示例非常清晰地演示了它。很抱歉我是新手,没有积分来投票支持你的回答! - Jason O'Neil
没问题。我也是新手。总会有其他人投票支持的。很高兴能帮到你。 - jlp
我相信你是对的,@Anentropic,我编辑了我的帖子以反映正确的调用约定。 - jlp
@jlp 很酷,谢谢你的回答,帮助我开始使用pexpect。 - Anentropic
在我的情况下,Python脚本中以下代码行未正常工作: thread = pexpect.spawn(cmd) 我修改了前面的这行 cmd = "foo.sh" cmd = "./foo.sh"希望对某些人有所帮助 :) - Hridaynath

3
  1. 通常不需要从shell中调用。
  2. 我通过经验得知,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())

3

我编写了一个专门的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")

请注意,此仅适用于输入文件事先已知持续时间的情况。

谢谢,现在我可以让一个Pyside6进度条与FFmpeg进程一起工作了... - user1098761

2
这些答案对我没用 :/ 这是我做的方式。
它来自我的项目 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)

希望这会有所帮助!

0
如果您有持续时间(您也可以从FFMPEG输出中获取),则可以通过读取编码时的经过时间(time)输出来计算进度。
一个简单的例子:
  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上,Select将无法在未从WinSock创建的文件上工作 :( - chaz

0
在ffmpeg-python的github上有一个例子--对于Windows机器来说,它不能直接使用(使用了Unix套接字)... https://github.com/kkroening/ffmpeg-python/blob/master/examples/show_progress.py 这里有一个简单的(在我看来更好的... :)) 视频转换脚本,带有进度显示(使用了ffmpeg-python和tqdm_rich): https://gist.github.com/pbouill/fddf767221b47f83c97d7813c03569c4

example video encoding/conversion with progress

不需要像tqdm文档建议的那样覆盖sys.stdout/stderr然后恢复,以获取子进程输出/拦截控制台输出(https://github.com/tqdm/tqdm/blob/master/examples/redirect_print.py)。
只需通过提供输入文件路径来创建一个视频转换器实例:
vc = VideoConverter(input_path=pathlib.Path('./path/to/my/file'))
vc.convert()

如果没有提供输出路径,则输出视频将放置在与输入文件相同的目录中,并附加“-converted.mp4”。您也可以调整输出/转换参数...默认情况下使用的是:
class VideoConverter:
    FF_OUTPUT_KWARGS = {
        'vcodec': 'libx265',
        'crf': 35 
    }
    ...

例如

vc = VideoConverter(
    input_path=pathlib.Path('./path/to/my/input_file.mp4'), 
    output_path=pathlib.Path('./path/to/my/output_file.mp4')),
    output_kwargs={
        'crf': 30,
        'vcodec': 'libx264'
    }
)
vc.convert()

您可以通过在调用“convert”时提供进度指标选择,来生成基于视频时间的进度报告或基于帧的进度报告。
class VideoConverter:
    ...
    class ProgressMetric(Enum):
            FRAMES = auto()
            TIME = auto()
    ...

vc.convert(self, progress_metric = VideoConverter.ProgressMetric.TIME)

你只需调用以下命令,即可将脚本目录中的所有内容转换:
VideoConverter.convert_all()

(还可以将输入目录、输出目录和输出关键字参数作为此函数的关键字参数进行指定)
希望这对你有所帮助!

0

你也可以使用PyQt4的QProcess来实现(就像原问题中所问的那样),通过将QProcess的一个槽连接到QTextEdit或其他控件上。虽然我对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()

0
看起来对我来说,使用ffmpeg的{{link1:-progress url(global)}}选项实际上是一个简单的解决方案。
ffmpeg -progress progress.log -i file.mp4 file.avi

文件progress.log将实时追加进度信息(更新周期使用-stats_period选项设置)。
信息将如下所示:
frame=584
fps=52.40
stream_0_0_q=0.0
bitrate=N/A
total_size=762
out_time_us=0
out_time_ms=0
out_time=00:00:00.000000
dup_frames=0
drop_frames=0
speed=   0x
progress=continue

从这些信息中,我们可以计算出进展情况。
一个更详细的例子在Programster的博客中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接