后端脚本里一旦碰到转码、抽音频、压缩视频或批量处理素材,FFmpeg 基本都是绕不过去的工具。但在 Python 里“能调起来”和“能稳定跑生产任务”是两回事:是否会阻塞、能不能拿到错误信息、长任务有没有超时和进度,决定了脚本到底好不好用。下面就按几类最常见场景拆开讲清楚,并把容易踩坑的细节一起补齐。
一次性任务怎么调用最省事
如果只是做一个简单的音视频处理任务,最直接的办法就是用 subprocess 调 FFmpeg。原文里先给了 subprocess.call 的写法,这种方式的优点就是足够直白:
import subprocess
# 提取音频
subprocess.call([
'ffmpeg', '-i', 'input.mp4',
'-vn', '-c:a', 'mp3', 'output.mp3'
])
call 会一直阻塞到命令执行结束,然后返回退出码。对于很短的小任务,这样写没有问题;但只要输入文件变大,比如压缩一个 2GB 视频,主线程就会一直等着,界面程序会卡住,服务端任务也不方便做更细的控制。
因此在 Python 3.5+ 环境里,更实用的选择通常是 subprocess.run:
import subprocess
result = subprocess.run(
['ffmpeg', '-i', 'input.mp4', '-vf', 'scale=1280:720', 'output.mp4'],
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
print('转换成功')
else:
print('转换失败:', result.stderr)
这段代码相比 call 的差别,主要有三个:
为什么更推荐 subprocess.run
第一,它返回的是 CompletedProcess 对象,不只是一个退出码。你可以直接读取 returncode、stdout 和 stderr,这对记录日志和排查失败原因更有帮助。

第二,capture_output=True 可以把标准输出和标准错误收集回来。FFmpeg 出错时,很多有效信息都在错误输出里,不抓下来就很难判断是参数问题、编码器问题,还是输入文件本身损坏。
第三,timeout=300 这个参数非常关键。很多脚本不是“转码失败”,而是“进程一直挂着不退出”。一旦 FFmpeg 因为输入文件损坏、读取异常或某些环境问题卡住,Python 进程也会跟着一直等待。给长任务设一个合理超时时间,通常比补一堆事后告警更有用。
长任务如何实时拿到 FFmpeg 进度
当处理的是大文件,或者你需要给用户显示进度条时,只拿最终退出码就不够了。FFmpeg 在运行过程中会持续输出状态信息,常见格式类似这样:
frame= 120 fps=30.0 time=00:00:04.00 bitrate=128.0kbits/s speed=1.0x
如果要实时读取这些信息,就要改用 subprocess.Popen,边跑边读输出。原文给出的方案是把 FFmpeg 的进度信息导到管道里,再由 Python 逐行解析:
import subprocess
import re
def convert_with_progress(input_path, output_path):
cmd = [
'ffmpeg', '-i', input_path,
'-c:v', 'libx264', '-preset', 'medium',
'-progress', 'pipe:1', # 进度信息输出到 stdout
output_path
]
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, bufsize=1
)
for line in process.stdout:
match = re.search(r'out_time_us=(d+)', line)
if match:
us = int(match.group(1))
seconds = us / 1_000_000
print(f'已处理: {seconds:.1f} 秒', end='r')
process.wait()
if process.returncode == 0:
print('n处理完成')
else:
error = process.stderr.read()
print(f'n处理失败: {error[:200]}...')
convert_with_progress('large_video.mp4', 'compressed.mp4')
这里的关键点不在于“用了 Popen”,而在于输出通道怎么设计。
-progress pipe:1 有什么作用
默认情况下,FFmpeg 的不少运行信息会打到 stderr。如果你只是简单读错误输出,往往会把真正的报错和进度文本混在一起,处理起来比较麻烦。加上 -progress pipe:1 后,FFmpeg 会把结构化的进度信息发到 stdout,这样 Python 端就能更明确地逐行消费。

为什么要设 bufsize=1
这个参数决定了按行缓冲读取。对实时进度场景来说,它能避免程序一直等到缓冲区攒满才读到内容,否则“进度更新”就会变成几秒一跳,失去实时意义。
解析进度时要注意版本差异
原文特别提到,不同版本 FFmpeg 的进度输出格式可能有细微差异。也就是说,正则不是写一次就永远稳定。比如文中建议先匹配新版常见字段,再回退到旧格式:
match = re.search(r'out_time_us=(d+)', line)
if not match:
match = re.search(r'time=(d+):(d+):(d+).(d+)', line)
这类“双层匹配”很有必要。否则你在本机测试通过,换一台 FFmpeg 版本不同的机器,进度条就可能直接失效。
批量处理文件时,为什么建议配合 Pathlib
单个文件处理和批量处理是两回事。前者关注命令能不能跑通,后者更在意目录遍历、输出文件命名、覆盖策略和路径兼容性。原文这部分给出的思路是:用 pathlib 管文件路径,用 FFmpeg 负责实际转换。

from pathlib import Path
import subprocess
def batch_convert_to_mp3(input_dir: str, output_dir: str, bitrate='192k'):
src = Path(input_dir)
dst = Path(output_dir)
dst.mkdir(parents=True, exist_ok=True)
files = list(src.glob('*.*'))
for i, file in enumerate(files, 1):
if file.suffix.lower() not in ('.mp4', '.mkv', '.mov', '.a vi'):
continue
output = dst / f'{file.stem}.mp3'
print(f'[{i}/{len(files)}] {file.name}')
subprocess.run([
'ffmpeg', '-y', '-i', str(file),
'-vn', '-c:a', 'libmp3lame',
'-b:a', bitrate,
str(output)
], capture_output=True)
batch_convert_to_mp3('./videos', './audios')
这段代码适合拿来做批量抽音频的基础模板,主要解决了几个常见问题。
目录和输出文件如何组织
Path(input_dir) 和 Path(output_dir) 让路径处理更统一,dst.mkdir(parents=True, exist_ok=True) 则保证输出目录不存在时也能自动创建。相比手写一堆 os.path 拼接逻辑,这种方式更直观,也更不容易在跨平台时出错。
为什么批量任务里几乎都要加 -y
原文明确指出,-y 用来自动覆盖已有文件。这个参数在批量任务里非常实用,因为一旦 FFmpeg 询问“是否覆盖”,脚本就会卡在交互确认上。对于无人值守脚本、定时任务或后台队列,这类卡住往往比转换失败更难发现。
筛选文件时要留意扩展名细节
示例里通过 file.suffix.lower() 判断扩展名,只处理 .mp4、.mkv、.mov、.a vi 等格式。这里的核心思路是对输入类型做白名单控制,避免把无关文件也喂给 FFmpeg。实际落地时,这个列表应该按你的业务场景收紧或补全,减少无效处理和报错。
上线前最好先处理的 4 类常见错误
从经验上看,Python 调 FFmpeg 的问题,很多并不出在转码参数本身,而是出在环境和文件状态。原文列了 4 个很常见的坑,这部分在实际项目里往往比命令本身更值得优先补齐。
1. FFmpeg 不在 PATH 里
这在 Windows 上尤其常见。脚本在你电脑上能跑,换到同事机器或服务器上就直接报找不到命令。最简单的处理方式,就是在脚本启动时先做一次检查:
import shutil
ffmpeg_path = shutil.which('ffmpeg')
if not ffmpeg_path:
raise RuntimeError('未找到 FFmpeg,请确认已安装并加入系统 PATH')
这样至少能在任务真正开始前给出明确报错,而不是等到后面调用失败再去追原因。
2. 输入文件被占用
在 Windows 环境里,如果视频文件正在被播放器或其他程序占用,FFmpeg 很可能会返回 Permission denied。这种报错看起来像权限问题,实际上常常是文件锁。原文建议在操作前先做一次只读打开测试:
def is_file_locked(filepath):
try:
with open(filepath, 'rb') as f:
return False
except PermissionError:
return True
这个检查不复杂,但能提前筛掉一批“本来命令没问题、只是文件状态不对”的失败任务。
3. 中文路径导致异常
如果运行环境是 Windows,路径里又包含中文字符,编码问题会比 Linux 或 macOS 更容易暴露。原文给出的经验是,尽量使用 Path 对象,并在传给 FFmpeg 前通过 resolve() 获取绝对路径。这样做的目的,是尽量减少路径编码和相对路径解析带来的不确定性。
4. 进度解析失败
这类问题通常不是 FFmpeg 没工作,而是你用来解析日志的规则太死。不同版本输出字段不完全一致,所以不要把单一格式写死。最稳妥的做法,就是像前面那样准备回退匹配逻辑,先抓 out_time_us,抓不到再尝试 time=... 形式。
实际项目里该怎么选调用方式
把前面的场景合起来看,Python 调 FFmpeg 的核心模式其实很明确:
- 一次性任务,优先用
subprocess.run。 - 需要实时展示处理进度,用
subprocess.Popen配合管道读取。 - 批量处理目录文件时,用
pathlib管路径,再把 FFmpeg 命令嵌进去。
而真正决定稳定性的,往往不是命令能否执行,而是有没有补上超时、覆盖、路径兼容、文件锁检查和进度回退解析这些边角。只要这些环节提前处理好,Python 调 FFmpeg 这件事并不复杂,代码也能保持足够轻量。







