FFmpeg 将 MP4 切片成 m3u8 并在前端播放,我踩过的坑都在这儿了
起因
手头有个项目,用户上传的 MP4 视频需要在网页上播放。视频文件大的有 2 个 G,小的也有几百兆。
直接丢一个 <video> 标签上去?浏览器得下载完整个文件才开始播。用户等个几十秒,页面白屏,体验极差。
我需要的是:边下边播、拖拽跳转、省带宽。HLS 协议,也就是 m3u8 那一套,正好解决这个问题。
那怎么把 MP4 切成 m3u8 呢?FFmpeg。
但这玩意儿参数又多又碎,网上抄来的命令经常跑不起来,或者跑出来播放不了。折腾了一下午,总算理清楚了。
这篇博客就是我当时踩坑的全过程。
HLS 到底是个啥
HLS,全称 HTTP Live Streaming,苹果搞的协议。核心思路很简单:把一个大视频切成很多小碎片(ts 文件),然后用一个索引文件(m3u8)管理这些碎片。
播放器拿到 m3u8,就知道”哦,有 100 个碎片,每个 10 秒”,然后逐个下载播放。用户看到的是流畅的视频,背后是播放器在疯狂下载碎片。
这样做的好处:
- 秒开:不需要下载完整视频,第一个碎片下载完就能播
- 自适应:可以准备多个码率的版本,根据网络切换
- 拖拽友好:拖到 50% 的位置,播放器算一下是第几个碎片,直接下载那个 ts 就行
坏处就是:需要服务端切片的步骤,比直接丢 MP4 多了一道工序。
FFmpeg 切片命令,一条一条讲
你可能会搜到的命令
网上常见的 FFmpeg 切片命令长这样:
1 | |
跑一遍确实能出来 m3u8 和一堆 ts 文件。但仔细想想,问题一堆:
- 重新编码了视频,速度慢得离谱。一个 1G 的文件,跑十几分钟
- 画质损失了,因为是二次编码
- 输出文件特别大,因为重新编码反而可能更大
我实际用的命令
如果原始 MP4 本身就是 H.264 编码(绝大部分 MP4 都是),我们根本不需要重新编码。直接复制流,只做切片:
1 | |
-c copy 是关键。意思是”视频流和音频流原封不动复制”,不做二次编码。这样跑起来的速度基本是硬盘 IO 的上限,1G 的文件几十秒切完。
但还是有个问题——如果关键帧间隔太大,FFmpeg 没办法在任意时间点切开。所以切片之前,最好先处理一下关键帧。
更靠谱的做法:先转好关键帧
HLS 的每一个 ts 文件,必须以关键帧开头。如果 MP4 的关键帧间隔是 250 帧(约 10 秒),那切片时间精度最高就是 10 秒。你想切 6 秒一片,它做不到。
所以我的做法分两步:
第一步,把 MP4 转成关键帧对齐的版本:
1 | |
参数说明:
-g 48:每 48 帧一个关键帧,假设帧率 24fps,就是 2 秒一个关键帧-sc_threshold 0:关闭场景检测自动插关键帧,确保严格按照 g 值来-preset fast:编码速度和质量的一个平衡-vf "scale=-2:720":缩放到 720p 高度,宽度自动等比
第二步,对这个处理过的文件切片:
1 | |
两步走虽然多了一步编码,但整体可控,切片精度高,ts 文件大小均匀。
生成多种清晰度的 m3u8
如果你的用户场景需要自适应码率,需要生成多个分辨率的版本,再加一个主 m3u8:
1 | |
跑完会生成三个文件夹 v0、v1、v2,各自有 ts 和 m3u8,外加一个 master.m3u8。播放器加载 master.m3u8,自动根据网速选择合适的分辨率。
这个命令比较重,适合后台上传转码的场景。
前端怎么播放 m3u8
切片完成了,m3u8 文件配好了。现在问题变成:浏览器里怎么播?
方案一:hls.js
最成熟的方案。HLS 协议最初是苹果的,原生支持只有 Safari。其他浏览器需要借助 JavaScript 库。
1 | |
几行代码就能播。hls.js 自己处理了碎片下载、缓存、切换码率这些事情。
方案二:直接 video 标签(仅 Safari)
如果你只在 Safari 环境,可以直接用 video 标签播:
1 | |
Safari 底层直接调系统的 MediaPlayer 播放 HLS,不需要任何额外库。但如果用户用 Chrome 打开,啥都不显示。
hls.js 的一些配置项
hls.js 的默认配置大部分场景够用,但有些场景需要调:
1 | |
低延迟模式对于直播场景特别重要,但对点播来说,保持默认就行。
部署服务端的时候要注意什么
CORS 问题
这是最常见的坑。ts 文件和 m3u8 文件放在 OSS 或者自己的 CDN 上,前端网页在另一个域名,浏览器会报跨域错误。
如果是 Nginx:
1 | |
如果是阿里云 OSS,在 Bucket 的跨域设置里加一条规则,来源写 * 或者你的域名。
Content-Type 问题
Nginx 默认可能不认识 .m3u8 和 .ts 文件,需要手动配一下:
1 | |
不配的话,播放器拿到 m3u8 文件,浏览器不认识,直接拒绝加载。这个坑我踩了快一个小时才反应过来。
踩坑总结
错误 1:切片出来的 ts 播放不了
大概率是原始视频的关键帧太稀疏。先用 ffprobe 看一下:
1 | |
I 帧太少的话,先转码再做切片。
错误 2:前端播放器报”Media Source Error”
一般都是 m3u8 文件里的 ts 路径问题。检查 m3u8 里面写的路径是不是实际的 URL 能访问到的。如果是相对路径,确保基准 URL 正确。
错误 3:首屏慢
原因是第一个 ts 文件太大。调小 -hls_time 的值,比如从 10 秒减到 4 秒。或者加 -hls_init_time 2,让第一个切片只有 2 秒。
错误 4:拖拽不精准
原因同上,关键帧间隔太大。确保 -g 的值够小,最好能让 FFmpeg 在每一秒都有一个关键帧位置可以切。
写在最后
FFmpeg 切片这活儿,说简单很简单,一句 -c copy 就能搞定。说复杂也复杂,关键帧、码率自适应、跨域、Content-Type,一环扣一环,哪个环节出问题都播不出来。
前端用 hls.js 播放 m3u8 已经是很成熟的方案了,文档齐全,社区活跃。如果只是做点播场景,不用想得太复杂,按照上面的步骤走一遍基本能通。
如果要做直播或者低延迟场景,HLS 的延迟通常会比 WebRTC 大一些(传统 HLS 十几秒是常事)。LL-HLS(低延迟 HLS)能把延迟压到 2-3 秒,但需要 CDN 支持,这里就不展开说了。
以上都是我实际碰壁后的经验。希望你能少走几个弯路。