从ffmpeg中获得实时输出,用于进度条(PyQt4, stdout)

时间:2021-06-19 00:06:02

I've looked at a number of questions but still can't quite figure this out. I'm using PyQt, and am hoping to run ffmpeg -i file.mp4 file.avi and get the output as it streams so I can create a progress bar.

我看了很多问题,但还是搞不清楚。我正在使用PyQt,希望运行ffmpeg -i文件。mp4文件。avi并在它流时获取输出,以便我可以创建进度条。

I've looked at these questions: Can ffmpeg show a progress bar? catching stdout in realtime from subprocess

我看过这些问题:ffmpeg能显示进度条吗?从子进程中实时捕获stdout

I'm able to see the output of a rsync command, using this code:

使用以下代码,我可以看到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()

But when I change the command to ffmpeg -i file.mp4 file.avi I receive no output. I'm guessing this has something to do with stdout / output buffering, but I'm stuck as to how to read the line that looks like

但是当我将命令更改为ffmpeg -i文件时。mp4文件。我没有收到任何输出。我猜想这与stdout / output缓冲有关,但我还是不知道如何读取这一行

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

Which I could use to figure out progress.

我可以用它来计算进度。

Can someone show me an example of how to get this info from ffmpeg into python, with or without the use of PyQt (if possible)

有没有人能给我举个例子,说明如何从ffmpeg中获取这些信息到python中,是否使用PyQt(如果可能的话)


EDIT: I ended up going with jlp's solution, my code looked like this:

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

Which gives this output:

这使这个输出:

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

Perfect!

完美!

6 个解决方案

#1


13  

The only way I've found to get dynamic feedback/output from a child process is to use something like pexpect:

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

the called sub process foo.sh just waits a random amount of time between 10 and 20 seconds, here's the code for it:

所谓的子进程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

You'll want to use some regular expression that matches the output you're getting from ffmpeg and does some kind of calculation on it to show the progress bar, but this will at least get you the unbuffered output from ffmpeg.

您将希望使用一些与从ffmpeg获得的输出相匹配的正则表达式,并对其进行某种计算以显示进度条,但这至少将使您获得来自ffmpeg的未缓冲输出。

#2


7  

In this specific case for capturing ffmpeg's status output (which goes to STDERR), this SO question solved it for me: FFMPEG and Pythons subprocess

在捕获ffmpeg的状态输出(转到STDERR)的这个特定示例中,这个SO问题为我解决了这个问题:ffmpeg和python子进程

The trick is to add universal_newlines=True to the subprocess.Popen() call, because ffmpeg's output is in fact unbuffered but comes with newline-characters.

诀窍是将universal_newlines=True添加到子进程. 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)

Also note that in this code sample the STDERR status output is directly redirected to subprocess.STDOUT

还要注意,在这个代码示例中,STDERR状态输出直接重定向到subprocess.STDOUT

#3


2  

  1. Calling from the shell is generally not required.
  2. 通常不需要从shell调用。
  3. I know from experince that part of the ffmpeg output comes on stderr, not stdout.
  4. 我从经验中知道,ffmpeg的部分输出来自stderr,而不是stdout。

If all you want to do is print the output line, like in your example above, then simply this will do:

如果您只想打印输出行,如上面的示例所示,那么简单地如下所示:

import subprocess

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

p = subprocess.Popen(args)

Note that the line of ffmpeg chat is terminated with \r, so it will overwrite in the same line! I think this means you can't iterate over the lines in p.stderr, as you do with your rsync example. To build your own progress bar, then, you may need to handle the reading yourself, this should get you started:

注意,ffmpeg聊天行以\r终止,因此它将在同一行中覆盖!我认为这意味着你不能遍历p中的直线。stderr,就像你使用rsync的例子一样。为了建立你自己的进度条,你可能需要自己处理阅读,这应该让你开始:

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

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

#4


2  

This answers didn't worked for me :/ Here is the way I did it.

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

Its from my project KoalaBeatzHunter.

来自我的项目KoalaBeatzHunter。

Enjoy!

享受吧!

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 = ''

Next, you need 3 more functions to implement it.

接下来,还需要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)

And finally you do:

最后你做的事:

# 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)

Hope this will help!

希望这将帮助!

#5


0  

If you have the duration (Which you can also get from the FFMPEG output) you can calculate the progress by reading the elapsed time (time) output when encoding.

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

A simple example:

一个简单的例子:

  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  

You can also do it pretty clearly with PyQt4's QProcess (as asked in the original question) by connecting a slot from the QProcess to a QTextEdit or whatever. I'm still pretty new to python and pyqt but here's how I just managed to do it:

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

#1


13  

The only way I've found to get dynamic feedback/output from a child process is to use something like pexpect:

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

the called sub process foo.sh just waits a random amount of time between 10 and 20 seconds, here's the code for it:

所谓的子进程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

You'll want to use some regular expression that matches the output you're getting from ffmpeg and does some kind of calculation on it to show the progress bar, but this will at least get you the unbuffered output from ffmpeg.

您将希望使用一些与从ffmpeg获得的输出相匹配的正则表达式,并对其进行某种计算以显示进度条,但这至少将使您获得来自ffmpeg的未缓冲输出。

#2


7  

In this specific case for capturing ffmpeg's status output (which goes to STDERR), this SO question solved it for me: FFMPEG and Pythons subprocess

在捕获ffmpeg的状态输出(转到STDERR)的这个特定示例中,这个SO问题为我解决了这个问题:ffmpeg和python子进程

The trick is to add universal_newlines=True to the subprocess.Popen() call, because ffmpeg's output is in fact unbuffered but comes with newline-characters.

诀窍是将universal_newlines=True添加到子进程. 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)

Also note that in this code sample the STDERR status output is directly redirected to subprocess.STDOUT

还要注意,在这个代码示例中,STDERR状态输出直接重定向到subprocess.STDOUT

#3


2  

  1. Calling from the shell is generally not required.
  2. 通常不需要从shell调用。
  3. I know from experince that part of the ffmpeg output comes on stderr, not stdout.
  4. 我从经验中知道,ffmpeg的部分输出来自stderr,而不是stdout。

If all you want to do is print the output line, like in your example above, then simply this will do:

如果您只想打印输出行,如上面的示例所示,那么简单地如下所示:

import subprocess

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

p = subprocess.Popen(args)

Note that the line of ffmpeg chat is terminated with \r, so it will overwrite in the same line! I think this means you can't iterate over the lines in p.stderr, as you do with your rsync example. To build your own progress bar, then, you may need to handle the reading yourself, this should get you started:

注意,ffmpeg聊天行以\r终止,因此它将在同一行中覆盖!我认为这意味着你不能遍历p中的直线。stderr,就像你使用rsync的例子一样。为了建立你自己的进度条,你可能需要自己处理阅读,这应该让你开始:

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

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

#4


2  

This answers didn't worked for me :/ Here is the way I did it.

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

Its from my project KoalaBeatzHunter.

来自我的项目KoalaBeatzHunter。

Enjoy!

享受吧!

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 = ''

Next, you need 3 more functions to implement it.

接下来,还需要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)

And finally you do:

最后你做的事:

# 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)

Hope this will help!

希望这将帮助!

#5


0  

If you have the duration (Which you can also get from the FFMPEG output) you can calculate the progress by reading the elapsed time (time) output when encoding.

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

A simple example:

一个简单的例子:

  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  

You can also do it pretty clearly with PyQt4's QProcess (as asked in the original question) by connecting a slot from the QProcess to a QTextEdit or whatever. I'm still pretty new to python and pyqt but here's how I just managed to do it:

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