本讲目标
-
掌握 Python 调用 FFmpeg 的两种方式
-
学会用 Python 批量处理视频
-
实战案例:打造一个交互式的视频处理工具箱
一、场景描述
你已经掌握了 FFmpeg 的常用命令,但如果每次都要手动输入命令,效率还是不够高。
用 Python 封装 FFmpeg,你可以:
-
一键批量处理整个文件夹的视频
-
用交互式菜单选择要执行的操作
-
把常用的处理流程保存为函数,随时调用
-
甚至可以打包成 EXE 发给不懂技术的同事用
二、Python 调用 FFmpeg 的两种方式
2.1 方式一:os.system(简单直接)
import os
# 执行 FFmpeg 命令
os.system('ffmpeg -i input.mp4 -c:v libx264 -crf 23 output.mp4')
优点:简单,一行代码就能跑。
缺点:
-
无法获取命令的输出信息
-
无法判断命令是否执行成功
-
无法实时获取进度
2.2 方式二:subprocess(推荐)
import subprocess
# 把命令拆分成列表
cmd = [
'ffmpeg',
'-i', 'input.mp4',
'-c:v', 'libx264',
'-crf', '23',
'output.mp4'
]
# 执行命令
result = subprocess.run(cmd, capture_output=True, text=True)
# 检查是否成功
if result.returncode == 0:
print('✅ 处理成功')
else:
print(f'❌ 处理失败: {result.stderr}')
优点:
-
可以获取输出和错误信息
-
可以判断执行结果
-
更安全(避免 shell 注入)
三、基础封装:FFmpeg 工具函数
3.1 完整代码
新建文件 ffmpeg_tools.py:
import os
import subprocess
import sys
class FFmpegTools:
"""FFmpeg 工具类,封装常用操作"""
def __init__(self, ffmpeg_path='ffmpeg'):
self.ffmpeg_path = ffmpeg_path
self._check_ffmpeg()
def _check_ffmpeg(self):
"""检查 FFmpeg 是否可用"""
try:
result = subprocess.run(
[self.ffmpeg_path, '-version'],
capture_output=True, text=True
)
if result.returncode == 0:
version_line = result.stdout.split('\n')[0]
print(f"✅ FFmpeg 可用: {version_line}")
else:
print("❌ FFmpeg 不可用,请检查安装")
sys.exit(1)
except FileNotFoundError:
print("❌ 未找到 FFmpeg,请确认已安装并配置环境变量")
sys.exit(1)
def convert_format(self, input_path, output_path, codec='libx264'):
"""
格式转换
参数:
input_path: 输入文件路径
output_path: 输出文件路径
codec: 视频编码器
"""
cmd = [
self.ffmpeg_path,
'-i', input_path,
'-c:v', codec,
'-c:a', 'aac',
'-y',
output_path
]
return self._run_command(cmd, f"格式转换: {os.path.basename(input_path)}")
def compress_video(self, input_path, output_path, crf=23, preset='medium'):
"""
压缩视频
参数:
input_path: 输入文件路径
output_path: 输出文件路径
crf: 画质(0-51,越小越好)
preset: 压缩速度
"""
cmd = [
self.ffmpeg_path,
'-i', input_path,
'-c:v', 'libx264',
'-crf', str(crf),
'-preset', preset,
'-c:a', 'aac',
'-b:a', '128k',
'-y',
output_path
]
return self._run_command(cmd, f"压缩视频: {os.path.basename(input_path)}")
def resize_video(self, input_path, output_path, width=1280):
"""
调整分辨率
参数:
input_path: 输入文件路径
output_path: 输出文件路径
width: 目标宽度(高度自动)
"""
cmd = [
self.ffmpeg_path,
'-i', input_path,
'-vf', f'scale={width}:-2',
'-c:v', 'libx264',
'-crf', '23',
'-c:a', 'aac',
'-y',
output_path
]
return self._run_command(cmd, f"调整分辨率: {os.path.basename(input_path)}")
def extract_audio(self, input_path, output_path, format='mp3'):
"""
提取音频
参数:
input_path: 输入文件路径
output_path: 输出文件路径
format: 音频格式(mp3/aac/wav)
"""
codec_map = {
'mp3': 'libmp3lame',
'aac': 'aac',
'wav': 'pcm_s16le'
}
audio_codec = codec_map.get(format, 'libmp3lame')
cmd = [
self.ffmpeg_path,
'-i', input_path,
'-vn',
'-c:a', audio_codec,
'-y',
output_path
]
return self._run_command(cmd, f"提取音频: {os.path.basename(input_path)}")
def cut_video(self, input_path, output_path, start=0, duration=10):
"""
裁剪视频片段
参数:
input_path: 输入文件路径
output_path: 输出文件路径
start: 起始时间(秒)
duration: 截取时长(秒)
"""
cmd = [
self.ffmpeg_path,
'-ss', str(start),
'-i', input_path,
'-t', str(duration),
'-c:v', 'libx264',
'-crf', '23',
'-c:a', 'aac',
'-y',
output_path
]
return self._run_command(cmd, f"裁剪视频: {os.path.basename(input_path)}")
def add_watermark(self, input_path, watermark_path, output_path, position='top-right'):
"""
添加图片水印
参数:
input_path: 输入文件路径
watermark_path: 水印图片路径
output_path: 输出文件路径
position: 水印位置
"""
pos_map = {
'top-left': '10:10',
'top-right': 'main_w-overlay_w-10:10',
'bottom-left': '10:main_h-overlay_h-10',
'bottom-right': 'main_w-overlay_w-10:main_h-overlay_h-10'
}
position_str = pos_map.get(position, '10:10')
cmd = [
self.ffmpeg_path,
'-i', input_path,
'-i', watermark_path,
'-filter_complex', f'overlay={position_str}',
'-c:v', 'libx264',
'-crf', '23',
'-c:a', 'copy',
'-y',
output_path
]
return self._run_command(cmd, f"添加水印: {os.path.basename(input_path)}")
def video_to_gif(self, input_path, output_path, start=0, duration=3,
width=480, fps=10):
"""
视频转 GIF(调色板优化)
参数:
input_path: 输入文件路径
output_path: 输出文件路径
start: 起始时间(秒)
duration: 截取时长(秒)
width: GIF 宽度
fps: 帧率
"""
# 先生成调色板
palette_path = output_path.replace('.gif', '_palette.png')
cmd_palette = [
self.ffmpeg_path,
'-ss', str(start),
'-t', str(duration),
'-i', input_path,
'-vf', f'fps={fps},scale={width}:-2:flags=lanczos,palettegen',
'-y',
palette_path
]
ret1 = self._run_command(cmd_palette, "生成调色板...")
if ret1 != 0:
return ret1
# 用调色板生成 GIF
cmd_gif = [
self.ffmpeg_path,
'-ss', str(start),
'-t', str(duration),
'-i', input_path,
'-i', palette_path,
'-lavfi', f'fps={fps},scale={width}:-2:flags=lanczos[x];[x][1:v]paletteuse',
'-y',
output_path
]
ret2 = self._run_command(cmd_gif, f"生成 GIF: {os.path.basename(input_path)}")
# 清理临时文件
if os.path.exists(palette_path):
os.remove(palette_path)
return ret2
def get_video_info(self, input_path):
"""
获取视频信息
返回:字典,包含时长、分辨率、编码等信息
"""
cmd = [
self.ffmpeg_path,
'-i', input_path,
'-f', 'null',
'-'
]
result = subprocess.run(cmd, capture_output=True, text=True)
stderr = result.stderr
info = {}
# 提取时长
for line in stderr.split('\n'):
if 'Duration' in line:
parts = line.strip().split(',')
for part in parts:
if 'Duration' in part:
info['duration'] = part.split(': ')[1].strip()
# 提取分辨率
if 'Stream' in line and 'Video' in line:
if 'x' in line:
import re
match = re.search(r'(\d+)x(\d+)', line)
if match:
info['width'] = int(match.group(1))
info['height'] = int(match.group(2))
# 提取编码
if '(' in line:
codec = line.split('(')[1].split(')')[0]
info['video_codec'] = codec
# 提取音频信息
if 'Stream' in line and 'Audio' in line:
if 'Audio:' in line:
audio_part = line.split('Audio:')[1].strip()
info['audio_codec'] = audio_part.split(' ')[0]
# 获取文件大小
info['file_size'] = os.path.getsize(input_path)
return info
def _run_command(self, cmd, description=""):
"""执行 FFmpeg 命令"""
if description:
print(f"🔄 {description}")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
print(f" ✅ 完成")
return 0
else:
print(f" ❌ 失败")
if result.stderr:
# 只显示最后几行错误
error_lines = result.stderr.strip().split('\n')[-3:]
for line in error_lines:
print(f" ⚠️ {line}")
return result.returncode
except Exception as e:
print(f" ❌ 异常: {e}")
return -1
def batch_process(self, input_dir, output_dir, process_func,
extensions=('.mp4', '.mkv', '.mov', '.avi')):
"""
批量处理文件夹中的视频
参数:
input_dir: 输入文件夹
output_dir: 输出文件夹
process_func: 处理函数,接收 (input_path, output_path)
extensions: 要处理的文件扩展名
"""
if not os.path.exists(input_dir):
print(f"❌ 输入文件夹不存在: {input_dir}")
return
os.makedirs(output_dir, exist_ok=True)
# 获取所有视频文件
files = []
for f in os.listdir(input_dir):
if f.lower().endswith(extensions):
files.append(f)
if not files:
print("❌ 没有找到视频文件")
return
print(f"\n📁 找到 {len(files)} 个视频文件\n")
success = 0
failed = 0
for i, filename in enumerate(files, 1):
input_path = os.path.join(input_dir, filename)
output_path = os.path.join(output_dir, filename)
print(f"[{i}/{len(files)}] ", end="")
ret = process_func(input_path, output_path)
if ret == 0:
success += 1
else:
failed += 1
print(f"\n📊 处理完成:成功 {success} 个,失败 {failed} 个")
四、交互式工具箱
4.1 完整代码
新建文件 video_toolbox.py:
import os
from ffmpeg_tools import FFmpegTools
def show_menu():
"""显示主菜单"""
print("\n" + "=" * 55)
print(" 视频处理工具箱 v1.0")
print("=" * 55)
print("1. 格式转换(MP4 / MKV / MOV / AVI)")
print("2. 压缩视频(控制文件大小)")
print("3. 调整分辨率(4K → 1080p / 720p)")
print("4. 提取音频(视频 → MP3 / AAC / WAV)")
print("5. 裁剪视频片段")
print("6. 添加图片水印")
print("7. 视频转 GIF")
print("8. 查看视频信息")
print("9. 批量处理")
print("0. 退出")
print("=" * 55)
def select_file(title="请选择视频文件"):
"""让用户选择文件"""
path = input(f"{title}:").strip().strip("\"'")
while not os.path.exists(path):
print("❌ 文件不存在,请重新输入")
path = input(f"{title}:").strip().strip("\"'")
return path
def select_folder(title="请选择文件夹"):
"""让用户选择文件夹"""
path = input(f"{title}:").strip().strip("\"'")
while not os.path.exists(path):
print("❌ 文件夹不存在,请重新输入")
path = input(f"{title}:").strip().strip("\"'")
return path
def main():
tools = FFmpegTools()
while True:
show_menu()
choice = input("请选择功能 (0-9):").strip()
if choice == "0":
print("👋 再见!")
break
elif choice == "1":
# 格式转换
input_path = select_file()
output_path = input("请输入输出文件路径(如 output.mp4):").strip()
print("\n选择目标格式:")
print("1. MP4 (H.264)")
print("2. MKV")
print("3. AVI")
fmt = input("请选择 (1-3,默认 1):").strip() or "1"
codec_map = {"1": "libx264", "2": "libx264", "3": "libxvid"}
codec = codec_map.get(fmt, "libx264")
tools.convert_format(input_path, output_path, codec)
elif choice == "2":
# 压缩视频
input_path = select_file()
name, ext = os.path.splitext(input_path)
default_output = f"{name}_compressed{ext}"
output_path = input(f"请输入输出路径(默认 {default_output}):").strip()
if not output_path:
output_path = default_output
try:
crf = int(input("请输入 CRF 值 (18-28,默认 23):").strip() or "23")
except:
crf = 23
tools.compress_video(input_path, output_path, crf)
elif choice == "3":
# 调整分辨率
input_path = select_file()
name, ext = os.path.splitext(input_path)
default_output = f"{name}_resized{ext}"
output_path = input(f"请输入输出路径(默认 {default_output}):").strip()
if not output_path:
output_path = default_output
try:
width = int(input("请输入目标宽度(如 1920/1280/720,默认 1280):").strip() or "1280")
except:
width = 1280
tools.resize_video(input_path, output_path, width)
elif choice == "4":
# 提取音频
input_path = select_file()
name = os.path.splitext(input_path)[0]
print("\n选择音频格式:")
print("1. MP3")
print("2. AAC")
print("3. WAV(无损)")
fmt = input("请选择 (1-3,默认 1):").strip() or "1"
ext_map = {"1": ".mp3", "2": ".aac", "3": ".wav"}
fmt_map = {"1": "mp3", "2": "aac", "3": "wav"}
audio_ext = ext_map.get(fmt, ".mp3")
audio_fmt = fmt_map.get(fmt, "mp3")
default_output = f"{name}_audio{audio_ext}"
output_path = input(f"请输入输出路径(默认 {default_output}):").strip()
if not output_path:
output_path = default_output
tools.extract_audio(input_path, output_path, audio_fmt)
elif choice == "5":
# 裁剪视频
input_path = select_file()
name, ext = os.path.splitext(input_path)
default_output = f"{name}_cut{ext}"
output_path = input(f"请输入输出路径(默认 {default_output}):").strip()
if not output_path:
output_path = default_output
try:
start = float(input("请输入起始时间(秒):").strip() or "0")
duration = float(input("请输入截取时长(秒,默认 10):").strip() or "10")
except:
start = 0
duration = 10
tools.cut_video(input_path, output_path, start, duration)
elif choice == "6":
# 添加水印
input_path = select_file("请选择视频文件")
watermark_path = select_file("请选择水印图片(PNG)")
name, ext = os.path.splitext(input_path)
default_output = f"{name}_watermarked{ext}"
output_path = input(f"请输入输出路径(默认 {default_output}):").strip()
if not output_path:
output_path = default_output
print("\n选择水印位置:")
print("1. 左上角")
print("2. 右上角")
print("3. 左下角")
print("4. 右下角")
pos = input("请选择 (1-4,默认 2):").strip() or "2"
pos_map = {"1": "top-left", "2": "top-right",
"3": "bottom-left", "4": "bottom-right"}
position = pos_map.get(pos, "top-right")
tools.add_watermark(input_path, watermark_path, output_path, position)
elif choice == "7":
# 视频转 GIF
input_path = select_file()
name = os.path.splitext(input_path)[0]
default_output = f"{name}.gif"
output_path = input(f"请输入输出路径(默认 {default_output}):").strip()
if not output_path:
output_path = default_output
try:
start = float(input("请输入起始时间(秒):").strip() or "0")
duration = float(input("请输入截取时长(秒,默认 3):").strip() or "3")
width = int(input("请输入宽度(像素,默认 480):").strip() or "480")
fps = int(input("请输入帧率(默认 10):").strip() or "10")
except:
start = 0
duration = 3
width = 480
fps = 10
tools.video_to_gif(input_path, output_path, start, duration, width, fps)
elif choice == "8":
# 查看视频信息
input_path = select_file()
info = tools.get_video_info(input_path)
print("\n📋 视频信息:")
print(f" 文件大小: {info.get('file_size', 0) / 1024 / 1024:.2f} MB")
print(f" 时长: {info.get('duration', '未知')}")
print(f" 分辨率: {info.get('width', '未知')}x{info.get('height', '未知')}")
print(f" 视频编码: {info.get('video_codec', '未知')}")
print(f" 音频编码: {info.get('audio_codec', '未知')}")
elif choice == "9":
# 批量处理
input_dir = select_folder("请输入输入文件夹")
output_dir = select_folder("请输入输出文件夹")
print("\n选择批量处理功能:")
print("1. 批量格式转换(转 MP4)")
print("2. 批量压缩")
print("3. 批量调整分辨率")
print("4. 批量提取音频")
choice_batch = input("请选择 (1-4):").strip()
if choice_batch == "1":
tools.batch_process(
input_dir, output_dir,
lambda inp, out: tools.convert_format(inp, out)
)
elif choice_batch == "2":
try:
crf = int(input("请输入 CRF 值 (18-28,默认 23):").strip() or "23")
except:
crf = 23
tools.batch_process(
input_dir, output_dir,
lambda inp, out: tools.compress_video(inp, out, crf)
)
elif choice_batch == "3":
try:
width = int(input("请输入目标宽度(默认 1280):").strip() or "1280")
except:
width = 1280
tools.batch_process(
input_dir, output_dir,
lambda inp, out: tools.resize_video(inp, out, width)
)
elif choice_batch == "4":
tools.batch_process(
input_dir, output_dir,
lambda inp, out: tools.extract_audio(inp, out)
)
else:
print("❌ 无效选择")
else:
print("❌ 无效选择,请重新输入")
input("\n按回车键继续...")
if __name__ == "__main__":
main()
五、运行效果
=======================================================
视频处理工具箱 v1.0
=======================================================
1. 格式转换(MP4 / MKV / MOV / AVI)
2. 压缩视频(控制文件大小)
3. 调整分辨率(4K → 1080p / 720p)
4. 提取音频(视频 → MP3 / AAC / WAV)
5. 裁剪视频片段
6. 添加图片水印
7. 视频转 GIF
8. 查看视频信息
9. 批量处理
0. 退出
=======================================================
请选择功能 (0-9):2
请选择视频文件:C:\Users\admin\Desktop\demo.mp4
请输入输出路径(默认 demo_compressed.mp4):
请输入 CRF 值 (18-28,默认 23):23
🔄 压缩视频: demo.mp4
✅ 完成
六、进阶:打包为 EXE
如果你想把工具箱发给不懂技术的同事用,可以用 PyInstaller 打包成 EXE。
6.1 安装 PyInstaller
pip install pyinstaller
6.2 打包命令
pyinstaller --onefile --windowed --icon=icon.ico video_toolbox.py
| 参数 | 含义 |
|---|---|
--onefile |
打包成单个 EXE 文件 |
--windowed |
不显示命令行窗口 |
--icon |
设置图标 |
6.3 注意事项
打包后的 EXE 仍然需要依赖 FFmpeg。有两种方式解决:
-
要求用户自行安装 FFmpeg(推荐,避免法律风险)
-
把 FFmpeg 的可执行文件打包进 EXE(需要修改代码中的路径)
七、本讲总结
| 知识点 | 掌握程度 |
|---|---|
os.system调用 FFmpeg |
✅ 已了解 |
subprocess调用 FFmpeg |
✅ 已掌握 |
| 封装常用操作为函数 | ✅ 已实践 |
| 交互式菜单工具箱 | ✅ 已构建 |
| 批量处理功能 | ✅ 已实现 |
| 打包为 EXE | ✅ 已了解 |
八、课后练习
-
给工具箱增加一个"视频拼接"功能(提示:用 concat 协议)
-
给批量处理增加进度条显示(提示:用 tqdm 库)
-
把工具箱打包成 EXE,发给朋友测试一下
本文完。如果你觉得有用,不妨收藏我的工具站:https://zz365.top ------ 你的本地化在线工具箱,安全、免费、即用即走。