FFmpeg 错误处理:如何从一堆废话中找到重点
在使用 Python 的 subprocess
模块调用外部工具,尤其是 ffmpeg
时,经常会碰到一个让人头疼的问题:一旦命令出错,抛出的 subprocess.CalledProcessError
异常会把标准错误输出(stderr)一股脑儿丢给你。这输出往往长得吓人,里面混杂着版本号、编译信息、配置参数等等,而真正有用的错误线索,可能就那么一两行,藏在这一大堆信息里找都找不着。
问题:FFmpeg 报错,日志全是“废话”
举个例子,假设你想用 ffmpeg
转换一个压根儿不存在的文件:
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
。如果这是在生产环境,或者一个复杂的任务流程里,面对这么长的日志,想快速搞清楚问题在哪儿,简直是噩梦。
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
的错误信息,通常有几个规律:
- 关键信息往往在最后几行,比如文件找不到、格式不支持这类提示。
- 会带有一些明显的关键词,比如 "Error"、"Invalid"、"No such file"、"Permission denied" 等等。
根据这些特点,我们可以写一个函数,专门从 stderr
里挖出有用的部分:
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}")
写这个函数时的一些想法和坑
关键词不一定全
我列的error_keywords
是凭经验来的,可能漏掉一些ffmpeg
的特殊错误提示。实际用的时候,遇到新情况可能得加几个关键词进去。上下文很重要
有时候光看错误那一行还不明白,比如“文件打不开”,得看前一行才知道是哪个文件。所以我加了点代码,尽量把上一行也带上。找不到关键词咋办
如果没匹配到关键词,我就退而求其次,拿最后一行当结果。总比把整页日志甩出来强吧,但也不一定每次都准。字符编码的麻烦
ffmpeg
的输出有时候会冒出奇怪的字符,不是标准的 UTF-8。为了避免程序崩掉,subprocess.run
里加了encoding="utf-8"
,必要时可以用errors="replace"
来兜底。日志怎么记才好
我的做法是,把简短的错误信息记在ERROR
级别,方便一眼看出问题;如果需要查细节,再把完整输出记到DEBUG
级别。这样既清楚又不丢信息。
用这个办法,我们就能从 subprocess.CalledProcessError
的 stderr
里快速捞出关键信息,让日志变得好读多了,排查问题也更快。这个思路不仅能用在 ffmpeg
上,其他输出又臭又长的命令行工具也能照着用。
核心就是摸清工具的错误输出有什么规律,然后动手把它“精简”出来。
虽然不保证每次都完美,但至少能帮你少翻几页日志,少挠几下头。