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
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 0 -hls_segment_filename "output_%03d.ts" output.m3u8

跑一遍确实能出来 m3u8 和一堆 ts 文件。但仔细想想,问题一堆:

  1. 重新编码了视频,速度慢得离谱。一个 1G 的文件,跑十几分钟
  2. 画质损失了,因为是二次编码
  3. 输出文件特别大,因为重新编码反而可能更大

我实际用的命令

如果原始 MP4 本身就是 H.264 编码(绝大部分 MP4 都是),我们根本不需要重新编码。直接复制流,只做切片:

1
ffmpeg -i input.mp4 -c copy -hls_time 10 -hls_list_size 0 -hls_segment_filename "output_%03d.ts" output.m3u8

-c copy 是关键。意思是”视频流和音频流原封不动复制”,不做二次编码。这样跑起来的速度基本是硬盘 IO 的上限,1G 的文件几十秒切完。

但还是有个问题——如果关键帧间隔太大,FFmpeg 没办法在任意时间点切开。所以切片之前,最好先处理一下关键帧。

更靠谱的做法:先转好关键帧

HLS 的每一个 ts 文件,必须以关键帧开头。如果 MP4 的关键帧间隔是 250 帧(约 10 秒),那切片时间精度最高就是 10 秒。你想切 6 秒一片,它做不到。

所以我的做法分两步:

第一步,把 MP4 转成关键帧对齐的版本:

1
ffmpeg -i input.mp4 -c:v libx264 -preset fast -g 48 -sc_threshold 0 -c:a aac -b:a 128k -vf "scale=-2:720" output_transcoded.mp4

参数说明:

  • -g 48:每 48 帧一个关键帧,假设帧率 24fps,就是 2 秒一个关键帧
  • -sc_threshold 0:关闭场景检测自动插关键帧,确保严格按照 g 值来
  • -preset fast:编码速度和质量的一个平衡
  • -vf "scale=-2:720":缩放到 720p 高度,宽度自动等比

第二步,对这个处理过的文件切片:

1
ffmpeg -i output_transcoded.mp4 -c copy -hls_time 6 -hls_list_size 0 -hls_segment_filename "output_%03d.ts" output.m3u8

两步走虽然多了一步编码,但整体可控,切片精度高,ts 文件大小均匀。

生成多种清晰度的 m3u8

如果你的用户场景需要自适应码率,需要生成多个分辨率的版本,再加一个主 m3u8:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ffmpeg -i input.mp4 \
-filter_complex "[0:v]split=3[v1][v2][v3]; \
[v1]scale=-2:1080[1080p]; \
[v2]scale=-2:720[720p]; \
[v3]scale=-2:480[480p]" \
-map "[1080p]" -c:v libx264 -b:v 3000k -g 48 -sc_threshold 0 \
-map "[720p]" -c:v libx264 -b:v 1500k -g 48 -sc_threshold 0 \
-map "[480p]" -c:v libx264 -b:v 800k -g 48 -sc_threshold 0 \
-map 0:a -c:a aac -b:a 128k \
-f hls -hls_time 6 -hls_list_size 0 \
-master_pl_name "master.m3u8" \
-var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" \
-hls_segment_filename "v%v/output_%03d.ts" \
"v%v/prog_index.m3u8"

跑完会生成三个文件夹 v0、v1、v2,各自有 ts 和 m3u8,外加一个 master.m3u8。播放器加载 master.m3u8,自动根据网速选择合适的分辨率。

这个命令比较重,适合后台上传转码的场景。

前端怎么播放 m3u8

切片完成了,m3u8 文件配好了。现在问题变成:浏览器里怎么播?

方案一:hls.js

最成熟的方案。HLS 协议最初是苹果的,原生支持只有 Safari。其他浏览器需要借助 JavaScript 库。

1
2
3
4
5
6
7
8
9
10
11
12
<video id="video" controls></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
const video = document.getElementById('video');
const hls = new Hls();

hls.loadSource('https://your-server.com/videos/output.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play();
});
</script>

几行代码就能播。hls.js 自己处理了碎片下载、缓存、切换码率这些事情。

方案二:直接 video 标签(仅 Safari)

如果你只在 Safari 环境,可以直接用 video 标签播:

1
2
3
<video controls>
<source src="https://your-server.com/videos/output.m3u8" type="application/x-mpegURL">
</video>

Safari 底层直接调系统的 MediaPlayer 播放 HLS,不需要任何额外库。但如果用户用 Chrome 打开,啥都不显示。

hls.js 的一些配置项

hls.js 的默认配置大部分场景够用,但有些场景需要调:

1
2
3
4
5
6
7
8
9
const hls = new Hls({
maxBufferLength: 30, // 最大缓冲 30 秒
maxMaxBufferLength: 60, // 最多缓冲 60 秒
enableWorker: true, // 开启 Web Worker,不阻塞主线程
startLevel: -1, // -1 表示自动选择码率
lowLatencyMode: true, // 低延迟模式
fragLoadingTimeOut: 20000, // 碎片加载超时
abrEwmaDefaultEstimate: 500000 // 初始带宽估计值(bps)
});

低延迟模式对于直播场景特别重要,但对点播来说,保持默认就行。

部署服务端的时候要注意什么

CORS 问题

这是最常见的坑。ts 文件和 m3u8 文件放在 OSS 或者自己的 CDN 上,前端网页在另一个域名,浏览器会报跨域错误。

如果是 Nginx:

1
2
3
4
5
location /videos/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, OPTIONS';
add_header Access-Control-Allow-Headers 'Range';
}

如果是阿里云 OSS,在 Bucket 的跨域设置里加一条规则,来源写 * 或者你的域名。

Content-Type 问题

Nginx 默认可能不认识 .m3u8 和 .ts 文件,需要手动配一下:

1
2
3
4
5
6
7
location ~ \.m3u8$ {
add_header Content-Type application/x-mpegURL;
}

location ~ \.ts$ {
add_header Content-Type video/MP2T;
}

不配的话,播放器拿到 m3u8 文件,浏览器不认识,直接拒绝加载。这个坑我踩了快一个小时才反应过来。

踩坑总结

错误 1:切片出来的 ts 播放不了
大概率是原始视频的关键帧太稀疏。先用 ffprobe 看一下:

1
ffprobe -v error -select_streams v:0 -show_entries frame=pict_type -of csv input.mp4 | grep I | wc -l

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 支持,这里就不展开说了。

以上都是我实际碰壁后的经验。希望你能少走几个弯路。


FFmpeg 将 MP4 切片成 m3u8 并在前端播放,我踩过的坑都在这儿了
https://notavia.cc/posts/ffmpeg-mp4-to-m3u8-hls-playback/
作者
lingyi
发布于
2026年6月5日
许可协议