Ffmpeg-python: Ability to track progress of an ffmpeg command

Created on 14 Dec 2017  路  9Comments  路  Source: kkroening/ffmpeg-python

Is there a way to track a progress after running an ffmpeg?

For example, below is what i would like to do:

import ffmpeg
ffmpeg.input('test.mp4').output('frame_%06d.jpg').run()

This command writes each frame of a video as an image to disk.

At the very least it would be great if we could see the output generated by running ffmpeg on commandline ffmpeg -i test.mp4 %06d.png -hide_banner

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'workflow_video_01.mp4':
  Metadata:
    major_brand     : mp42
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    creation_time   : 2036-02-06 06:28:16
    encoder         : HandBrake 0.10.2 2015060900
  Duration: 00:50:57.00, start: 0.000000, bitrate: 7040 kb/s
    Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1920x1080 [SAR 1:1 DAR 16:9], 7038 kb/s, 25 fps, 25 tbr, 90k tbn, 50 tbc (default)
    Metadata:
      creation_time   : 2036-02-06 06:28:16
      handler_name    : VideoHandler
Output #0, image2, to 'dump/%06d.png':
  Metadata:
    major_brand     : mp42
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf56.40.101
    Stream #0:0(und): Video: png, rgb24, 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc (default)
    Metadata:
      creation_time   : 2036-02-06 06:28:16
      handler_name    : VideoHandler
      encoder         : Lavc56.60.100 png
Stream mapping:
  Stream #0:0 -> #0:0 (h264 (native) -> png (native))
Press [q] to stop, [?] for help
frame=  677 fps= 59 q=-0.0 Lsize=N/A time=00:00:27.08 bitrate=N/A  

Any ideas on how to do this?

enhancement

Most helpful comment

Perhaps the above example could be condensed into something like the following at some point:

def progress_handler(progress_info):
    print('{:.2f}'.format(progress_info['percentage']))

(ffmpeg
    .input('in.mp4')
    .output('out.mp4')
    .progress(progress_handler)
    .overwrite_output()
    .run()
)

All 9 comments

I think these are the only options:

From man ffmpeg:

-progress url (global)
   Send program-friendly progress information to url.

   Progress information is written approximately every second and at the end of the
   encoding process. It is made of "key=value" lines. key consists of only alphanumeric
   characters. The last key of a sequence of progress information is always "progress".

To implement either of these, the programs that use this library need to be structured for some degree of concurrency.

The progress checking code either needs to be run asynchronously or in another thread. In the first case for obvious reasons: you constantly need to poll the socket for incoming connections; in the second case to avoid filling up the stderr buffer in case you wait for too long between one poll and another, unless you're okay with ffmpeg randomly getting stuck.

This can be done with threading.Thread, twisted, asyncio or anything else, probably except multiprocessing (having a separate process do the polling thing would totally be an overkill and would only make things more complicated).

Just a note for those looking for a quick solution: If you run ffmpeg from a jupyter notebook, you can see the stdout and stderr from the ffmpeg command in the terminal from which jupyter was started. I didn't notice this at first, since I tend to keep that terminal minimized.

Here's a semi-hacked-together example using gevent and the -progress param @Depau mentioned:

import gevent.monkey; gevent.monkey.patch_all()

import contextlib
import ffmpeg
import gevent
import os
import shutil
import socket
import subprocess
import sys
import tempfile


@contextlib.contextmanager
def _tmpdir_scope():
    tmpdir = tempfile.mkdtemp()
    try:
        yield tmpdir
    finally:
        shutil.rmtree(tmpdir)


def _watch_progress(filename, sock, handler):
    connection, client_address = sock.accept()
    data = ''
    with contextlib.closing(connection):
        while True:
            more_data = connection.recv(16)
            if not more_data:
                break
            data += more_data
            lines = data.split('\n')
            for line in lines[:-1]:
                parts = line.split('=')
                key = parts[0] if len(parts) > 0 else None
                value = parts[1] if len(parts) > 1 else None
                handler(key, value)
            data = lines[-1]


@contextlib.contextmanager
def watch_progress(handler):
    with _tmpdir_scope() as tmpdir:
        filename = os.path.join(tmpdir, 'sock')
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        with contextlib.closing(sock):
            sock.bind(filename)
            sock.listen(1)
            child = gevent.spawn(_watch_progress, filename, sock, handler)
            try:
                yield filename
            except:
                gevent.kill(child)
                raise


duration = float(ffmpeg.probe('in.mp4')['format']['duration'])

prev_text = None

def handler(key, value):
    global prev_text
    if key == 'out_time_ms':
        text = '{:.02f}%'.format(float(value) / 10000. / duration)
        if text != prev_text:
            print(text)
            prev_text = text


with watch_progress(handler) as filename:
    p = subprocess.Popen(
        (ffmpeg
            .input('in.mp4')
            .output('out.mp4')
            .global_args('-progress', 'unix://{}'.format(filename))
            .overwrite_output()
            .compile()
        ),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
    out = p.communicate()

if p.returncode != 0:
    sys.stderr.write(out[1])
    sys.exit(1)
  • It creates and listens on a unix-domain socket and passes that to ffmpeg.
  • The watcher runs in its own gevent greenlet, but it could be changed to use actual threads instead if desired.
  • In order to know the progress percentage, it uses the duration reported by ffprobe, since the normal progress info doesn't seem to contain such info.
  • It runs with subprocess.Popen to silence stdout/stderr. There's probably a better way to silence ffmpeg, in which case .run() can be used instead of calling subprocess manually.

I'm not sure if/when this will be incorporated into ffmpeg-python, but something to play with in the mean time.

Perhaps the above example could be condensed into something like the following at some point:

def progress_handler(progress_info):
    print('{:.2f}'.format(progress_info['percentage']))

(ffmpeg
    .input('in.mp4')
    .output('out.mp4')
    .progress(progress_handler)
    .overwrite_output()
    .run()
)

i think this implementation is nice: https://github.com/althonos/ffpb
The output is designed for a CLI but the idea of providing a callable collection looks good.
Maybe getting in contact with the author and see if he can integrate here or allows us to use his design ?

what's the release plan about this?

I found another ffmpeg python wrapper asynchronously parsing stderr:

https://github.com/jonghwanhyeon/python-ffmpeg/blob/ccfbba93c46dc0d2cafc1e40ecb71ebf3b5587d2/ffmpeg/ffmpeg.py#L114

using regular expression r'(frame|fps|size|time|bitrate|speed)\s*\=\s*(\S+)':
https://github.com/jonghwanhyeon/python-ffmpeg/blob/ccfbba93c46dc0d2cafc1e40ecb71ebf3b5587d2/ffmpeg/utils.py#L8

Updates on this?

@kkroening You there? We still waiting on this update.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

monokal picture monokal  路  3Comments

tossorrow picture tossorrow  路  6Comments

Matttman picture Matttman  路  6Comments

oveddan picture oveddan  路  3Comments

prajwal-mulkalwar picture prajwal-mulkalwar  路  6Comments