ffmpeg-python icon indicating copy to clipboard operation
ffmpeg-python copied to clipboard

Ability to track progress of an ffmpeg command

Open cdeepakroy opened this issue 7 years ago • 16 comments

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?

cdeepakroy avatar Dec 14 '17 16:12 cdeepakroy

I think these are the only options:

  • Parse the progress lines from stderr: https://github.com/bramp/ffmpeg-cli-wrapper/issues/21
  • Listening on some port for progress notices from ffmpeg: https://stackoverflow.com/a/31353647

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

depau avatar Dec 15 '17 09:12 depau

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.

rudolfbyker avatar Dec 23 '17 14:12 rudolfbyker

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.

kkroening avatar May 09 '18 08:05 kkroening

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

kkroening avatar May 09 '18 08:05 kkroening

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 ?

kodsama avatar May 27 '18 06:05 kodsama

what's the release plan about this?

alphabetaxz avatar Jul 10 '18 09:07 alphabetaxz

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

darkdragon-001 avatar Jul 22 '19 21:07 darkdragon-001

Updates on this?

muuvmuuv avatar Nov 28 '19 14:11 muuvmuuv

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

modbender avatar Sep 04 '20 12:09 modbender

Are there any updates on this?

dxing97 avatar Jul 27 '21 23:07 dxing97

The version of the example above from the examples folder uses unix sockets so was not usable for my windows use case.

Python is not my strongest language, having only been using it for a few months, and this solution is cobbled together from a couple of examples in this repo and some diving into stackoverflow, but this modification to that example seems to work. Rather than using a socket the progress stats are passed to stdout so this example does not allow for streaming the ffmpeg output:

#!/usr/bin/env python
import argparse
import ffmpeg
from queue import Queue
import sys
import textwrap
from threading import Thread
from tqdm import tqdm

def reader(pipe, queue):
    try:
        with pipe:
            for line in iter(pipe.readline, b''):
                queue.put((pipe, line))
    finally:
        queue.put(None)

parser = argparse.ArgumentParser(description=textwrap.dedent('''\
    Process video and report and show progress bar.

    This is an example of using the ffmpeg `-progress` option with
    stdout to report progress in the form of a progress bar.

    The video processing simply consists of converting the video to
    sepia colors, but the same pattern can be applied to other use
    cases.
'''))

parser.add_argument('in_filename', help='Input filename')
parser.add_argument('out_filename', help='Output filename')

if __name__ == '__main__':
    args = parser.parse_args()
    total_duration = float(ffmpeg.probe(args.in_filename)['format']['duration'])
    error = ""

    # See https://ffmpeg.org/ffmpeg-filters.html#Examples-44
    sepia_values = [.393, .769, .189, 0, .349, .686, .168, 0, .272, .534, .131]
    try:
        sepia = (
            ffmpeg
            .input(args.in_filename)
            .colorchannelmixer(*sepia_values)
            .output(args.out_filename)
            .global_args('-progress', 'pipe:1')
            .overwrite_output()
            .run_async(pipe_stdout=True, pipe_stderr=True)
        )
        q = Queue()
        Thread(target=reader, args=[sepia.stdout, q]).start()
        Thread(target=reader, args=[sepia.stderr, q]).start()
        bar = tqdm(total=round(total_duration, 2))
        for _ in range(2):
            for source, line in iter(q.get, None):
                line = line.decode()
                if source == sepia.stderr:
                    error.append(line)
                else:
                    line = line.rstrip()
                    parts = line.split('=')
                    key = parts[0] if len(parts) > 0 else None
                    value = parts[1] if len(parts) > 1 else None
                    if key == 'out_time_ms':
                        time = max(round(float(value) / 1000000., 2), 0)
                        bar.update(time - bar.n)
                    elif key == 'progress' and value == 'end':
                        bar.update(bar.total - bar.n)
        bar.close()

    except ffmpeg.Error as e:
        print(error, file=sys.stderr)
        sys.exit(1)

The threading and queue is a way to avoid ffmpeg filling output buffers and hanging. The output from stdout and stderr is added to the same queue along with a stream identifier which is then read in the main thread until a value of None is encountered (one for each stream, hence the range(2)). The stream identifier is used to buffer the stderr stream again in case of error and process the stdout stream for progress updates. If you are not going to use the stderr output except if there is a problem you could add , '-loglevel', 'error' to the global_args so ffmpeg only actually outputs to it when there is an issue.

ffmpeg can occasionally report a negative time when it starts processing a file depending on the input file which is why the progress duration calculation is wrapped in a max(<calc>,0) statement.

For some reason I don't understand using the context manager form of tqdm could cause the ffmpeg process not to exit so I manually create and close it which, in my testing, does not suffer from the same behaviour.

Gulski avatar Sep 22 '21 10:09 Gulski

Maybe, I think displaying ffmpeg stderr in cli friendly fmt is not main responsibility of this wrapper. So, I don't think it necessarily needs to be implemented. However, it is certainly a useful feature.

sota0121 avatar Jun 28 '22 11:06 sota0121

This is how I solved it:

import tempfile
import threading
import time



class ProgressFfmpeg(threading.Thread):
    def __init__(self, vid_duration_seconds, progress_update_callback):
        threading.Thread.__init__(self, name='ProgressFfmpeg')
        self.stop_event = threading.Event()
        self.output_file = tempfile.NamedTemporaryFile(mode='w+', delete=False)
        self.vid_duration_seconds = vid_duration_seconds
        self.progress_update_callback = progress_update_callback

    def run(self):

        while not self.stop_event.is_set():
            latest_progress = self.get_latest_ms_progress()
            if latest_progress is not None:
                completed_percent = latest_progress / self.vid_duration_seconds
                self.progress_update_callback(completed_percent)
            time.sleep(1)

    def get_latest_ms_progress(self):
        lines = self.output_file.readlines()

        if lines:
            for line in lines:
                if 'out_time_ms' in line:
                    out_time_ms = line.split('=')[1]
                    return int(out_time_ms) / 1000000.0
        return None

    def stop(self):
        self.stop_event.set()

    def __enter__(self):
        self.start()
        return self

    def __exit__(self, *args, **kwargs):
        self.stop()

To use it:


def on_update_example(progress):
    print(progress)


with ProgressFfmpeg(total_duration_seconds, on_update_example) as progress:
    cmd.output(output_path, **output_args).global_args('-progress', progress.output_file.name).run()

TomRaz avatar Dec 04 '22 10:12 TomRaz

I was able to solve this issue (only the progress bar) with the **kwargs by passing:

arg_dict = {"loglevel": "quiet", "stats": None}

to

stream = ffmpeg.output(stream, output_path, **arg_dict)

The end result is the progress bar and only the progress bar, and it does get updated in real time, i.e.:

frame= 1728 fps=101 q=31.0 size=    4096kB time=00:00:57.87 bitrate= 579.8kbits/s speed=3.37x

Project reference: https://github.com/Aperocky/batch-compress-video-cli/blob/master/src/video_compress.py#L115-L134

Aperocky avatar Jan 19 '23 06:01 Aperocky

I stumbled upon this type of issue a couple of months ago and it's similar to how @Gulski did it. It's perfectly fine to input any type of pipe here, even to a socket, so one can stream over network if needed.

f = ffmpeg.output(stream, output_path, progress="pipe:1")
a = f.run_async(pipe_stdout=True, pipe_stderr=True)
progress = dict()
while a.poll() is None:
    for std in select.select([p.stdout, p.stderr], [], [])[0]:
        if std.name == p.stdout.name:
            l = p.stdout.readline()
            m = re.match(r"(\w+)=(.*)", l.decode().rstrip())
            if m:
                progress[m.group(1)] = m.group(2)
            # Access "out_time" and similar values to do what needs to be done

McTwist avatar May 06 '23 09:05 McTwist