第10讲:Python 调用 FFmpeg 批量处理——打造你的视频处理工具箱


本讲目标

  • 掌握 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。有两种方式解决:

  1. 要求用户自行安装 FFmpeg(推荐,避免法律风险)

  2. 把 FFmpeg 的可执行文件打包进 EXE(需要修改代码中的路径)


七、本讲总结

知识点 掌握程度
os.system调用 FFmpeg ✅ 已了解
subprocess调用 FFmpeg ✅ 已掌握
封装常用操作为函数 ✅ 已实践
交互式菜单工具箱 ✅ 已构建
批量处理功能 ✅ 已实现
打包为 EXE ✅ 已了解

八、课后练习

  1. 给工具箱增加一个"视频拼接"功能(提示:用 concat 协议)

  2. 给批量处理增加进度条显示(提示:用 tqdm 库)

  3. 把工具箱打包成 EXE,发给朋友测试一下


本文完。如果你觉得有用,不妨收藏我的工具站:https://zz365.top ------ 你的本地化在线工具箱,安全、免费、即用即走。

相关推荐
神经蛙199615 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
颜酱15 小时前
14 | 验证并修正 LLM 生成的 SQL
人工智能·python
AI创界者15 小时前
AIGC进阶】Sulphur-2 视频生成大模型离线实战:文生视频/图生视频本地一键部署整合包解压即用与调优指南
人工智能·aigc·音视频
颜酱15 小时前
13 | 使用 LangChain 生成 SQL
人工智能·python·langchain
PieroPc15 小时前
Python 写的 Windows凭据添加工具 第二版 解决Win11 23H2, 25H2,26H1,共享文件和打印 一部份问题,不是全部!
开发语言·windows·python
颜酱16 小时前
12 | 组装 SQL 生成上下文
人工智能·python
The_Ticker16 小时前
大宗商品行情API接入教程
开发语言·python·websocket·程序人生·区块链
用户83562907805116 小时前
如何使用 Python 合并多个 Excel 文件
后端·python
颜酱16 小时前
11 | 合并过滤召回结果 - 确定需要的表、字段和指标
人工智能·python
circuitsosk17 小时前
AI系统的效果评测体系:从离线指标到线上业务反馈闭环
人工智能·python