Skip to content

FFmpeg Error Handling: How to Find the Key Points in a Pile of Verbose Output

When using Python's subprocess module to call external tools, especially ffmpeg, a common headache arises: when a command fails, the thrown subprocess.CalledProcessError exception dumps the entire standard error output (stderr) at you. This output is often intimidatingly long, mixed with version numbers, compilation info, configuration parameters, and so on. The truly useful error clue, perhaps just one or two lines, is buried within this massive pile of information, making it hard to find.

The Problem: FFmpeg Reports an Error, but the Log is Full of "Noise"

For example, suppose you try to convert a non-existent file using ffmpeg:

python
import subprocess
import logging

logger = logging.getLogger("FFmpegRunner")

cmd = ["ffmpeg", "-hide_banner", "-i", "no_such_file.mp4", "output.mp4"]

try:
    subprocess.run(cmd, check=True, capture_output=True, text=True, encoding="utf-8")
except subprocess.CalledProcessError as e:
    logger.error(f"FFmpeg failed!\nCommand: {' '.join(cmd)}\nError output:\n{e.stderr}")

After running this code, e.stderr might spit out a huge amount of text: FFmpeg version info, a list of supported encoders... Scrolling to the very end, you might finally see a simple line like no_such_file.mp4: No such file or directory. If this is in a production environment or a complex task pipeline, facing such a long log and trying to quickly understand the problem is a nightmare.

bash
C:\Users\c1\Videos>ffmpeg -c:v h264_amf -i 480.mp4  -c:v 152.mp4
ffmpeg version N-112170-gb61733f61f-20230924 Copyright (c) 2000-2023 the FFmpeg developers
  built with gcc 13.2.0 (crosstool-NG 1.25.0.232_c175b21)
  configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-w64-mingw32- --arch=x86_64 --target-os=mingw32 --enable-gpl --enable-version3 --disable-debug --disable-w32threads --enable-pthreads --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libharfbuzz --enable-libvorbis --enable-opencl --disable-libpulse --enable-libvmaf --disable-libxcb --disable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --enable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libvpl --enable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --enable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --disable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags=-DLIBTWOLAME_STATIC --extra-cxxflags= --extra-ldflags=-pthread --extra-ldexeflags= --extra-libs=-lgomp --extra-version=20230924
  libavutil      58. 25.100 / 58. 25.100
  libavcodec     60. 27.100 / 60. 27.100
  libavformat    60. 13.100 / 60. 13.100
  libavdevice    60.  2.101 / 60.  2.101
  libavfilter     9. 11.100 /  9. 11.100
  libswscale      7.  3.100 /  7.  3.100
  libswresample   4. 11.100 /  4. 11.100
  libpostproc    57.  2.100 / 57.  2.100
Trailing option(s) found in the command: may be ignored.
Unknown decoder 'h264_amf'
Error opening input file 480.mp4.
Error opening input files: Decoder not found

We need a way to extract the truly critical error information, preventing it from being drowned in the "noise".

The Solution: Intelligently Extracting Key Information

Simply printing the entire e.stderr is definitely not acceptable; it's too messy. A better approach is: from this pile of output, pick out the few lines that best explain the problem.

Observing ffmpeg error messages, there are usually a few patterns:

  1. Key information is often in the last few lines, such as prompts like file not found, format not supported.
  2. It often contains some obvious keywords, like "Error", "Invalid", "No such file", "Permission denied", etc.

Based on these characteristics, we can write a function specifically to dig out the useful parts from stderr:

python
def extract_concise_error(stderr_text: str, max_lines=3, max_length=250) -> str:
    """Extract concise error information from stderr, typically the last few lines containing keywords."""
    if not stderr_text:
        return "Unknown error (stderr is empty)"

    # Split stderr into lines
    lines = stderr_text.strip().splitlines()
    if not lines:
        return "Unknown error (stderr has no content)"

    # Common error keywords
    error_keywords = ["error", "invalid", "fail", "could not", "no such",
                      "denied", "unsupported", "unable", "can't open", "conversion failed"]

    # Only look at the last few lines (default max 3 lines)
    start = max(0, len(lines) - max_lines)
    for i in range(len(lines) - 1, start - 1, -1):  # Search from the end backwards
        line = lines[i].strip()
        if not line:  # Skip empty lines
            continue

        # If this line contains a keyword, it's likely what we're looking for
        if any(keyword in line.lower() for keyword in error_keywords):
            # Also include the previous line for context, which might be more helpful
            if i > 0 and lines[i-1].strip():
                return f"{lines[i-1].strip()}\n{line}"[:max_length] + ("..." if len(line) > max_length else "")
            return line[:max_length] + ("..." if len(line) > max_length else "")

    # If no keywords found, fall back to the last non-empty line
    for line in reversed(lines):
        if line.strip():
            return line[:max_length] + ("..." if len(line) > max_length else "")
    
    return "Unknown error (no specific issue found)"

# Use it like this:
try:
    subprocess.run(cmd, check=True, capture_output=True, text=True, encoding="utf-8")
except subprocess.CalledProcessError as e:
    short_error = extract_concise_error(e.stderr)
    logger.error(f"FFmpeg failed (exit code: {e.returncode})! Command: {' '.join(cmd)}, Error: {short_error}")
    # If full output is needed, log it at DEBUG level
    # logger.debug(f"Full error output:\n{e.stderr}")

Thoughts and Pitfalls When Writing This Function

  1. Keywords Might Not Be Exhaustive
    The error_keywords I listed are based on experience and might miss some specific ffmpeg error prompts. In practice, you might need to add a few keywords when encountering new situations.

  2. Context is Important
    Sometimes just the error line itself isn't clear, e.g., "cannot open file"; you need to see the previous line to know which file. That's why I added code to try and include the previous line.

  3. What If No Keywords Are Found?
    If no keywords match, I fall back to using the last non-empty line as the result. It's still better than dumping the entire log page, but it might not be accurate every time.

  4. The Hassle of Character Encoding
    ffmpeg output sometimes contains strange characters, not standard UTF-8. To prevent the program from crashing, encoding="utf-8" is added in subprocess.run. If necessary, errors="replace" can be used as a fallback.

  5. How to Log Effectively
    My approach is to log the concise error message at the ERROR level for easy identification of the problem. If details are needed for investigation, log the full output at the DEBUG level. This keeps things clear without losing information.

Using this method, we can quickly fish out the key information from the stderr of subprocess.CalledProcessError, making logs much more readable and speeding up troubleshooting. This approach isn't limited to ffmpeg; it can be adapted for other command-line tools with verbose and lengthy output.

The core idea is to understand the patterns of the tool's error output and then take action to "distill" it.

While it's not guaranteed to be perfect every time, it can at least help you flip through fewer pages of logs and scratch your head less often.