有次交付一批视频给客户,对方反馈"手机上看不了"。我在电脑上用 VLC、PotPlayer、浏览器各试了一遍——全部正常。这让排查变得很困难,因为"在我这儿是好的"。
后来我把 ffprobe 的所有输出都打印出来,一项项去对照目标设备的解码能力,才找到原因:我们用了 10bit HEVC(yuv420p10le)。电脑上的软件解码器全支持,但客户那边几款老安卓手机和一台智能电视的硬件解码器只支持 8bit HEVC,播出来是黑屏或者干脆报错。
从那以后我做视频交付前会过一遍兼容性检查。这篇写完整的排查流程:profile、level、像素格式、codec tag、moov 位置、音频格式,每一项都有设备"不支持就翻脸"的例子,最后给一份最保守的参数模板(以及什么时候该放弃保守)。
TL;DR:排查顺序是 编码格式 → profile/level → 像素格式(8bit/10bit) → 分辨率帧率 → 容器与 codec tag → 音频格式 → moov 位置。最常见的三个杀手:10bit(老硬件解码器不支持)、HEVC/H.265(支持度碎片化,iOS 需要
hvc1tag)、profile/level 超出设备能力。最保守的通用配置:H.264 + high profile + level 4.0/4.1 + yuv420p(8bit) + AAC-LC 48kHz +-movflags +faststart。真正管用的验证方法是在目标设备上真机播放,而不是在电脑上看。
目录
- 一、先看一个"播不了"的完整排查表
- 二、编码格式:H.264 仍然是最大公约数
- 三、profile 与 level:设备的"能力上限声明"
- 四、像素格式:10bit 是头号杀手
- 五、分辨率与帧率的上限
- 六、容器、codec tag 与 moov 位置
- 七、音频:被忽略的那一路
- 八、诊断流程:拿着 ffprobe 输出对照
- 九、参数模板:保守 vs 现代
- 十、坑清单
一、先看一个"播不了"的完整排查表
这是我现在的排查清单(按顺序,从最常见到最少见):
| 排查项 | 怎么看 | 不支持的表现 |
|---|---|---|
| 编码格式 | codec_name |
黑屏 / 只有声音 / 直接报错 |
| profile | profile |
部分设备拒绝解码 |
| level | level |
同上(分辨率/帧率/码率超限) |
| 像素格式 | pix_fmt |
黑屏(10bit 最常见) |
| 分辨率/帧率 | width/height/avg_frame_rate |
卡顿 / 播不了 |
| 容器 tag | codec_tag_string |
iOS/QuickTime 打不开 |
| moov 位置 | ffprobe 能否秒开 |
播放前要下载完整个文件 |
| 音频编码 | codec_name(audio) |
没声音 / 整体播放失败 |
| 采样率 | sample_rate |
少数设备无声 |
二、编码格式:H.264 仍然是最大公约数
| 编码 | 支持度 | 说明 |
|---|---|---|
| H.264 / AVC | 几乎 100% | 过去十五年所有设备都支持,包括很老的 |
| H.265 / HEVC | 约 60~80% | 2015 年前的手机/电视基本不支持硬件解码;iOS/macOS 支持好但要求特定 tag |
| VP9 | 浏览器好,硬件差 | YouTube 大量使用;安卓支持好,iOS Safari 从 14 开始支持(部分) |
| AV1 | 新设备 | 2020 年后的硬件,老设备只能软解(慢、发热) |
我的默认选择:
- 要发给别人看的成片 → H.264(除非确认对方设备都支持 H.265);
- 自己归档/内部使用 → H.265(省空间);
- 网页播放 → H.264 为主,可以额外提供 VP9/AV1 档(多码率 ABR)。
一个真实的教训:我曾经给客户全量转成了 H.265(省了 40% 空间,客户很高兴),结果他们把视频发给终端用户,一部分用户打不开。最后又重新出了一份 H.264 版本。省下的存储成本,抵不过一次返工和客户的信任损失。
判断对方能不能播 H.265,最实用的办法是问清楚他们的最低端设备是什么。如果答案里有"老安卓盒子""几年前的电视""某些客户的旧手机",就老老实实用 H.264。
三、profile 与 level:设备的"能力上限声明"
profile 是"用了哪些编码工具",level 是"多大规模"(分辨率 × 帧率 × 码率的组合上限)。
H.264 的 profile
| profile | 特性 | 支持度 |
|---|---|---|
| baseline | 无 B 帧、无 CABAC | 最老的设备(视频会议、老手机) |
| main | 有 B 帧、CABAC | 绝大部分设备 |
| high | 加 8×8 变换、自定义量化 | 2009 年后的设备基本都支持 |
| high10 / high422 / high444 | 10bit / 4:2:2 / 4:4:4 | 硬件解码支持很差 |
结论:
- 最大兼容 →
main(甚至baseline,如果你要给非常老的设备); - 常规使用 →
high(压缩率比 main 好 10~15%,兼容性依然很好); - 千万别用 high10/high422 做交付(除非确认目标支持)。
-profile:v high
H.264 的 level
level 限制了"解码器要处理多大的数据"。常见的对应关系(以官方规格书为准,这里给实用的近似):
| level | 典型支持 | 最大码率(High profile) |
|---|---|---|
| 3.0 | 720×480@30 | 10 Mbps |
| 3.1 | 1280×720@30 | 14 Mbps |
| 4.0 | 1920×1080@30 | 20 Mbps |
| 4.1 | 1920×1080@30 / 1280×720@60 | 50 Mbps |
| 4.2 | 1920×1080@60 | 50 Mbps |
| 5.0 | 2560×1920@30 | 135 Mbps |
| 5.1 | 3840×2160@30 | 240 Mbps |
| 5.2 | 3840×2160@60 | 240 Mbps |
注意 level 是"能力要求":一个标了 level 5.1 的视频,只支持 4.1 的设备会直接拒绝播放(即使分辨率只有 720p)。
所以我们有个反直觉的优化:如果你把 720p 视频编码时让它用了 level 5.1(ffmpeg 默认会根据分辨率/帧率/码率自动选,有时偏保守地选高),老设备就播不了。显式指定 level 可以避免这个问题:
-level:v 4.1
常见错误:4K 视频忘了指定 level,ffmpeg 自动用了 5.1,结果某些只支持 5.0 的设备播不了。实际上 4K@30 用 5.0 也够(5.0 支持 3840×2160@?)。这种情况我一般会显式指定并实测。
HEVC 的 level 体系类似,但多了 tier(Main tier / High tier),High tier 允许更高码率。老设备的 HEVC 硬件解码通常只支持 Main tier 到 level 4.1(也就是 1080p@30 左右)。
四、像素格式:10bit 是头号杀手
这是"电脑能播、手机不能播"最常见的原因。
| pix_fmt | 位深 | 色度采样 | 兼容性 |
|---|---|---|---|
yuv420p |
8bit | 4:2:0 | 最好,几乎所有设备 |
yuv420p10le |
10bit | 4:2:0 | 电脑软件解码 OK;老硬件解码器不支持 |
yuv422p |
8bit | 4:2:2 | 硬件解码支持很差 |
yuv444p |
8bit | 4:4:4 | 同上,更差 |
10bit 的收益(前面讲过):减少色带(banding),渐变天空这类场景明显,同样码率下 VMAF 略高。
10bit 的代价:
- 老安卓、老电视的硬件解码器不支持 → 黑屏或者只有声音;
- 部分浏览器的 HEVC 硬解不支持 10bit;
- 编码耗时 +15% 左右。
我的规则:
母版 / 归档 → 10bit(画质优先,自己用)
对外交付 / 移动端播放 → 8bit(兼容优先)
怎么检查:
ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt -of csv=p=0 out.mp4
# yuv420p10le ← 如果要发给移动端,这就是问题
转成 8bit:
ffmpeg -i in.mp4 -c:v libx264 -crf 20 -pix_fmt yuv420p -c:a copy out_8bit.mp4
注意:从 10bit 转 8bit 是有损的(会重新编码),所以母版留 10bit,交付时再转,别一开始就用 8bit 编码(否则想出 10bit 版就得重来)。
五、分辨率与帧率的上限
设备的解码能力有硬上限,超过就播不了(或者极卡):
| 设备类型 | 典型上限 |
|---|---|
| 老安卓手机(2015 前) | 1080p@30(H.264 high 都勉强) |
| 中端安卓(2018 后) | 4K@30 H.264 / 1080p@60 HEVC |
| iPhone(2017 后) | 4K@60 HEVC |
| 智能电视/盒子 | 差异巨大,便宜的只到 1080p@30 |
| 微信内置播放器 | 有分辨率和码率限制(且会二次转码) |
两个常见坑:
1. 高帧率(60fps)比高分辨率更容易出问题。很多设备能播 4K@30 但播不了 1080p@60(因为后者对解码吞吐要求更高)。给不确定设备的内容,30fps 更安全。
2. 奇数分辨率或非常规尺寸。有些硬件解码器要求宽高是偶数(甚至 16 的倍数)。用 -vf scale=1280:-2 保证偶数:
-vf "scale=1280:-2" # -2 表示自动计算且保证是偶数
-2 而不是 -1:-1 只保证比例,-2 保证偶数。遇到 "width not divisible by 2" 的报错就用这个。
六、容器、codec tag 与 moov 位置
codec tag:iOS 的隐形门槛
MP4 文件里,H.265 有两种 tag:
hev1:标准写法;hvc1:Apple 要求的写法。
iOS/macOS 的 QuickTime 和 Safari 只认 hvc1。ffmpeg 默认写 hev1,导致 HEVC 视频在 iPhone 上打不开(在安卓和电脑上正常——又是这个剧情)。
# 转成 iOS 能认的 tag(不重新编码,秒完成)
ffmpeg -i in.mp4 -c copy -tag:v hvc1 out_ios.mp4
H.264 也有类似情况(avc1 是标准,一般没问题)。
moov 位置:影响"能否边下边播"
MP4 的 moov box 是索引。如果它在文件末尾(ffmpeg 默认编码后就在末尾),播放器必须先下载完整个文件才能开始播放——对本地文件无所谓,对网络播放是致命的(用户要等几秒甚至几分钟)。
# 把 moov 挪到开头
ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4
这个参数我每次交付都加,成本为零(只重写容器,不重新编码),收益是网页/移动端能秒开。
检查:
# 看 moov 在哪(用 python 读 box 顺序,或者简单判断:能否快速起播)
ffprobe -v error -show_entries format=start_time,duration -of csv=p=0 out.mp4
更直接的办法:用浏览器打开,看是不是要等很久才开始播。
容器选择
| 容器 | 兼容 | 说明 |
|---|---|---|
| MP4 | 最好 | 万能选择 |
| MKV | 差(移动端) | 电脑播放器很好,电视/手机经常不认 |
| WebM | 浏览器 | VP9/AV1 用 |
| TS | 流媒体 | HLS 用 |
交付一律用 MP4(除非是 HLS 分发)。MKV 虽然功能强(多音轨、内挂字体、章节),但移动端支持度差太多。
七、音频:被忽略的那一路
视频播不了,有时候是音频的锅。
| 音频格式 | 兼容性 | 说明 |
|---|---|---|
| AAC-LC | 最好 | 万能选择 |
| HE-AAC (AAC+) | 中等 | 部分老设备不支持,且音质一般 |
| MP3 | 很好 | MP4 容器里也能放 |
| Opus | 差(MP4 里) | WebM 里很好,MP4 里支持有限 |
| AC-3 / E-AC-3 | 中等 | 电视/影院好,手机一般 |
| FLAC / PCM | 差 | 无损,但很多播放器不支持 |
# 明确指定 AAC-LC(不要用 HE-AAC)
-c:a aac -profile:a aac_low -ar 48000 -b:a 128k
-profile:a aac_low 很重要:ffmpeg 的 aac 编码器默认是 LC,但如果你用了某些参数或者编码器版本不同,可能输出 HE-AAC(-profile:a aac_he),老设备播不了或者音质怪。
采样率:44100 和 48000 都安全。有些设备对 32000 以下的低采样率或者 96000 的高采样率有问题。统一用 48000(视频的标准采样率)。
声道:立体声(2.0)最安全。5.1 在某些设备上会被降混或者无声。
八、诊断流程:拿着 ffprobe 输出对照
我现在的标准动作(拿到一个"播不了"的文件):
ffprobe -v error -show_format -show_streams -of json out.mp4 | jq '.streams[] | {
codec_type, codec_name, codec_tag_string, profile, level,
width, height, pix_fmt, avg_frame_rate, sample_rate, channels
}'
输出示例(有问题的那份):
{
"codec_type": "video",
"codec_name": "hevc",
"codec_tag_string": "hev1", ← 问题 1:iOS 需要 hvc1
"profile": "Main 10", ← 问题 2:10bit
"level": 120, ← level 4.0(120 = 4.0×30)
"width": 1920, "height": 1080,
"pix_fmt": "yuv420p10le", ← 问题 3:10bit 实锤
"avg_frame_rate": "30/1"
}
三个问题叠加,难怪老设备播不了。修复:
ffmpeg -i in.mp4 \
-c:v libx264 -profile:v high -level 4.1 -crf 20 -preset slow \
-pix_fmt yuv420p -vf "scale=1920:-2" \
-c:a aac -profile:a aac_low -ar 48000 -b:a 128k \
-movflags +faststart \
-tag:v avc1 \
out_compatible.mp4
(H.264 的场景下 tag 一般不用管,avc1 是默认的。)
level 的数字含义:ffprobe 输出的是编码值(比如 120),H.264 的 level 换算为 值 / 30(120 / 30 = 4.0)。HEVC 是 值 / 30(比如 120 = 4.0)。这个换算我每次都要查,干脆记在这里。
九、参数模板:保守 vs 现代
模板 A:最保守(要发给未知设备)
ffmpeg -i input.mp4 \
-c:v libx264 -profile:v main -level 3.1 \
-crf 23 -preset slow \
-pix_fmt yuv420p -vf "scale=1280:-2,fps=30" \
-g 60 -keyint_min 60 \
-c:a aac -profile:a aac_low -ar 44100 -b:a 128k -ac 2 \
-movflags +faststart \
output_safe.mp4
适用:给客户/终端用户的交付、要在微信里传播、不明确目标设备。
代价:720p@30 + main profile,画质和体积都不是最优,但几乎不可能播不了。
模板 B:常规(1080p,现代设备)
ffmpeg -i input.mp4 \
-c:v libx264 -profile:v high -level 4.1 \
-crf 20 -preset slow \
-pix_fmt yuv420p -vf "scale=1920:-2" \
-c:a aac -profile:a aac_low -ar 48000 -b:a 128k \
-movflags +faststart \
output_1080p.mp4
适用:大多数场景(网页播放、手机播放、近几年设备)。
模板 C:HEVC(内部归档/已知支持)
ffmpeg -i input.mp4 \
-c:v libx265 -profile:v main -level 4.1 \
-crf 26 -preset slow -pix_fmt yuv420p \
-tag:v hvc1 \
-c:a aac -profile:a aac_low -ar 48000 -b:a 128k \
-movflags +faststart \
output_hevc.mp4
-tag:v hvc1 保证 iOS 能播;8bit + main profile 保证老硬件也能硬解。
模板 D:10bit 母版(自己用,不对外)
ffmpeg -i input.mp4 \
-c:v libx265 -crf 19 -preset slow \
-pix_fmt yuv420p10le \
-x265-params "psy-rd=2.0:aq-mode=3" \
-c:a copy \
master_10bit.mp4
决策表:
| 场景 | 编码 | profile/level | pix_fmt | 容器/tag |
|---|---|---|---|---|
| 未知设备交付 | H.264 | main / 3.1 | yuv420p | MP4 + faststart |
| 常规网页/手机 | H.264 | high / 4.1 | yuv420p | MP4 + faststart |
| 已知支持 H.265 | H.265 | main / 4.1 | yuv420p | MP4 + hvc1 |
| 内部母版 | H.265 | main10 | yuv420p10le | MKV/MP4 |
| HLS 分发 | H.264 多档 | high / 4.1 | yuv420p | TS + master.m3u8 |
十、坑清单
- 用 10bit 交付给移动端 → 黑屏。对外交付一律 8bit。
- HEVC 用默认 tag → iOS 打不开。加
-tag:v hvc1。 - 没加
+faststart→ 网页播放要下载完整文件才能开始。 - 用 high10/high422 profile → 硬件解码不支持。
- 不指定 level,让 ffmpeg 自动选到 5.1 → 只支持 4.1 的老设备拒绝播放。
- HE-AAC 音频 → 部分设备无声或音质差。用
-profile:a aac_low。 - 采样率奇怪(22050、96000)→ 少数设备不支持。统一 44100 或 48000。
- 奇数分辨率 → 编码器报错或播放异常。用
scale=W:-2。 - 60fps 发给老设备 → 播不了或者极卡。不确定就降到 30fps。
- MKV 交付 → 移动端/电视经常不认。用 MP4。
- 只在电脑上验证 → 电脑上软件解码器什么都支持。必须在目标设备真机测。
- 用 VLC 能播就以为没问题 → VLC 自带解码器,不代表系统播放器能播。要用系统原生播放器测。
- 微信里播不了 → 微信有自己的转码和限制(分辨率、码率、时长)。要测就在微信里真发一次。
- 忽略音频 → 视频没问题但没声音,用户也认为"播不了"。
- 多音轨/多字幕 → 部分播放器只认第一条,或者干脆不显示。交付时只保留一条默认音轨。
- 颜色和亮度看着不对 → 色彩空间/范围(tv vs pc range)问题,可以看项目里色彩空间那篇。
最后说说"验证"这件事。
我现在的规矩是:交付前,在至少两个真实的目标设备上播放(优先选客户群里最低端的那个设备)。电脑上的播放器(VLC、PotPlayer)自带全套软件解码器,它们能播完全不能说明任何问题——VLC 能播 10bit HEVC 4:4:4 的怪东西,但用户的手机不行。
用系统原生播放器测:iOS 用"照片"或者 Safari,安卓用系统相册/自带播放器,电视就用电视自己的播放器。这才是真实体验。
还有一条经验:问客户要"最低端设备型号",然后按它来设计参数。听起来很土,但这比任何"兼容性最佳实践"都可靠。因为兼容性问题不是"平均水平"的问题,是短板的问题——你按中端设备做,那 5% 的老设备用户就会来投诉,而他们往往是声音最大的那一群。
我们现在的交付文档里会写一行"本视频编码参数:H.264 high@4.1 / yuv420p 8bit / AAC-LC 48kHz / 已开启 faststart,最低支持设备:iPhone 6 及以上、Android 5.0 及以上"。这行字很值钱——它既是承诺,也是出问题时界定责任的依据。