Parse FFmpeg Progress Using Python Subprocess

Tracking the progress of an FFmpeg operation in Python requires launching the process via subprocess.Popen and reading its output stream in real time. Rather than attempting to parse the erratic, carriage-return-delimited output FFmpeg prints to stderr by default, the most reliable approach is to use FFmpeg's -progress pipe:1 flag. This directs FFmpeg to write machine-readable, key-value pairs directly to standard output (stdout), allowing your Python script to easily capture frames, execution time, and completion status.

The Strategy: Using Machine-Readable Progress

FFmpeg includes a dedicated -progress flag that outputs periodic key-value metrics:

  • frame: Current frame processed.
  • fps: Current processing speed in frames per second.
  • total_size: Size of the output file so far in bytes.
  • out_time_us: Processing position in microseconds.
  • progress: Set to continue during processing, and end when finished.

Directing this data to pipe:1 routes it to stdout, keeping it separate from standard log messages (which go to stderr).

Python Implementation

The following script executes FFmpeg to convert a video and reads the structured output line-by-line:

import subprocess
import sys

def run_ffmpeg_with_progress(input_path: str, output_path: str):
    # Construct the command with the -progress flag sending data to stdout (pipe:1)
    cmd = [
        "ffmpeg",
        "-y",
        "-i", input_path,
        "-progress", "pipe:1",
        "-nostats",
        output_path
    ]

    # Launch FFmpeg
    process = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        bufsize=1
    )

    progress_data = {}

    # Read stdout line by line as it is generated
    for line in process.stdout:
        line = line.strip()
        if not line:
            continue
        
        # FFmpeg outputs lines in key=value format
        if "=" in line:
            key, value = line.split("=", 1)
            progress_data[key.strip()] = value.strip()

        # The 'progress' key indicates the end of a single progress update block
        if line.startswith("progress="):
            frame = progress_data.get("frame", "N/A")
            fps = progress_data.get("fps", "N/A")
            out_time = progress_data.get("out_time", "N/A")
            status = progress_data.get("progress")

            sys.stdout.write(f"\rFrame: {frame} | FPS: {fps} | Time: {out_time}")
            sys.stdout.flush()

            if status == "end":
                print("\nTranscoding complete.")
                break

    # Wait for the process to exit cleanly
    process.wait()

    if process.returncode != 0:
        error_output = process.stderr.read()
        raise RuntimeError(f"FFmpeg error:\n{error_output}")

if __name__ == "__main__":
    run_ffmpeg_with_progress("input.mp4", "output.mkv")

Calculating Completion Percentage

Because FFmpeg does not automatically report progress as a percentage, you must provide the source file's total duration to calculate it.

  1. Retrieve Total Duration: Use ffprobe before transcoding to obtain the file's duration in seconds:
def get_duration(input_path: str) -> float:
    cmd = [
        "ffprobe",
        "-v", "error",
        "-show_entries", "format=duration",
        "-of", "default=noprint_wrappers=1:nokey=1",
        input_path
    ]
    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
    return float(result.stdout.strip())
  1. Compute Percentage: Inside the progress loop, read out_time_us (microseconds), convert it to seconds, and divide by the total duration:
out_time_us = int(progress_data.get("out_time_us", 0))
current_seconds = out_time_us / 1_000_000
percent = min(100.0, (current_seconds / total_duration) * 100)
sys.stdout.write(f"\rProgress: {percent:.2f}%")

Key Considerations

  • Buffering: Setting bufsize=1 ensures line-buffering is enabled, preventing delays between FFmpeg generating an update and Python processing it.
  • Deadlocks: When reading stdout, ensure stderr does not fill the operating system's pipe buffer. If you do not need FFmpeg's logs, route stderr=subprocess.DEVNULL. If you need error logs, consume them asynchronously or only read them after stdout closes if the process fails.