Skip to content

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

When using Python's subprocess module to call external tools, especially ffmpeg, a common headache arises: if the command fails, the subprocess.CalledProcessError exception dumps the entire standard error output (stderr) at you. This output is often intimidatingly long, cluttered with version numbers, compilation info, configuration parameters, and more, while the truly useful error clues—maybe just one or two lines—are buried deep within.

Problem: FFmpeg Errors, Logs Full of "Noise"

For example, suppose you try to convert a non-existent file with 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, supported encoder lists… and only at the very end will you see a simple line like no_such_file.mp4: No such file or directory. If this happens in a production environment or a complex task flow, quickly identifying the issue from such lengthy logs can be 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 without letting it drown in the "noise."

Solution: Intelligently Extract Key Information

Simply printing the entire e.stderr is not ideal—it's too messy. A better approach is to pick out the few lines that best explain the problem from the pile of output.

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

  1. Key information is often in the last few lines, such as "file not found" or "format not supported."
  2. They often contain 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 info from stderr, usually the last few lines with 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 (no content in stderr)"

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

    # Only check 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):
            # Include the previous line for context if possible
            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)"

# Usage example:
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}")
    # For full output, log at DEBUG level
    # logger.debug(f"Full error output:\n{e.stderr}")

Thoughts and Pitfalls When Writing This Function

  1. Keywords May Not Be Exhaustive
    The error_keywords list is based on experience and might miss some specific ffmpeg error messages. In practice, you may need to add more keywords as new cases arise.

  2. Context Matters
    Sometimes the error line alone isn't clear—for example, "file cannot be opened" might require the previous line to identify which file. That's why the code tries to include the previous line for context.

  3. What If No Keywords Are Found?
    If no keywords match, the function falls back to the last non-empty line. It's not perfect, but it's better than dumping the entire log.

  4. Character Encoding Hassles
    ffmpeg output can sometimes contain unexpected characters that aren't standard UTF-8. To prevent crashes, encoding="utf-8" is used in subprocess.run, and errors="replace" can be added as a fallback if needed.

  5. How to Log Effectively
    The recommended approach is to log the concise error at ERROR level for quick visibility and the full output at DEBUG level for detailed investigation. This keeps logs clear without losing information.

Using this method, we can quickly extract key information from subprocess.CalledProcessError's stderr, making logs much more readable and speeding up troubleshooting. This idea isn't limited to ffmpeg—it can be applied to other command-line tools with verbose outputs.

The core is to understand the patterns in the tool's error output and then "distill" it down.

While it may not be perfect every time, it can at least save you from scrolling through pages of logs and scratching your head too much.