Skip to main content

Better FFmpeg Progress

Runs an FFmpeg command and shows a progress bar with percentage progress, elapsed time and ETA.

FFmpeg outputs something like:

frame=  692 fps= 58 q=28.0 size=    5376KiB time=00:00:28.77 bitrate=1530.3kbits/s speed=2.43x

Better FFmpeg Progress outputs something like:

⠏ Processing abc.webm (5.50 MB / ~23.91 MB) ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  23% 0:00:04 00:15

Where:

  • Processing abc.webm is the description of the progresss bar. Filenames longer than 60 characters are truncated.
  • (5.50 MB / ~23.91 MB) is the current output size and the estimated final output size.
  • 23% is the percentage progress.
  • 0:00:04 is the time elapsed.
  • 00:15 is the estimated time until the FFmpeg process completes.

Installation

pip install better-ffmpeg-progress --upgrade

Usage

Create an instance of the FfmpegProcess class and supply a list of arguments like you would when using subprocess.run() or subprocess.Popen(). Example:

from better_ffmpeg_progress import FfmpegProcess, FfmpegProcessError

command = [
    "ffmpeg",
    "-i",
    "https://media.xiph.org/video/derf/y4m/ducks_take_off_1080p50.y4m",
    "-map",
    "0:V",
    "-c:V",
    "libx264",
    "-preset",
    "ultrafast",
    "-f",
    "null",
    "-",
]

try:
    # Pass use_tqdm=True to use tqdm instead of Rich.
    process = FfmpegProcess(command)

    result = process.run()
    print(
        f"Done in {result.elapsed_secs:.1f} seconds. "
        f"Processed {result.processed_secs:.1f} seconds of media."
    )
except FfmpegProcessError as e:
    print(
        "An error occurred when running Better FFmpeg Progress:\n"
        f"{e}"
    )

run() returns an FfmpegResult describing the finished run. You do not have to use it: process.run() on its own works exactly as it did in earlier versions, so existing code does not need to change. If FFmpeg fails, run() raises an exception instead of returning, so a result is only returned when FFmpeg succeeded.

Trimming, looping and other options that change the duration

The expected duration of the output is calculated from the command, so the progress bar and ETA reflect the output rather than the whole input file. These options are taken into account:

  • -ss, -sseof, -t and -to, as input or output options. They are ignored when -copyts is used.
  • -stream_loop and -loop 1 (to repeat an image). An input that is looped indefinitely has an unknown duration, unless it is trimmed with e.g. -t.
  • -shortest: the output ends with the shortest input, e.g. ffmpeg -loop 1 -i cover.jpg -i song.mp3 -shortest video.mp4 takes the duration of song.mp3.
  • -frames:v (or -vframes), using the output's -r or else the frame rate of the first input.

Duration Override

By default, Better FFmpeg Progress detects the duration of the input file and uses it to calculate percentage progress and ETA.

For partial, damaged, streamed or synthetic inputs, FFmpeg may be unable to detect the duration correctly. In these cases, you can provide the duration manually using duration_override.

The value should be the duration of the input file, in seconds:

from better_ffmpeg_progress import FfmpegProcess

command = [
    "ffmpeg",
    "-i",
    "partial-input.flv",
    "-c",
    "copy",
    "recovered-output.mkv",
]

process = FfmpegProcess(
    command,
    duration_override=1222.64,
    print_detected_duration=True,
)

result = process.run()
print(f"Recovered {result.output_size_mb:.1f} MB")

Generated (lavfi) Inputs

Inputs generated by FFmpeg's lavfi device, such as test patterns, are supported. FFprobe cannot detect the duration of these inputs, so provide it with duration_override:

from better_ffmpeg_progress import FfmpegProcess

command = [
    "ffmpeg",
    "-f",
    "lavfi",
    "-i",
    "testsrc2=duration=30:size=1920x1080:rate=30",
    "-c:v",
    "libx264",
    "output.mp4",
]

process = FfmpegProcess(command, duration_override=30)
result = process.run()
print(f"Encoded {result.processed_secs:.0f} seconds in {result.elapsed_secs:.1f} seconds")

By default, the FFmpeg log file is named after the source, e.g. lavfi_testsrc2_ffmpeg_log.txt.

Optional Arguments

An instance of FfmpegProcess accepts the following optional arguments:

  • ffmpeg_log_level — Set a value for FFmpeg's -loglevel option. Default: "verbose".
  • ffmpeg_log_file — Filepath or file-like object to which FFmpeg stderr will be written. Default: <input filename>_ffmpeg_log.txt.
  • print_detected_duration — Print the detected duration. Default: False.
  • duration_override — Specify input file duration in seconds to use for progress calculation, instead of trying to detect this automatically with FFprobe. Default: None.
  • use_tqdm — Use tqdm instead of Rich for the progress bar. Default: False.
  • overwrite — What to do if the output file already exists: True overwrites it (and any other existing output), False raises FfmpegCommandError, and None asks whether to overwrite it. Ignored if the command contains -y or -n. Writing to the null device (e.g. /dev/null) never asks. Default: None.
  • quiet — Do not show a progress bar or status messages. Default: False.
  • progress_callback — A function to call each time FFmpeg reports its progress. See Progress callback. Default: None.
  • delete_log_on_success — Delete the FFmpeg log file if FFmpeg succeeds. Default: False.
  • on_progress — Like progress_callback, but called with an FfmpegProgress that also has the speed, frames per second and more. See Progress callback. Default: None.

Creating an FfmpegProcess only validates the command. FFprobe is run, and you are asked whether to overwrite an existing output file, when run() is called.

The run method accepts the following optional argument:

  • print_command — Print the FFmpeg command being executed. Default: False.

The result of run()

If FFmpeg succeeds, run() returns an FfmpegResult. Capturing it is optional, so process.run() without assigning the result is fine. If FFmpeg fails, an exception is raised instead (see Errors). The result has these attributes:

  • elapsed_secs — How long FFmpeg ran for, in seconds.
  • processed_secs — Seconds of the output processed, as last reported by FFmpeg.
  • duration_secs — The expected output duration used for the progress bar, or None if it was unknown.
  • output_size_mb — The size of the output in MB, as last reported by FFmpeg.
  • log_file — The path or file object that FFmpeg's log was written to.
result = FfmpegProcess(command).run()
print(f"Took {result.elapsed_secs:.1f} seconds, output is {result.output_size_mb:.1f} MB")
print(f"Speed: {result.processed_secs / result.elapsed_secs:.1f}x real time")

Using tqdm

By default, the Rich library is used to display a progress bar. tqdm can be used instead by passing use_tqdm=True. tqdm is an optional dependency, installed with:

pip install "better-ffmpeg-progress[tqdm]" --upgrade
process = FfmpegProcess(command, use_tqdm=True)
result = process.run()
print(f"Finished in {result.elapsed_secs:.1f} seconds")

Progress callback

To show progress in your own UI (e.g. a GUI or a web app), pass a function as progress_callback. It is called with the number of seconds processed, the expected output duration in seconds (None if it is unknown) and the current output size in MB. Combine it with quiet=True to hide the progress bar:

def on_progress(secs: float, duration: float | None, size_mb: float) -> None:
    if duration:
        print(f"{secs / duration:.0%}")

FfmpegProcess(command, quiet=True, progress_callback=on_progress).run()

If the callback raises an exception, FFmpeg is stopped and the exception is propagated by run().

For more detail, pass a function as on_progress instead. It is called with an FfmpegProgress, which has these attributes: secs, duration_secs, size_mb, percentage (None if the duration is unknown), speed (e.g. 2.5 for 2.5x real time), fps, frame and bitrate_kbps. Values that FFmpeg has not reported (yet) are None.

from better_ffmpeg_progress import FfmpegProcess, FfmpegProgress

def on_progress(progress: FfmpegProgress) -> None:
    if progress.percentage is not None:
        print(f"{progress.percentage:.0f}% at {progress.speed}x")

FfmpegProcess(command, quiet=True, on_progress=on_progress).run()

Errors

All exceptions raised by the package are subclasses of FfmpegProcessError, which has these attributes:

  • returncode — FFmpeg's exit code, or None if FFmpeg was not started or had not exited.
  • log_file — The path or file object that FFmpeg's log was written to.

Pressing Ctrl+C raises FfmpegProcessInterruptedError. FFmpeg is first asked to stop the way pressing q does, so it can finish writing the output file, e.g. an MP4 file stays playable. If it does not stop within 5 seconds, it is terminated.

FFmpeg runs in its own process group, so it is not stopped by signals sent to your program's group. On Linux, it is sent SIGTERM if your program exits without stopping it, e.g. if it is killed with SIGKILL. On macOS and Windows, FFmpeg keeps running in that case.

Logging

Warnings, such as when the input duration cannot be detected, are logged with the logging module using the better_ffmpeg_progress logger. Without any logging configuration, Python prints them to standard error. To hide them:

import logging

logging.getLogger("better_ffmpeg_progress").setLevel(logging.ERROR)

Writing to standard output

On Linux and macOS, the output can be written to standard output, e.g. -f mpegts -, to pipe it to another program. The progress bar is then shown on standard error. This is not supported on Windows, where FFmpeg's progress is read from its standard output.

Duration detection

FFprobe is looked for next to the FFmpeg executable first (e.g. /opt/ffmpeg/bin/ffprobe for /opt/ffmpeg/bin/ffmpeg), then in PATH. If it does not finish within 30 seconds, e.g. for an unresponsive URL, the duration is treated as unknown.

Input options that change how the input is read (-f, -framerate, -pattern_type, -start_number, -safe and -protocol_whitelist) are passed to FFprobe, so e.g. the duration of an image sequence with -framerate is correct.

For the concat demuxer (-f concat -i list.txt), FFprobe cannot detect the total duration, so the list file is read and the durations of its files are added up, taking inpoint, outpoint and duration into account.

Limitations

  • Progress is calculated from the duration of the first input (the first -i), or the shortest input with -shortest.
  • The last argument of the command is treated as the output file, e.g. when checking whether it already exists. With multiple outputs, only the last one is checked before FFmpeg runs. If another output already exists, FFmpeg does not run and FfmpegCommandError is raised, unless overwrite=True or -y is used.

Development

Install the package in editable mode with the development dependencies, then run the tests (FFmpeg and FFprobe must be installed):

pip install -e ".[dev]"
pytest

Lint, format and type-check the code like CI does:

ruff check .
ruff format --check better_ffmpeg_progress tests test.py
mypy

test.py is a standalone example that encodes a test pattern and shows the progress bar.

Publishing

Update __version__ in better_ffmpeg_progress/__init__.py and add an entry to CHANGELOG.md.

One-time setup:

python -m pip install --upgrade build twine pkginfo

Then, for each release:

rm -rf dist
python -m build
python -m twine check dist/*
python -m twine upload dist/*

Metadata

Release files for better-ffmpeg-progress 7.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for better-ffmpeg-progress 7.1.0
File Size Uploaded Provenance Actions
better_ffmpeg_progress-7.1.0.tar.gz 34.3 kB Without provenance Details for better_ffmpeg_progress-7.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for better-ffmpeg-progress 7.1.0
File Interpreter ABI Platform Actions
better_ffmpeg_progress-7.1.0-py3-none-any.whl Python 3 none any Details for better_ffmpeg_progress-7.1.0-py3-none-any.whl

Total release size: 61.0 kB

Release files / better_ffmpeg_progress-7.1.0.tar.gz

Download URL better_ffmpeg_progress-7.1.0.tar.gz
Size 34.3 kB
Tags Source
SHA-256 checksum
How to use checksums
01d3ca722c2ad555cb56bd2f4c15847c04fcdcbef956f08761e6621e42918c21
BLAKE2b-256 checksum
How to use checksums
d7a03305bee080ad8644889972945ecd57825478b9b2cf10c9086865dbae03ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.6

Release files / better_ffmpeg_progress-7.1.0-py3-none-any.whl

Download URL better_ffmpeg_progress-7.1.0-py3-none-any.whl
Size 26.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
71077aab66fef266f32647ca643a5186de764cb7fdd2f9ceab6fc2f7048075a7
BLAKE2b-256 checksum
How to use checksums
e57835ab90653aff596fa4da4cf650849bd2b9d96d87d7a0b7eaad971daf4f01
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.6

Security for better-ffmpeg-progress 7.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

7.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page