☰
Python+FFmpeg批量实现H.265转H.264,解决视频兼容性问题
2026/9/28 7:19:26 网站建设 项目流程

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,用来显示进度条,就算不装也不影响功能。所以我通常直接装官方原版 Python,Windows 安装包下载后,第一个弹窗不急着点 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 tqdm

2.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.log

input/ 放要处理的原始视频,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", "stream=codec_name", "-of", "json", str(path), ] r = subprocess.run(cmd, capture_output=True, text=True) 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 stream=codec_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 目录,找出所有 HEVC:

def 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_files

rglob("*") 会把子目录里的文件也找出来,很适合处理多层目录的 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, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 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, workers=1): with ThreadPoolExecutor(max_workers=workers) 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_nvenc,Intel 核显一般用 h264_qsv,AMD 是 h264_amf。硬件编码的优势是快,4K 视频也能接近实时处理;缺点是同等码率下画质可能略差一点,文件体积往往比软件编码大,比较适合“快速交付”场景,不适合精细归档。

我通常建议把编码器做成命令行参数:默认 libx264,可以用 --encoder h264_nvenc 切换。如果你经常转 4K,可以试下 preset p7 或 -tune ll,不同显卡的具体参数描述有些差异,查 ffmpeg -h encoder=h264_nvenc 即可。

6.2 加入黑帧、花屏检测

转码工具做久了,最怕的不是失败,而是“成功但结果有问题”。比如某一帧花屏、黑屏,或者音画不同步。为了在交付前发现问题,我在脚本里加了一个简单的检查任务:转码完成后,用 ffmpeg 截取几个关键帧,转成小图目录,人工快速翻看;再对比源文件和输出文件的时长是否一致。虽然不是百分之百自动质检,但至少能拦下很多低级错误。

6.3 后续可以继续扩展的方向

这个工具后续还有很多扩展空间:接入 NAS 的文件监控,新增视频时自动触发转码;接到聊天机器人,转发视频时自动按终端类型选择编码;还可以转出两个版本,一个高码率高画质,一个低码率小体积,方便快速预览。我现在实际使用的版本已经把这些功能拆成模块,每次新增需求都比从零开始写要快很多。

6.4 给后来者的一点建议

我的体会是:视频转码工具的最大价值,不在于“把 H.265 变成 H.264”这一下,而在于把重复操作自动化,并且能够在出问题时快速定位到底哪一步挂了。做这类工具别一开始就想着加各种高端功能,先把基础链路跑通,再一步步增强。哪天你面对几百个文件夹的视频,看到脚本能自动识别 HEVC、自动转码、自动写日志,那种踏实感,真不是手动点软件能比的。遇到问题也别慌,说到底只是 FFmpeg 命令和 Python 流程的组合,调试起来比自己想象中简单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询