#!/usr/bin/env python # -*- coding: utf-8 -*- ''' # @Time : 2025/9/25 15:30 # @Author : reenrr # @File : merge_video.py ''' # !/usr/bin/env python # -*- coding: utf-8 -*- ''' # @Time : 2025/09/25 # @Author : reenrr # @File : video_merger.py # 功能描述:基于 FFmpeg 合并多个视频文件(支持跨格式、进度显示) # 依赖:需先安装 FFmpeg 并配置环境变量 ''' import os import subprocess import platform from typing import List class VideoMerger: def __init__(self): # 检查 FFmpeg 是否安装(通过调用 ffmpeg -version 验证) self._check_ffmpeg_installed() def _check_ffmpeg_installed(self) -> None: """检查 FFmpeg 是否已安装并配置环境变量""" try: # 调用 FFmpeg 版本命令,无异常则说明安装成功 subprocess.run( ["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True ) except FileNotFoundError: raise EnvironmentError( "未找到 FFmpeg!请先安装 FFmpeg 并配置系统环境变量。\n" "下载地址:https://ffmpeg.org/download.html\n" "Windows 配置:将 FFmpeg 的 bin 目录添加到 PATH 环境变量;\n" "macOS:使用 brew install ffmpeg;\n" "Linux:使用 sudo apt install ffmpeg(Ubuntu)。" ) except subprocess.CalledProcessError: raise RuntimeError("FFmpeg 安装异常,请重新安装。") def _create_video_list_file(self, video_paths: List[str], list_file_path: str = "video_list.txt") -> str: """ 创建 FFmpeg 所需的视频列表文件(用于合并多个视频) FFmpeg 合并需通过文本文件指定视频路径,格式为:file '视频路径' """ # 检查所有视频文件是否存在 for idx, path in enumerate(video_paths): if not os.path.exists(path): raise FileNotFoundError(f"第 {idx + 1} 个视频文件不存在:{path}") # 检查是否为文件(而非目录) if not os.path.isfile(path): raise IsADirectoryError(f"第 {idx + 1} 个路径不是文件:{path}") # 写入视频列表(处理路径中的特殊字符,兼容 Windows 反斜杠) with open(list_file_path, "w", encoding="utf-8") as f: for path in video_paths: # 统一路径分隔符为正斜杠(FFmpeg 兼容正斜杠) normalized_path = path.replace("\\", "/") # 写入格式:file '路径'(单引号避免路径含空格/特殊字符) f.write(f"file '{normalized_path}'\n") return list_file_path def merge_videos(self, video_paths: List[str], output_path: str = "merged_output.mp4", overwrite: bool = False) -> None: """ 合并多个视频文件 :param video_paths: 待合并的视频路径列表(顺序即合并顺序) :param output_path: 输出视频路径(默认当前目录 merged_output.mp4) :param overwrite: 是否覆盖已存在的输出文件(默认不覆盖) """ # 1. 基础参数校验 if len(video_paths) < 2: raise ValueError("至少需要 2 个视频文件才能合并!") # 2. 处理输出文件(若已存在且不允许覆盖,直接退出) if os.path.exists(output_path) and not overwrite: raise FileExistsError(f"输出文件已存在:{output_path},若需覆盖请设置 overwrite=True。") # 3. 创建 FFmpeg 视频列表文件 list_file = self._create_video_list_file(video_paths) print(f"已创建视频列表文件:{list_file}") # 4. 构造 FFmpeg 合并命令 # -f concat:使用 concat 协议(合并多个文件) # -safe 0:允许处理任意路径的文件(避免 FFmpeg 限制相对路径) # -i {list_file}:输入视频列表文件 # -c copy:直接复制音视频流(不重新编码,速度快;若格式不兼容会自动 fallback 编码) # -y:强制覆盖输出文件(仅 overwrite=True 时启用) ffmpeg_cmd = [ "ffmpeg", "-f", "concat", "-safe", "0", "-i", list_file, "-c", "copy", # 核心参数:复制流(快速合并),若格式不兼容可删除此句(自动编码) "-hide_banner", # 隐藏 FFmpeg 版本横幅(简化输出) "-loglevel", "info" # 显示进度和关键日志(可改为 warning 减少输出) ] # 若允许覆盖,添加 -y 参数 if overwrite: ffmpeg_cmd.append("-y") # 添加输出路径 ffmpeg_cmd.append(output_path) try: print(f"\n开始合并视频(共 {len(video_paths)} 个):") for idx, path in enumerate(video_paths, 1): print(f" {idx}. {os.path.basename(path)}") print(f"\n输出路径:{os.path.abspath(output_path)}") print("=" * 50) # 5. 执行 FFmpeg 命令(实时打印进度) process = subprocess.Popen( ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # 合并 stdout 和 stderr,统一捕获进度 text=True, bufsize=1 # 行缓冲,实时输出日志 ) # 实时打印 FFmpeg 输出(显示合并进度) for line in process.stdout: print(line.strip(), end="\r" if "time=" in line else "\n") # 进度行覆盖显示,其他行换行 # 等待命令执行完成,获取返回码 process.wait() if process.returncode == 0: print("\n" + "=" * 50) print(f"视频合并成功!输出文件:{os.path.abspath(output_path)}") else: raise RuntimeError(f"FFmpeg 执行失败(返回码:{process.returncode}),请检查日志排查问题。") finally: # 6. 清理临时文件(视频列表文件) if os.path.exists(list_file): os.remove(list_file) print(f"\n已清理临时列表文件:{list_file}") if __name__ == "__main__": # -------------------------- 配置参数(根据需求修改) -------------------------- # 1. 待合并的视频路径列表(顺序即合并顺序,支持绝对路径/相对路径) # 示例1:相对路径(视频文件与脚本在同一目录) # VIDEO_PATHS = [ # "video1.mp4", # "video2.mkv", # "video3.avi" # ] # 示例2:绝对路径(Windows 需用双反斜杠或正斜杠) VIDEO_PATHS = [ r"C:\Project\zjsh\data_collection\data_collection\camera01_videos\video_20250925_062802_part1.mp4", # Windows 绝对路径(r 表示原始字符串,避免转义) r"C:\Project\zjsh\data_collection\data_collection\camera01_videos\video_20250925_062905_part2.mp4" ] # 2. 输出视频路径(默认当前目录,可自定义) OUTPUT_PATH = "merged_video.mp4" # 3. 是否覆盖已存在的输出文件(True=覆盖,False=不覆盖) OVERWRITE = True # -------------------------- 执行合并 -------------------------- try: merger = VideoMerger() merger.merge_videos( video_paths=VIDEO_PATHS, output_path=OUTPUT_PATH, overwrite=OVERWRITE ) except Exception as e: print(f"\n合并失败:{str(e)}")