1. 为什么流媒体转码绕不开 FFmpeg 与 DASH/HLS
做音视频这行的朋友,迟早会碰到一个需求:把手里的一堆 MP4、MOV、MKV 之类的文件,转成能在网页、App、机顶盒上流畅播放的流媒体格式。这时候绕不开两个关键词——HLS和DASH,也绕不开一个工具——FFmpeg。
我最早接触这块是帮一个做在线教育的小团队处理课程视频。他们原本的做法是直接扔 MP4 到服务器,用户点开就整文件下载,结果一个 40 分钟的视频,进度条拖一下要等半天,移动网络下更是卡到没法看。后来改成 HLS 切片,体验立刻不一样了:起播快、能自适应码率、拖动响应也快。这套流程的核心工具就是 FFmpeg,它几乎能把你听说过的所有封装格式和编码格式互相转换,而且命令行就能搞定,特别适合做自动化批处理。
这篇文章我想聊的是:怎么用 FFmpeg 把普通视频转成 HLS 和 DASH 格式,包括单码率、多码率自适应、切片参数怎么定、常见坑怎么排。适合两类人看:一是刚接触流媒体、想快速跑通一条转码流水线的开发者;二是已经在用 FFmpeg 但总被切片时长、码率阶梯、播放兼容性这些问题困扰的运维或后端同学。我不打算堆一堆理论,而是按我实际踩过的路子,把每一步为什么这么做讲清楚,你照着改改参数就能用。
先说清楚一个前提:HLS 和 DASH 本质上都是基于 HTTP 的自适应流媒体协议。它们把一整个视频切成很多小片段(segment),再配一个索引文件(manifest/playlist)告诉播放器每个片段在哪、什么码率。播放器根据当前网速自动选择合适码率的片段,网速好就切高清,网速差就切低清,这就是所谓的 ABR(自适应码率)。FFmpeg 在这中间扮演的角色,就是把源文件切成符合协议规范的片段和索引。
2. HLS 与 DASH 的核心差异与选型思路
2.1 两种协议到底差在哪
很多人一上来就问“我该用 HLS 还是 DASH”,其实这个问题得看你的播放端。我先把两者的关键区别摆出来,你对照自己的场景选。
| 对比维度 | HLS | DASH |
|---|---|---|
| 索引文件 | m3u8 播放列表 | MPD 清单文件 |
| 切片格式 | 通常是 TS,也支持 fMP4 | 通常是 fMP4 |
| 原生支持 | Safari、iOS、部分安卓原生支持 | 需要 dash.js 等播放器库 |
| 延迟表现 | 传统较高,低延迟方案在改善 | 天生更适合低延迟 |
| 编码标准 | 以 H.264/H.265 为主 | 编解码支持更开放 |
| 生态成熟度 | 非常成熟,兼容性最好 | 稍逊,但标准化程度高 |
简单说,HLS 胜在兼容性,DASH 胜在开放性和低延迟潜力。如果你的用户里有大量 iOS 设备,HLS 基本是必选项,因为 Safari 和 iOS 原生播放器对 HLS 支持最好。如果你做的是自己的 App 或网页,用 dash.js、shaka-player 这类库,那 DASH 也完全没问题。
2.2 我的选型经验
实际项目里,我一般这么决策:
- 面向大众、要最大兼容性:优先 HLS,必要时 HLS + DASH 双协议输出。
- 追求低延迟直播:DASH 或 LL-HLS(低延迟 HLS),切片时长压到 1-2 秒。
- 纯点播、自有播放器:DASH 更灵活,尤其是需要多音轨、多字幕时。
- 存储和带宽敏感:两者都支持多码率,但要注意切片格式,fMP4 通常比 TS 更省空间。
提示:HLS 用 TS 切片时,每个切片都带 PAT/PMT 表,有一定冗余开销。如果对存储敏感,可以考虑 HLS 也用 fMP4 切片,但要注意老设备兼容性。
这里有个容易被忽略的点:HLS 和 DASH 并不是互斥的。很多生产环境会同时输出两套,让播放器根据能力自己选。FFmpeg 完全支持一次转码、多路输出,后面我会给具体命令。
3. 转码前的准备工作与参数规划
3.1 环境与工具确认
动手之前,先把环境理清楚。FFmpeg 的安装方式很多,我建议直接用系统包管理器或者官方静态编译版本,避免自己编译踩依赖坑。
# 确认 FFmpeg 是否可用及版本 ffmpeg -version # 查看是否支持需要的编码器(比如 libx264、aac) ffmpeg -encoders | grep -E "libx264|aac"版本上,我建议用 4.4 以上的版本,对 fMP4、DASH 的支持更完善。如果你要用硬件加速(比如某些 GPU 编码),还得确认编译时带了对应的编码器。
3.2 源文件信息探查
转码前一定要先看源文件,别上来就转。用ffprobe看分辨率、码率、帧率、音轨情况:
ffprobe -v error -show_format -show_streams input.mp4重点看几个东西:分辨率(决定你要不要缩放)、原始码率(决定码率阶梯怎么定)、帧率(决定 GOP 设置)、音轨数量和编码(决定音频怎么处理)。我见过有人直接拿 4K 源去转 360p,结果没加缩放参数,转出来还是 4K,白白浪费算力。
3.3 码率阶梯怎么定
多码率自适应流的灵魂是码率阶梯(bitrate ladder)。定得好,用户切换顺滑;定得差,要么浪费带宽,要么切换时画质跳变明显。
我的经验是,阶梯不要太多,3-5 档足够。档位之间码率大致按 1.5-2 倍递增,分辨率按常见档位走。下面是一个我常用的参考表:
| 档位 | 分辨率 | 视频码率 | 音频码率 | 适用场景 |
|---|---|---|---|---|
| 低 | 426x240 | 300 kbps | 64 kbps | 弱网、移动 |
| 中 | 640x360 | 800 kbps | 96 kbps | 普通移动 |
| 较高 | 1280x720 | 2500 kbps | 128 kbps | 宽带、桌面 |
| 高 | 1920x1080 | 5000 kbps | 128 kbps | 高清需求 |
这些数字不是死的,得根据你的内容类型调。比如动画类内容压缩率高,码率可以低一些;体育、动作类画面复杂,码率要给足。我一般会先转一版,实际看看画质和文件大小,再微调。
注意:码率阶梯里,相邻档位的分辨率最好是整数倍关系(比如 360p 和 720p),这样播放器切换时缩放更自然,也方便某些硬件解码器处理。
4. 单码率 HLS 转码实操
4.1 最基础的 HLS 切片命令
先从最简单的开始,把 MP4 转成单码率 HLS:
ffmpeg -i input.mp4 \ -c:v libx264 -preset medium -crf 23 \ -c:a aac -b:a 128k \ -f hls \ -hls_time 6 \ -hls_playlist_type vod \ -hls_segment_filename "output/segment_%03d.ts" \ output/index.m3u8逐段解释一下:
-c:v libx264:视频用 H.264 编码,兼容性最好。-preset medium:编码速度与压缩率的平衡点,追求速度用 faster,追求体积用 slow。-crf 23:恒定质量模式,数值越小画质越好、文件越大,23 是常用默认值。-c:a aac -b:a 128k:音频转 AAC,128kbps。-f hls:输出格式指定为 HLS。-hls_time 6:每个切片 6 秒。-hls_playlist_type vod:点播类型,播放列表里会保留所有切片。-hls_segment_filename:切片文件命名规则。
跑完之后,output目录里会有一堆.ts文件和一个index.m3u8。用支持 HLS 的播放器打开 m3u8 就能播。
4.2 切片时长怎么选
-hls_time这个参数很关键。切片太短,文件数量多、请求频繁、索引文件大;切片太长,起播慢、切换码率迟钝。
我的经验值:
- 点播:6-10 秒比较稳妥,兼顾起播速度和请求开销。
- 直播:2-4 秒,追求更低延迟可以到 1 秒,但对服务器和网络要求高。
- 低延迟场景:配合 LL-HLS 的 partial segment,可以做到 1 秒以内。
还有一个坑:实际切片时长不一定等于你设的值。FFmpeg 会在关键帧(I 帧)处切,所以如果 GOP 是 2 秒,你设 6 秒,实际可能切在 6 秒附近的关键帧上。要精确控制,得同时设置 GOP:
-g 48 -keyint_min 48 -sc_threshold 0假设帧率 24,-g 48就是每 48 帧一个关键帧,即 2 秒一个 I 帧。-sc_threshold 0禁用场景切换插入关键帧,保证 GOP 规整。
4.3 音频单独处理的情况
有时候源文件音轨有问题,比如采样率不标准、多音轨。我一般显式指定音频参数:
-c:a aac -ar 48000 -ac 2 -b:a 128k-ar 48000统一采样率,-ac 2转立体声。如果源是 5.1 声道,直接转立体声能避免很多播放兼容问题。
5. 多码率自适应流的完整实现
5.1 为什么要做多码率
单码率 HLS 只是入门。真正上线,用户网络环境千差万别,必须做多码率自适应。原理是:同一份内容转出多个码率版本,每个版本单独切片,然后一个 master playlist 把它们串起来,播放器根据网速选。
5.2 用 filter_complex 一次转多路
FFmpeg 可以用一个命令同时输出多个码率,核心是filter_complex做分流和缩放:
ffmpeg -i input.mp4 \ -filter_complex \ "[0:v]split=3[v1][v2][v3]; \ [v1]scale=w=426:h=240[v1out]; \ [v2]scale=w=1280:h=720[v2out]; \ [v3]scale=w=1920:h=1080[v3out]" \ -map "[v1out]" -c:v:0 libx264 -b:v:0 300k -preset medium \ -map "[v2out]" -c:v:1 libx264 -b:v:1 2500k -preset medium \ -map "[v3out]" -c:v:2 libx264 -b:v:2 5000k -preset medium \ -map a:0 -c:a aac -b:a 128k \ -f hls \ -hls_time 6 \ -hls_playlist_type vod \ -master_pl_name master.m3u8 \ -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" \ -hls_segment_filename "output/v%v/segment_%03d.ts" \ "output/v%v/index.m3u8"这段命令信息量比较大,我拆开讲:
split=3把视频流复制成三路,分别缩放。-map把每路视频映射到不同的输出流,-b:v:0这种带索引的写法指定每路的码率。-master_pl_name master.m3u8生成主播放列表。-var_stream_map定义每个 variant 包含哪些流,v:0,a:0表示第一路视频配第一路音频。-hls_segment_filename里的%v会替换成 variant 编号,自动分目录。
跑完你会得到v0、v1、v2三个目录,每个里面有切片和 index.m3u8,根目录有 master.m3u8。播放器打开 master 就能自适应。
5.3 音频复用的小技巧
上面命令里音频只编码了一次,被三个 variant 共用。这是常见做法,能省算力。但如果你的场景需要每个码率配不同音质,也可以分别编码。我一般点播场景共用一份音频就够了,因为音频码率占比小,共用不影响体验。
提示:
-var_stream_map的写法容易出错,尤其是多音轨时。建议先用单音轨跑通,再逐步加复杂度。报错时重点看 FFmpeg 提示的 stream map 不匹配信息。
5.4 DASH 多码率输出
DASH 的命令结构和 HLS 类似,只是输出格式和参数不同:
ffmpeg -i input.mp4 \ -filter_complex \ "[0:v]split=3[v1][v2][v3]; \ [v1]scale=w=426:h=240[v1out]; \ [v2]scale=w=1280:h=720[v2out]; \ [v3]scale=w=1920:h=1080[v3out]" \ -map "[v1out]" -c:v:0 libx264 -b:v:0 300k \ -map "[v2out]" -c:v:1 libx264 -b:v:1 2500k \ -map "[v3out]" -c:v:2 libx264 -b:v:2 5000k \ -map a:0 -c:a aac -b:a 128k \ -f dash \ -seg_duration 6 \ -use_template 1 \ -use_timeline 1 \ -adaptation_sets "id=0,streams=v id=1,streams=a" \ output/manifest.mpd关键参数:
-seg_duration 6:切片时长,对应 HLS 的 hls_time。-use_template 1:用模板方式生成切片 URL,索引文件更简洁。-use_timeline 1:生成时间线信息,播放器定位更准。-adaptation_sets:定义自适应集,视频一组、音频一组。
DASH 默认输出 fMP4 切片(.m4s),比 TS 更现代,也更省空间。
6. 常见问题排查与避坑经验
6.1 播放器打不开或黑屏
这是最常见的问题。排查顺序我一般这么走:
- 先确认切片文件生成了没有。如果目录里只有 m3u8 没有 ts/m4s,说明切片环节就失败了。
- 看 m3u8 内容。用文本编辑器打开,检查切片路径对不对、时长信息是否正常。
- 确认编码格式。H.264 + AAC 是兼容性最好的组合,如果你用了 H.265,很多浏览器播不了。
- 检查跨域。网页播放时,m3u8 和切片的 HTTP 响应头要允许跨域,否则播放器拿不到数据。
6.2 切片时长和预期不符
前面提过,切片在关键帧处切。如果你发现切片时长忽长忽短,八成是 GOP 不规整。解决办法就是显式设置-g、-keyint_min、-sc_threshold 0,让关键帧间隔固定。
6.3 音画不同步
多码率转码时,音画不同步通常是因为音频和视频的时间基不一致,或者切片边界没对齐。我的处理办法:
- 加
-vsync cfr强制恒定帧率。 - 音频加
-async 1做时间戳校正。 - 确保所有 variant 用同一份音频,减少对齐问题。
6.4 转码速度太慢
多码率转码是算力大户。几个提速思路:
- 用
-preset faster或veryfast,牺牲一点压缩率换速度。 - 用硬件编码,比如某些 GPU 的 H.264 编码器,速度能快好几倍。
- 把多码率拆成多个 FFmpeg 进程并行跑,充分利用多核。
下面这张表是我整理的常见问题速查:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 播放黑屏 | 编码不兼容 | 改用 H.264 + AAC |
| 切片缺失 | 路径或权限问题 | 检查输出目录写权限 |
| 时长异常 | GOP 不规整 | 固定关键帧间隔 |
| 音画不同步 | 时间基不一致 | 加 vsync/async 校正 |
| 转码慢 | 算力不足 | 降 preset 或上硬件编码 |
| 切换卡顿 | 码率阶梯不合理 | 调整档位和分辨率 |
6.5 几个我踩过的坑
第一个坑:输出目录不存在导致失败。FFmpeg 不会自动创建多级目录,-hls_segment_filename里的路径目录得提前mkdir -p。
第二个坑:master playlist 里路径写错。用%v变量时,如果 segment 和 playlist 的相对路径没算对,播放器找不到切片。建议转完后手动打开 master.m3u8 核对一遍。
第三个坑:CRF 和码率混用。多码率场景要用-b:v指定码率,别用 CRF,否则每个 variant 的实际码率不可控,自适应逻辑就乱了。
第四个坑:忽略音频采样率。源音频是 44100Hz,转出来没统一,某些播放器会有杂音。统一成 48000Hz 最稳。
7. 生产环境的进阶考量
7.1 批量转码的脚本化
实际项目不可能手动敲命令。我一般写个 shell 脚本或 Python 脚本,遍历目录批量处理。核心是把上面的命令参数化,输入文件、输出目录、码率阶梯都做成变量。跑之前先小批量测试,确认参数没问题再全量跑。
7.2 存储与 CDN 配合
切片文件数量多,尤其是低延迟场景。建议切片目录结构清晰,按 variant 分目录,方便 CDN 缓存。m3u8 这类索引文件缓存时间要短,切片文件可以长缓存,因为它们内容不变。
7.3 加密与版权保护
如果内容需要保护,HLS 支持 AES-128 加密,DASH 支持多种 DRM 方案。FFmpeg 可以生成加密密钥和加密后的切片,但密钥管理和授权逻辑得自己实现。这块复杂度较高,建议有明确需求再上。
我在实际项目里最大的体会是:转码参数没有万能模板,必须结合内容类型和用户网络实测调优。同一套码率阶梯,用在动画片和体育直播上效果完全不同。我的做法是先转一版,找几个典型网络环境实测,看起播时间、切换次数、卡顿率,再回头调参数。这个过程可能要迭代两三轮,但调好之后体验提升非常明显。
另外一个小技巧:转码时保留一份原始的高码率源文件,别转完就删。后续如果要加新码率档位或者换协议,可以直接从源文件重新转,不用二次压缩损失画质。这个习惯帮我省过好几次返工的时间。