H.265 编码的视频看着就让人心动同样一部片源H.265 比 H.264 能少占 30%~50% 的体积码率也低非常适合硬盘仓储和网络传输。可问题在于H.265 推出了这么多年如今依然不是所有软件和硬件都能流畅解码。电脑上明明有播放器打开 H.265 文件却是黑屏、花屏、有声音没画面剪辑软件导不进工程分享给朋友又打不开那节省下来的空间瞬间就变成了麻烦。我自己做视频素材归档和 NAS 资源整理时最常遇到的就是这类场景。一开始也想过换一个万能播放器但后来发现治标不治本因为素材最终要交给别人用他们手里的工具和播放软件各不相同我只能够从源头把编码变成兼容性更好的 H.264。于是我用 Python 写了一套批量转码工具专门处理 H.265 到 H.264 的转换跑了几个月把关键细节都摸透了。这篇文章围绕这套工具的完整实现展开从环境准备到核心代码再到各种报错排查全部一块一块讲清楚。如果你正准备做类似的视频处理工具可以直接把思路和代码拿走改。1. H.265 转 H.264需求场景与方案选型1.1 为什么不是换个能硬解 HEVC 的播放器而是转码单从使用者角度讲遇到 H.265 打不开最简单的办法确实是换播放器。以 PotPlayer、完美解码这类集成解码器的播放器为例硬解 HEVC 非常轻松几分钟就能把黑屏变成正常画面。但如果你在做视频交付、素材归档、节目上传真正的问题就不是“自己能不能播”而是“接收方那个设备能不能播”。我曾经给一个客户交付过一批 H.265 的样片对方在微信里点开直接提示“不支持该视频格式”后来电话沟通才知道他用的电脑还算新但浏览器和默认播放器都不支持 HEVC。那一刻我彻底明白面对一个不懂技术的接收方你没法要求他去装解码器、调整分离器、开启硬解。最稳妥的方案只有一个让视频变成最普遍的 H.264 / MP4 组合任何设备拿过去都能直接放。H.264 的兼容性好到什么程度2008 年之后的笔记本、各种智能电视、网页浏览器里的 video 标签基本全部支持。MP4 封装加 H.264 视频流几乎成了所有平台的默认标准。相比之下H.265 在浏览器和很多终端播放器上支持度依然参差不齐和微信、网盘、剪辑软件对接时最容易出问题。所以我把“转码”而不是“让所有人换播放器”定为最终方案。1.2 Python FFmpeg为什么这个搭配最合适转码这件事摆在面前的选项有三条图形化软件、直接命令行 FFmpeg、Python 封装 FFmpeg。图形化软件最简单但没法批量也没法做成自动化流程直接命令行 FFmpeg 其实已经很强大不过要处理几十个文件、还要只筛选出 HEVC 编码的视频、输出自定义文件名手敲命令能敲到崩溃。Python 在这里做的是“调度”扫描目录、调用 ffprobe 识别编码、筛选目标文件、构造 FFmpeg 命令、控制并发、记录日志。真正的转码引擎仍然是 FFmpeg。为什么不让 Python 直接处理像素数据理论上可以用 PyAV、imageio-ffmpeg 甚至 OpenCV 读帧再写视频但这样做的问题是你既要自己维护帧缓冲、又要处理音视频同步性能还比不过高度优化的 FFmpeg。在我这个项目里FFmpeg 承担了最核心的编解码工作而 Python 把这些操作串起来组装成可复用的工具。一句话总结调度的活交给 Python力气活交给 FFmpeg。2. 环境准备把工具链装好再动手2.1 Python 安装的常见坑视频转码脚本其实不需要太新潮的 Python 版本3.8 以上就够了核心用的是 subprocess、pathlib、json 这些标准库唯一推荐的第三方库是 tqdm用来显示进度条就算不装也不影响功能。所以我通常直接装官方原版 PythonWindows 安装包下载后第一个弹窗不急着点 Install Now先勾上 Add Python to PATH。这一条不勾后面在命令行敲 python 就会提示 python 不是内部或外部命令我帮同事装机遇到的绝大多数问题都出在这里。装完之后有个值得坚持的习惯建虚拟环境。项目单独用一个 venv可以避免和全局 Python 包冲突也防止换机器时到处补环境。我的做法python -m venv .venv # Windows PowerShell 或 CMD .venv\Scripts\activate # macOS / Linux source .venv/bin/activate激活后终端前面会出现 (.venv)后面直接用 pip 安装 tqdm 即可pip install tqdm2.2 FFmpeg全局可用才是真香FFmpeg 是整个工具的心脏安装方式跟系统有关。Windows 推荐从视频工具官网下载完整版预编译包解压后把 bin 目录路径加进系统环境变量 PATH。macOS 可以通过 Homebrew 装Linux 直接用包管理器装# macOS brew install ffmpeg # Ubuntu / Debian sudo apt install ffmpeg # CentOS / Fedora sudo dnf install ffmpeg安装完成之后在命令行里验证ffmpeg -version ffprobe -version能正常显示版本号说明环境没问题。这里我要重点提醒网上有一些精简版或在线安装器体积只有几 MB用起来很容易缺编码器。后面执行转码时只要报 Unknown encoder libx264十有八九是这类精简版的问题。我自己一直用完整版并且在项目 README 里就写明这一条省得后面同事踩坑。2.3 项目目录与脚本骨架我把脚本和目录保持简洁一般是这样video_transcoder/ ├── transcode.py ├── input/ ├── output/ │ └── done_list.txt └── logs/ └── transcode.loginput/ 放要处理的原始视频output/ 放转码结果logs/ 记运行日志。这个结构在 NAS 或者家用电脑上都很实用。我还习惯用 pathlib 的 Path 而不是字符串拼接路径因为 Windows 和 Linux 的路径分隔符不一样直接拼字符串很容易在跨平台时出各种奇怪问题。3. 核心转码逻辑识别、构造命令、控制参数3.1 用 ffprobe 识别 H.265 视频不能通过后缀名判断编码。一个叫 movie.mp4 的文件既可能是 H.264也可能是 H.265甚至可能是 MPEG-4。真正可靠的判断方式是拿 ffprobe 去读视频流信息。下面是我自己用的函数import json import subprocess from pathlib import Path def get_video_codec(path: Path): cmd [ ffprobe, -v, error, -select_streams, v:0, -show_entries, streamcodec_name, -of, json, str(path), ] r subprocess.run(cmd, capture_outputTrue, textTrue) if r.returncode ! 0: return None data json.loads(r.stdout) streams data.get(streams, []) return streams[0][codec_name] if streams else None-selecting_streams v:0 表示只取第一个视频流-show_entries streamcodec_name 只关心 codec_name。绝大多数视频文件只有一个视频流这样写足够用。函数返回的值如果是 hevc就说明是 H.265如果是 h264直接跳过。这里我还要提一个细节个别封装文件里的视频流可能标记为 h265 而不是 hevc所以我通常两种都判断一下避免漏掉。3.2 转码命令核心参数逐一拆解识别出 HEVC 后下一步是构造转码命令。我常用的模板是ffmpeg -y -i input.mp4 -map 0 -c:v libx264 -crf 20 -preset medium -c:a copy -c:s copy -movflags faststart output.mp4一行一行拆开看-y输出文件存在时直接覆盖。批量处理最怕停在“文件已存在是否覆盖”这种交互上加了它就不会卡住。-map 0把原文件里所有流全部映射到输出。没有这个参数时FFmpeg 可能会只取一个视频流和一个音频流导致多声道或字幕丢失。-c:v libx264视频编码器设为 H.264。-crf 20质量系数越小越清晰文件越大。-preset medium编码速度与压缩率平衡点。-c:a copy音频不重编码直接复制进入输出文件。-c:s copy字幕流直接复制。-movflags faststart将 MP4 的索引放到文件头方便在线播放。有人会问音频直接复制会不会导致其他设备不支持要看原音频是什么编码。很多下载的电影和相机素材音频可能是 AAC、AC3、DTS 等。AAC 复制没问题AC3 部分老设备可能不认DTS 更特殊。如果你明确知道接受方设备很旧可以改为 -c:a aac -b:a 192k把音频也转成最通用的 AAC。这个选项我建议做成可配置的后面脚本里会体现。3.3 CRF 和 preset参数怎么选才合适CRF 是新手最容易纠结的地方。可以把它理解成“心目中的质量目标”数字越低编码器投入的码率越高画面损失越小文件也越大。x264 的 CRF 范围是 0 到 51实际常用的是 18 到 28。我自己长期测试下来的结论想接近无损或者作为成片交付用 18 到 20。普通备份、网络分享用 20 到 23。手机上看个大概对体积敏感用 24 到 26。另一个参数 preset 控制的是压缩效率。预设越快编码越快但同码率下质量可能略差预设越慢编码越慢但理论上能在相近体积下保留更多细节。实际项目中medium 到 slow 之间的差距足够用了。我把默认值设为 medium同时支持命令行参数调整。至少我自己的经验里veryslow 那种大量耗时的设置绝大多数场合换不来肉眼可见的提升。还要注意码率控制。有时你想限定输出体积比如“转完以后不能超过 5GB”那就得改用固定码率模式比如 -b:v 8M -maxrate 10M -bufsize 16M。但视频场景复杂固定码率的效率不如 CRF所以我默认不放 -b:v把控制权交给 CRF。3.4 批量处理的安全设计断点续传和任务清单批量转码最大的敌人不是慢而是跑了一半崩溃。文件如果已经写坏还能重来最怕的是整个程序中断然后你已经不知道哪些文件转完了。我的办法是每个文件转码成功后往 done_list.txt 里写一行路径。下一次启动时扫描结果会和 done_list 做集合差只处理还没成功的文件。这样就天然支持中断恢复。另一个设计是“扫描清单”和“转码执行”分开。先扫一遍目录把所有 HEVC 文件列出来存成 JSON 快照再逐条处理。这样即使中途遇到磁盘满了或者某个文件损坏导致进程退出重启后仍可以从上次的进度继续跑。配合日志系统每天晚上睡前一查就能掌握进展。4. 完整实现可以拿来改的转码脚本4.1 目录扫描与编码检测下面是我在这个项目里使用的代码片段。第一步遍历 input 目录找出所有 HEVCdef scan_hevc_files(input_dir: Path): hevc_files [] for file_path in input_dir.rglob(*): # 递归遍历所有子目录 if file_path.suffix.lower() not in (.mkv, .mp4, .ts, .mov, .flv, .avi): continue codec get_video_codec(file_path) if codec in (hevc, h265): hevc_files.append(file_path) return hevc_filesrglob(*) 会把子目录里的文件也找出来很适合处理多层目录的 NAS 场景。后缀名单可以按需加。如果不先做后缀过滤每个文件都去跑一次 ffprobe文件数量大时稍微有点浪费但用于个人批量处理完全没问题。4.2 转码执行与结果校验转码函数我保留得尽量简单def transcode_one(src: Path, dst: Path, crf: int, preset: str): cmd [ ffmpeg, -y, -i, str(src), -map, 0, -c:v, libx264, -crf, str(crf), -preset, preset, -c:a, copy, -c:s, copy, -movflags, faststart, str(dst) ] r subprocess.run(cmd, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue) if r.returncode 0: check get_video_codec(dst) if check h264: return True else: return False else: # 留最后一部分报错信息方便排查 error_tail r.stderr[-2000:] if r.stderr else (no stderr) logging.error(转码失败 %s: %s, src.name, error_tail) return False注意里面的 get_video_codec(dst)转码完成之后再验一次编码防止 FFmpeg 中途异常但返回码仍为 0 的情况。虽然这种情况很少但多做一道校验总归稳妥。如果校验没通过我不会把它写进 done_list确保下次重跑时还会再试一次。4.3 并发还是串行我建议先串行再考虑并发很多教程一上来就推荐用 ThreadPoolExecutor 并发转码我不太建议新手这么做。FFmpeg 本身就是 CPU 密集型任务单线程已经能占满一个核心把 4 个转码线程塞进同一台机器即使 CPU 支持超线程磁盘和内存也会相互争抢最终总耗时反而可能变长。我的做法是默认串行最多提供一个 --workers 参数来控制并发数。即使用多线程也必须注意不同 FFmpeg 进程同时在转同一个文件会冲突输出目录如果共享要保证文件名不重复。下面是我用 ThreadPoolExecutor 的简化版本适合多核机器但并发数一般不要超过物理核心数的一半from concurrent.futures import ThreadPoolExecutor, as_completed def run_batch(tasks, workers1): with ThreadPoolExecutor(max_workersworkers) as pool: futures {pool.submit(transcode_entry, t): t for t in tasks} for future in as_completed(futures): task futures[future] try: ok, msg future.result() mark_done(task, ok) except Exception as exc: logging.error(队列任务异常 %s: %s, task, exc)主逻辑里就是 run_batch(tasks, args.workers)。并发线程需要正确地协调对同一份 done_list 的写入简单做法是处理完一个就追加一行。如果追求极致稳定串行足够用。4.4 日志记录与对比数据为了让转码结果直观我在日志里记录原始文件大小、输出文件大小和耗时。每次跑完一个文件控制台输出类似[OK] 源文件 movie_01.mkv(2.34 GB) - 输出 movie_01.mp4(1.12 GB) 耗时 328s 压缩比 52% [SKIP] movie_02.mp4 已经是 h264跳过这样看日志就知道每个文件到底有没有被正确转换。压缩比为负的情况也有可能出现少数高动态画面或复杂纹理场景转完反而会变大这时就该提醒自己调整 CRF。5. 常见问题与排查实录5.1 运行 ffmpeg 提示不是内部或外部命令环境变量没配好。Windows 上打开“高级系统设置 - 环境变量”把包含 ffmpeg.exe 的目录加进 PATH保存后重新开一个命令行窗口再执行 ffmpeg -version 验证。Linux 或 macOS 同理。最容易忽略的是修改 PATH 后旧终端窗口不会自动刷新要用新窗口。5.2 libx264 编码器缺失精简版的 FFmpeg 可能只包含少量编码器运行时会报 Unknown encoder libx264。先执行 ffmpeg -encoders 查询输出里有没有 libx264。没有就说明需要换完整版 FFmpeg。macOS 上用 brew 安装一般没问题Windows 就换一个完整的预编译包重新配一遍 PATH。5.3 转出来的文件没有声音最常见的原因是原视频音频流不止一条比如有多语言配音或评论音轨而我一开始只映射了第一条音频另一种情况是原音频编码特别copy 无损保留后播放器却不支持。解决方法有两种使用 -map 0 把所有流都保留下来或者选择音频转码例如统一转为 AAC ffmpeg -i input.mkv -map 0 -c:v libx264 -crf 20 -preset medium -c:a aac -b:a 192k -c:s copy output.mp4如果只想保留某一条音轨可以先用 ffprobe 查看流序号再用 -map 0:a:1 选择第二条音轨。5.4 中文文件名或路径乱码Windows 下经常表现为转码失败、文件找不到或者日志里路径变成乱码。原因是控制台编码与文件系统编码不一致。我在脚本开头加了一个容易忽略的细节使用 pathlib.Path 处理路径并尽量避免在控制台打印非 ASCII 字符日志保存时显式用 UTF-8 编码。如果已经出现乱码可以把命令行代码页切到 UTF-8或者改用 Path 完整操作路径而不是手动拼接字符串。5.5 转码时 CPU 很烫、内存占满批量转码本来就是重活。如果 8 核机器跑 8 个并发任务温度可能立刻飙到 90 度以上建议限制 --workers 2 或 4。内存占满可能是因为 FFmpeg 为了效率会预分配缓冲单个 1080p 文件通常不会太夸张但转 4K 或高码率素材时内存占用会明显上升这时可以降低并发或限制预设。5.6 播放器或者剪辑软件打不开转码后的文件转码完成后先自己验证一下用系统自带的播放器打开再用浏览器拖进去看能否在线播放。如果还是打不开第二步才考虑换播放器验证。但如果别的播放器能开说明工具转出来的文件本身没问题是当前设备的解码能力问题。此时可以再确认一下输出容器格式如果是 MKV、AVI部分旧软件可能不识别建议统一封装成 MP4。另外H.264 只是视频编码音频编解码也可能成为瓶颈确认音频格式是否为 AAC 或 MP3。5.7 怎么确认原视频是不是 HEVC有些人会被“拿到一个视频在播放器里黑屏”困住不确定是不是 H.265。在命令行里跑一遍 ffprobe看 codec_name 是不是 hevc 就能直接判断。如果输出文件能在别的播放器里正常播放也可以确认源文件本身没问题只是当前播放器缺少 HEVC 解码能力。这时候你再去用转码工具处理而不是去乱下载解码器。6. 经验补充与后续扩展6.1 GPU 硬件加速转码速度的明显提升如果你的电脑有 NVIDIA、AMD 或 Intel 核显转码时可以考虑硬件编码器。FFmpeg 里 NVIDIA 的 H.264 硬件编码器叫 h264_nvencIntel 核显一般用 h264_qsvAMD 是 h264_amf。硬件编码的优势是快4K 视频也能接近实时处理缺点是同等码率下画质可能略差一点文件体积往往比软件编码大比较适合“快速交付”场景不适合精细归档。我通常建议把编码器做成命令行参数默认 libx264可以用 --encoder h264_nvenc 切换。如果你经常转 4K可以试下 preset p7 或 -tune ll不同显卡的具体参数描述有些差异查 ffmpeg -h encoderh264_nvenc 即可。6.2 加入黑帧、花屏检测转码工具做久了最怕的不是失败而是“成功但结果有问题”。比如某一帧花屏、黑屏或者音画不同步。为了在交付前发现问题我在脚本里加了一个简单的检查任务转码完成后用 ffmpeg 截取几个关键帧转成小图目录人工快速翻看再对比源文件和输出文件的时长是否一致。虽然不是百分之百自动质检但至少能拦下很多低级错误。6.3 后续可以继续扩展的方向这个工具后续还有很多扩展空间接入 NAS 的文件监控新增视频时自动触发转码接到聊天机器人转发视频时自动按终端类型选择编码还可以转出两个版本一个高码率高画质一个低码率小体积方便快速预览。我现在实际使用的版本已经把这些功能拆成模块每次新增需求都比从零开始写要快很多。6.4 给后来者的一点建议我的体会是视频转码工具的最大价值不在于“把 H.265 变成 H.264”这一下而在于把重复操作自动化并且能够在出问题时快速定位到底哪一步挂了。做这类工具别一开始就想着加各种高端功能先把基础链路跑通再一步步增强。哪天你面对几百个文件夹的视频看到脚本能自动识别 HEVC、自动转码、自动写日志那种踏实感真不是手动点软件能比的。遇到问题也别慌说到底只是 FFmpeg 命令和 Python 流程的组合调试起来比自己想象中简单。