
前阵子被H265视频折腾得够呛。H265HEVC编码确实省空间相同画质体积能比H264小一半左右但问题也随之而来——很多老播放器、非专业剪辑软件、单位里的大屏系统压根不认H265。要么直接黑屏要么只有声音没画面。手头一批重要素材全是这个格式网上找的转码工具要么收费要么转出来画质劣化得离谱。后来我干脆用Python写了一个视频转码工具专门干H265转H264这种活顺带解决了批量处理效率低下的问题。这篇文章就把我整个设计思路、核心代码和踩过的坑都翻出来供有同样需求的朋友参考。1. 项目背景与核心需求拆解1.1 为什么非要把H265转成H264H265的普及度比大家想象中慢很多。虽然Apple设备、新出的安卓旗舰都硬解H265但很多兼容性场景依然是H264的天下。比如老旧电脑的浏览器端播放H264基本是默认标准非编软件尤其早版本的对H265支持极差导入卡顿甚至不识别电视盒子、投影仪、车载影音系统大多只内置了H264解码器很多企事业单位的文档管理系统上传视频只认H264编码我当时面临的实际场景是把所有培训录像H265编码无损转成H264格式以便分发到各个没有安装解码器的Windows电脑上播放。需求看起来简单但真要手动用格式工厂之类一个个操作几百个文件能点死人。所以自然而然想到了用Python脚本批量调用ffmpeg做一个带进度条和并发控制的小工具。1.2 我最终选择Python方案的原因先泼一盆冷水如果你只需要转一两个文件命令行直接敲ffmpeg就够了根本不用写程序。但一旦涉及批量、重命名、选择性转码、失败重试、进度监控脚本的价值就出来了。Python在这个场景的优势跨平台Windows、Linux、macOS都能跑不用为了个转码工具改环境subprocess模块能干净地调用外部程序ffmpeg本身是现成的转码引擎我们只需要做逻辑编排生态成熟进度条用tqdm硬件信息用psutilGUI可以用PyQt或者Tkinter想怎么扩展都行代码易读后期维护成本低写个小工具不需要上C/GoPython足够有人问过我怎么不用Bat脚本Windows批处理写个循环也能做但出错处理、并发控制、日志记录、参数配置都太折磨人。Python写起来行云流水也更方便把经验沉淀成一段可复用的代码。2. 整体方案设计与技术选型2.1 转码引擎ffmpeg走起ffmpeg是视频转码的事实标准体积大但能力极强支持H264/H265编码、音视频流复制、滤镜、元数据编辑。我的工具本质上是一个“ffmpeg命令生成器任务调度器”核心思路不是自己实现编解码算法那工作量太大了而是利用ffmpeg的成熟能力。为什么不用OpenCVOpenCV的VideoWriter虽然也能输出H264但依赖特定后端压缩率、质量控制和批量处理能力都不如ffmpeg直接。而且ffmpeg可以轻松处理音频流、字幕流、章节信息等OpenCV在这方面非常弱。为什么不用MoviePyMoviePy的底层也是ffmpeg但它适合做特效和片段剪辑处理大批量整片转码时初始化开销和对象模型太重而且并发支持不如直接用subprocess跑ffmpeg来得干净。2.2 任务调度多进程还是多线程转码是CPU重负载操作而且ffmpeg进程本身就能吃满多个核心。如果我在Python里开多线程去并发调用ffmpeg会受到GIL影响吗实际上subprocess启动外部进程后GIL不会影响ffmpeg的执行因为那是独立进程。但问题在于Python层面如果同时启动太多ffmpeg进程CPU会过载反而拖慢整体速度。我采用的策略是先扫描文件夹内所有视频文件逐个探测编码格式只收集H265编码的文件用concurrent.futures.ProcessPoolExecutor或ThreadPoolExecutor限制并发数为CPU物理核心数的一半避免所有核心被打满导致电脑假死每个任务内部调用ffmpeg返回成功或失败信息收集结果并生成日志报表有人担心多进程更“正宗”其实在这个场景里线程池更适合因为主要耗时在等待外部ffmpeg进程结束不涉及Python对象重度共享线程开销更小代码也更简单。实测两种方式速度差不多线程池在Windows上更省内存。2.3 界面与交互设计我最初只做命令行版因为我自己用起来最顺手。但后来同事说看不懂命令行索性加了一个简单的Tkinter图形界面就两个核心控件一个选择源文件夹一个开始转码的按钮。Tkinter是Python自带的不需要额外安装依赖发出去也不会因为缺PyQt不好装而报错。当然如果你想把工具做得更专业可以选用PySide6/PyQt6支持更多控件和样式但体积和分发成本会高不少。我最终保留了Tkinter版本因为我的目标用户根本不需要炫酷UI他们只需要“选文件夹、点按钮、等结果”。3. 核心实现手把手写转码工具3.1 环境准备与依赖安装先把基本环境配好# 1. 安装Python3.8推荐3.10 # 2. 安装ffmpeg并将ffmpeg.exe所在目录添加到系统PATH # 验证ffmpeg是否安装成功 ffmpeg -version # 安装Python依赖库 pip install tqdm psutil如果你用的是Windowsffmpeg建议从官方网站下载release版本。下载后是一个zip包解压到任意位置然后把bin目录路径加到系统环境变量PATH里。验证方式是在新开命令行里输入ffmpeg -version如果能打印出版本信息就代表成功。另外高版本的ffmpeg可能不在H.264这个名字上倒腾而是叫libx264。同时Windows下ffmpeg默认可能不带libx265编码器但解码H265是内置的所以我们要转码H265→H264只需要H264编码器libx264这个默认就包含不需要额外编译。所以一般不需要自己编译ffmpeg直接用官方release即可。3.2 获取视频编码信息转码前需要先判断视频到底是不是H265编码。最简单的方式是调用ffprobe它是ffmpeg自带的媒体探测工具。import subprocess import json def get_video_info(filepath): cmd [ ffprobe, -v, quiet, -print_format, json, -show_format, -show_streams, filepath ] result subprocess.run(cmd, capture_outputTrue, textTrue, encodingutf-8) if result.returncode ! 0: raise RuntimeError(fffprobe failed: {result.stderr}) return json.loads(result.stdout)在这个JSON里我们遍历streams列表找到codec_type video的流读取它的codec_name。如果是hevc就说明需要转码。还可以顺带读取分辨率、帧率、像素格式等信息这些对于后续命令参数调整很有用。3.3 构建转码命令与参数细节核心转码命令大概长这样ffmpeg -i input.mkv -c:v libx264 -preset medium -crf 23 -c:a copy -c:s copy output.mp4但实际使用中我踩过不少坑最终改进版是这样def build_ffmpeg_cmd(input_path, output_path, video_bitrateNone, audio_bitrateNone, presetmedium): cmd [ ffmpeg, -y, -i, input_path, -map, 0, -c:v, libx264, -preset, preset, ] if video_bitrate: cmd [-b:v, video_bitrate] else: cmd [-crf, 20] # 音频默认复制原音轨除非源音频编码不兼容 cmd [-c:a, copy] # 字幕流尝试复制如果输出容器不支持则可能会失败所以可加 optional cmd [-c:s, mov_text if output_path.endswith(.mp4) else copy] cmd [-movflags, faststart] cmd [-max_muxing_queue_size, 1024] cmd [output_path] return cmd这里参数为什么这么设-map 0保留所有流视频、音频、字幕、章节避免转码后音轨/字幕丢失-preset medium编码速度与压缩率的平衡点。预设越慢压得越小但速度也慢。如果文件多且不追求极限体积medium比较推荐-crf 20视觉无损级别的质量参数。CRF值越小质量越高1823是常见范围20是我多次对比后的甜点值-c:a copy直接复制音频流不进行重编码速度快且音质无损-movflags faststart将moov atom移到文件头部适合网络播放避免上传到平台后客户端必须下载完整文件才能播放-max_muxing_queue_size 1024防止多音轨、多字幕时出现“Too many packets buffered”报错还有一个重要选项当源分辨率特别大比如4K想要压到1080p可以加-vf scale1920:-2其中-2表示按原比例自动计算高度并且保证偶数否则H264编码会报“width/height not divisible by 2”。这个操作在批量处理电视录屏时经常用到。3.4 进度展示与任务并发控制ffmpeg的进度信息在stdout中输出但直接丢给终端会刷屏。我的做法是解析-progress参数输出到临时文件然后由Python读取估算剩余时间。但更简单粗暴的方式是对每个待转码文件记录开始和结束时间用tqdm在总任务层面显示百分比。虽然单个文件内部的进度不够细致但整体进度直观明了用户也不需要知道单文件压到哪一秒。from concurrent.futures import ThreadPoolExecutor, as_completed from tqdm import tqdm def transcode_file(task): input_path, output_path task cmd build_ffmpeg_cmd(input_path, output_path) process subprocess.run(cmd, capture_outputTrue, textTrue, encodingutf-8) if process.returncode 0: return input_path, output_path, True, else: return input_path, output_path, False, process.stderr[-500:] def batch_transcode(tasks, max_workers, base_dir): results [] with ThreadPoolExecutor(max_workersmax_workers) as executor: futures {executor.submit(transcode_file, task): task for task in tasks} with tqdm(totallen(futures), desc转码进度, unitfile) as pbar: for future in as_completed(futures): input_path, output_path, ok, err future.result() results.append((input_path, output_path, ok, err)) pbar.update(1) pbar.set_postfix(successok) return results关于并发数我建议CPU核心数如果是8核16线程并发数可以设为45如果只是普通办公本4核8线程并发数设为23如果使用硬件加速后面会讲并发数可以适当提高但硬盘IO也可能成为瓶颈另外如果长时间满负荷运行要关注CPU温度。我在脚本里加了一个psutil轮询CPU利用率和温度的开关当温度超过85度时自动等待几个任务再启动新的防止电脑过热重启。3.5 完整业务逻辑整个工具的处理流程选择文件夹或拖拽文件到窗口递归遍历所有视频文件.mp4/.mkv/.mov/.avi/.ts等逐个探测编码只保留H265视频流生成输出路径默认在源文件旁边创建h264_output文件夹保持原文件名但容器改为.mp4构建任务列表显示待转码数量点击开始转码控制并发数实时将成功/失败结果写入日志文件完成后统计成功率这个流程确保用户不会把非H265文件也塞进转码流程浪费时间和硬盘空间。4. 实操过程与运行效果4.1 单文件转码实测先拿一个2.8GB的MKV文件测试1080pH265 10bit编码内置两条音轨和两条字幕。命令示例ffmpeg -y -i input.mkv -map 0 -c:v libx264 -preset medium -crf 20 -c:a copy -c:s mov_text -movflags faststart output.mp4这里注意如果源视频是10bit H265pix_fmt是yuv420p10le直接转H264时要考虑像素格式问题。H264常见的是8bit yuv420p如果不指定ffmpeg会自动转换但画质上会有些微损失。如果想在转码时做色调映射可以加-vf formatyuv420p。其实我测试下来10bit转8bit后暗部会有轻微banding但播放器兼容性优先没办法绝大部分消费级编码都是8bit。单文件耗时2.8GB文件大约播放时长1.5小时在8核16线程的台式机上用medium预设耗时约22分钟转码后文件大小1.4GB。画质经过抽查没有感知差异但文件的播放兼容性立即提升。4.2 批量文件夹转码批量转码时我建了个输出目录结构把文件按照源文件夹层级复制出来media_folder/ 01_培训/ part1.mkv 02_会议/ day2.mp4 h264_output/ 01_培训/ part1.mp4 02_会议/ day2.mp4这样用os.walk遍历原始文件夹对每个H265文件生成相同相对路径可以避免文件名冲突和杂乱。批量测试100个文件总数据量约180GB耗时6小时大概因为里面有几个4K长视频。使用并发4的情况下CPU占用维持在70%85%不会让电脑完全卡死期间还能正常打字办公。日志文件每一行记录文件名、耗时、输出大小、返回码方便出问题后追溯。4.3 踩过的坑与参数微调第一个坑是输出容器格式。刚开始统一输出成.mp4但有些MKV内封了PGS字幕图形字幕MP4容器只能支持mov_text或者外挂字幕直接复制轨道会失败。解决方案检测字幕编码格式如果是hdmv_pgs_subtitle要么转成srt用-c:s srt要么保留MKV容器作为输出。为了统一性我最终选择把PGS字幕烧录进画面里-vf subtitles...或者丢弃在业务逻辑里加个选项。第二个坑是音频的DTS音轨。多数MP4播放器不支持DTS所以-c:a copy可能导致播放器没声音。排查后发现这个情况后我在构建命令时加了检查如果音频编码是dts或ac3且输出为MP4就强制重编码为AAC 192kbps避免用户播放时一脸懵。第三个坑是旋转元数据。用手机竖屏拍的视频H264编码时如果源文件的rotation元数据是90°直接转码后播放器可能识别不了旋转信息导致横屏播放。我通过ffprobe读取side_data_list里的rotation值如果非零就用-metadata:s:v rotate0 -vf transpose1之类的手段把画面转正再编码。第四个坑是大文件的写入稳定性。有个4K 60fps的视频在转码到一半时系统提示“No space left on device”检查发现输出目录剩余空间少于预估值。我在工具启动前会统计源文件总大小并提示用户输出目录剩余空间至少要是源文件总量的1.5倍否则拒绝执行。5. 常见问题与排查技巧实录5.1 找不ffmpeg或版本异常症状程序一运行就报FileNotFoundError: ffmpeg not found。排查思路在命令行执行ffmpeg -version如果提示命令不存在说明已写入PATH如果你用的是IDE里的PythonIDE有时不会自动加载新修改的环境变量需要重启IDE如果不想改动系统PATH可以在Python代码中直接指定ffmpeg的绝对路径比如FFMPEG_BIN rD:\tools\ffmpeg\bin\ffmpeg.exe这也更便于以后调试避坑下载ffmpeg解压后不要只把ffmpeg.exe放进去必须把bin目录整个放到PATH中因为ffprobe也在那个目录。5.2 转出来的视频依然无法播放症状输出是.mp4但某些播放器还是提示无法渲染或格式不支持。可能原因输出像素格式非yuv420p。部分播放器不支持yuv444p或yuv422p加上-pix_fmt yuv420p即可编码配置过于激进。比如用了-tune film或者-profile:v设为high10这种罕见配置放弃兼容性。建议默认不主动指定profile让ffmpeg根据容器自动选Main或High文件扩展名与真实编码不符。检查一下ffprobe输出的codec_name确保输出流是h264实战建议批量转码前先转一个最短的样本放到目标播放器上实测确认没问题再跑全量。5.3 转出来的视频没有声音症状画面正常音频播不出来。排查用ffprobe查看输出文件的音频流编码如果音频编码是dts、ac3或truehd很大概率是播放器不支持修改命令加上-c:a aac -b:a 192k如果是AAC但依然没声音检查播放器是不是识别了多个音轨共享了音量控制这个比较玄学但至少AAC是通用性最好的音频编码我的默认策略在转码时先尝试-c:a copy然后事后用ffprobe检查音频编码。如果是兼容性差的编码自动重跑一次用AAC重新编码这条音轨。虽然多耗时间但保证输出一定能用。5.4 转码速度太慢或者CPU占用过高原因软件编码libx264在medium预设下速度大概每分钟转24秒的1080p视频。8核机器转1小时视频大约要1525分钟如果设置成了veryslow时间会成倍增长。解决调整-preset为faster牺牲体积换速度画质差距肉眼几乎看不出使用硬件加速。Intel核显可以加-hwaccel qsv -c:v h264_qsvN卡加-hwaccel cuda -c:v h264_nvenc。注意硬件编码速度极快但画质和体积略逊于软件编码对于内部交付足够减少并发数。如果多任务并发导致CPU温度高降频总吞吐反而下降不如减少并发数稳定输出我实际使用中对于时间紧的批量任务可以直接用h264_nvenc即使质量设置低一点也能在几分钟内完成一个大文件而且显卡不参与渲染时不怎么影响桌面流畅度。5.5 磁盘空间不足或输出路径有中文导致失败症状日志里出现Invalid argument或Permission denied。原因某些ffmpeg版本在Windows下无法正确处理非ASCII字符路径尤其中文文件名。这在老版本ffmpeg中很常见。解决升级ffmpeg到最新版本新版本对Unicode路径支持好很多仍不行就用文件复制的方式转换先把中文路径下的源文件复制到临时英文路径比如C:\temp转码成功后再复制回原目录同时保持英文名输出路径中避免使用空格和特殊符号用下划线替代这个坑很隐蔽尤其在批量工具里几十个文件只有一个文件名是中文偏偏就它失败排查半天才发现是编码问题。5.6 转换后文件变大了很多原因H265压缩率比H264高同码率下H264文件必然更大。比如一个100M的H265按原始码率转成H264可能要200M以上。调整使用-crf控制质量而非码率。CRF值可以适当增大比如23画质依然不错体积能比20小不少指定-maxrate和-bufsize限制峰值码率比如-maxrate 10M -bufsize 20M如果目的是网络传输可以考虑用H265直接打包MP4然后要求客户端安装解码器。但总有人不肯装所以还是老老实实转H264我做工具时最核心的取舍就是质量优先还是兼容优先。如果只是内部自用建议保持高画质如果是发布到公开平台可以适当压一压。6. 一点扩展与个人体会这个工具后续我又加了几个小功能支持拖拽文件列表、自动跳过已完成的相同文件用MD5对比源和输出、转码完成提示音、简单的图形界面。每次有朋友来求助“视频打不开”我都直接扔给他们这个脚本顺手教他们怎么看视频编码。我个人在实际操作中的体会是H265转H264这种工作最重要的是“批量自动化异常兜底”不是炫技。代码再花哨转码失败率高了没人敢用。所以我的脚本在主干上很简单始终围绕ffmpeg做薄薄一层封装但在参数检查、文件校验、日志收集上做得比较扎实。最后再分享一个小技巧转码前先用ffprobe把所有视频的编码信息导出成CSV这样能预判哪些文件是H265、哪些是H264、哪些是重编码的高风险文件比如10bit、VFR。做完这个前置检查批量转码基本不会出幺蛾子。搞定了这批视频也算是把一个困扰许久的老大难彻底解决了。