Skip to content

FFmpeg 错误处理:如何从一堆废话中找到重点

在使用 Python 的 subprocess 模块调用外部工具,尤其是 ffmpeg时,经常会碰到一个让人头疼的问题:一旦命令出错,抛出的 subprocess.CalledProcessError 异常会把标准错误输出(stderr)一股脑儿丢给你。这输出往往长得吓人,里面混杂着版本号、编译信息、配置参数等等,而真正有用的错误线索,可能就那么一两行,藏在这一大堆信息里找都找不着。

问题:FFmpeg 报错,日志全是“废话”

举个例子,假设你想用 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 出错了!\n命令: {' '.join(cmd)}\n错误输出:\n{e.stderr}")

运行这段代码后,e.stderr 可能会吐出一大堆东西:FFmpeg 的版本信息、支持的编码器列表……翻到最后,你才能看到一句简单的 no_such_file.mp4: No such file or directory。如果这是在生产环境,或者一个复杂的任务流程里,面对这么长的日志,想快速搞清楚问题在哪儿,简直是噩梦。

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

我们需要一个办法,把真正关键的错误信息揪出来,别让它淹没在“废话”里。

解决办法:聪明地提取关键信息

直接把整个 e.stderr 打印出来肯定不行,太乱了。更好的思路是:从这一堆输出中,挑出最能说明问题的那几句话。

观察一下 ffmpeg 的错误信息,通常有几个规律:

  1. 关键信息往往在最后几行,比如文件找不到、格式不支持这类提示。
  2. 会带有一些明显的关键词,比如 "Error"、"Invalid"、"No such file"、"Permission denied" 等等。

根据这些特点,我们可以写一个函数,专门从 stderr 里挖出有用的部分:

python
def extract_concise_error(stderr_text: str, max_lines=3, max_length=250) -> str:
    """从 stderr 里挑出简洁的错误信息,通常是最后几行带关键词的。"""
    if not stderr_text:
        return "未知错误(stderr 是空的)"

    # 把 stderr 按行拆开
    lines = stderr_text.strip().splitlines()
    if not lines:
        return "未知错误(stderr 没内容)"

    # 常见的错误关键词
    error_keywords = ["error", "invalid", "fail", "could not", "no such",
                      "denied", "unsupported", "unable", "can't open", "conversion failed"]

    # 只看最后几行(默认最多 3 行)
    start = max(0, len(lines) - max_lines)
    for i in range(len(lines) - 1, start - 1, -1):  # 从后往前找
        line = lines[i].strip()
        if not line:  # 空行跳过
            continue

        # 如果这行里有关键词,基本就是我们要找的
        if any(keyword in line.lower() for keyword in error_keywords):
            # 再带上上一行做上下文,可能更有用
            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 "")

    # 如果没找到关键词,就拿最后一行凑合
    for line in reversed(lines):
        if line.strip():
            return line[:max_length] + ("..." if len(line) > max_length else "")
    
    return "未知错误(没找到具体问题)"

# 用的时候这样写:
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 出错啦(退出码: {e.returncode})!命令: {' '.join(cmd)},错误: {short_error}")
    # 如果需要完整输出,可以用 DEBUG 级别记下来
    # logger.debug(f"完整错误输出:\n{e.stderr}")

写这个函数时的一些想法和坑

  1. 关键词不一定全
    我列的 error_keywords 是凭经验来的,可能漏掉一些 ffmpeg 的特殊错误提示。实际用的时候,遇到新情况可能得加几个关键词进去。

  2. 上下文很重要
    有时候光看错误那一行还不明白,比如“文件打不开”,得看前一行才知道是哪个文件。所以我加了点代码,尽量把上一行也带上。

  3. 找不到关键词咋办
    如果没匹配到关键词,我就退而求其次,拿最后一行当结果。总比把整页日志甩出来强吧,但也不一定每次都准。

  4. 字符编码的麻烦
    ffmpeg 的输出有时候会冒出奇怪的字符,不是标准的 UTF-8。为了避免程序崩掉,subprocess.run 里加了 encoding="utf-8",必要时可以用 errors="replace" 来兜底。

  5. 日志怎么记才好
    我的做法是,把简短的错误信息记在 ERROR 级别,方便一眼看出问题;如果需要查细节,再把完整输出记到 DEBUG 级别。这样既清楚又不丢信息。

用这个办法,我们就能从 subprocess.CalledProcessErrorstderr 里快速捞出关键信息,让日志变得好读多了,排查问题也更快。这个思路不仅能用在 ffmpeg 上,其他输出又臭又长的命令行工具也能照着用。

核心就是摸清工具的错误输出有什么规律,然后动手把它“精简”出来。

虽然不保证每次都完美,但至少能帮你少翻几页日志,少挠几下头。