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

时间:2011-10-03 08:20:45

标签: python ffmpeg pyqt subprocess stdout

我看了很多问题,但仍然无法弄清楚这一点。我正在使用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缓冲有关,但我不知道如何读取看起来像

的行
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

完美!

7 个答案:

答案 0 :(得分:14)

我发现从子进程获得动态反馈/输出的唯一方法是使用像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的无缓冲输出。 / p>

答案 1 :(得分:13)

在捕获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

答案 2 :(得分:3)

  1. 通常不需要从shell调用。
  2. 我从经验中知道,部分ffmpeg输出来自stderr,而不是stdout
  3. 如果您只想打印输出线,就像上面的示例一样,那么这样就可以了:

    import subprocess
    
    cmd = 'ffmpeg -i file.mp4 file.avi'
    args = cmd.split()
    
    p = subprocess.Popen(args)
    

    请注意,ffmpeg聊天行以\r终止,因此它会覆盖同一行!我认为这意味着您不能像{rs}示例那样迭代p.stderr中的行。要建立自己的进度条,那么,您可能需要自己处理阅读,这应该让您开始:

    p = subprocess.Popen(args, stderr=subprocess.PIPE)
    
    while True:
      chatter = p.stderr.read(1024)
      print("OUTPUT>>> " + chatter.rstrip())
    

答案 3 :(得分: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)

希望这会有所帮助!

答案 4 :(得分:1)

这里是一个dedicated function,以百分比显示进度,它可以与您可能已经拥有的任何ffmpeg命令一起使用(作为字符串列表):

for progress in run_ffmpeg_command(["ffmpeg", "-i", "test.mp4", "test2.mp4"])
  print(progress)

这将打印0到100。

这个想法是启用-progress选项,从stderr输出解析持续时间,然后,在获得进度时间后,只需对其进行划分即可。该代码是从this Gist借来的。

import subprocess
import re
from typing import Iterator

DUR_REGEX = re.compile(
    r"Duration: (?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})"
)
TIME_REGEX = re.compile(
    r"out_time=(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})"
)


def to_ms(s=None, des=None, **kwargs) -> float:
    if s:
        hour = int(s[0:2])
        minute = int(s[3:5])
        sec = int(s[6:8])
        ms = int(s[10:11])
    else:
        hour = int(kwargs.get("hour", 0))
        minute = int(kwargs.get("min", 0))
        sec = int(kwargs.get("sec", 0))
        ms = int(kwargs.get("ms", 0))

    result = (hour * 60 * 60 * 1000) + (minute * 60 * 1000) + (sec * 1000) + ms
    if des and isinstance(des, int):
        return round(result, des)
    return result


def run_ffmpeg_command(cmd: "list[str]") -> Iterator[int]:
    """
    Run an ffmpeg command, trying to capture the process output and calculate
    the duration / progress.
    Yields the progress in percent.
    """
    total_dur = None

    cmd_with_progress = [cmd[0]] + ["-progress", "-", "-nostats"] + cmd[1:]

    stderr = []

    p = subprocess.Popen(
        cmd_with_progress,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=False,
    )

    while True:
        line = p.stdout.readline().decode("utf8", errors="replace").strip()
        if line == "" and p.poll() is not None:
            break
        stderr.append(line.strip())

        if not total_dur and DUR_REGEX.search(line):
            total_dur = DUR_REGEX.search(line).groupdict()
            total_dur = to_ms(**total_dur)
            continue
        if total_dur:
            result = TIME_REGEX.search(line)
            if result:
                elapsed_time = to_ms(**result.groupdict())
                yield int(elapsed_time / total_dur * 100)

    if p.returncode != 0:
        raise RuntimeError(
            "Error running command {}: {}".format(cmd, str("\n".join(stderr)))
        )

    yield 100

答案 5 :(得分:0)

如果您有持续时间(您也可以从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)

答案 6 :(得分:0)

您还可以通过将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()