ffmpeg-python
ffmpeg-python copied to clipboard
Ability to track progress of an ffmpeg command
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?
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).
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.
Are there any updates on this?
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.
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.
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()
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
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