从零搭一个本地 HLS 点播服务器(nginx + ffmpeg 全细节)
有时候你需要的不是"把视频下载到本地",而是"把一段视频变成能在浏览器里边下边看的流"——比如给团队内部分享教学片、本地搭测试环境验证播放器对 HLS 的兼容性、或者做个简单的内部视频库。这篇文章记的是用一台普通笔记本就能跑起来的最小可用方案,以及那些文档不会写、但一定会撞上的细节。
一、为什么选 HLS 而不是 RTMP/WebRTC
- RTMP:需要常驻推流服务(如 nginx-rtmp-module 或 SRS),长连接、端口容易被防火墙挡,浏览器原生也不直接播 RTMP,得靠 Flash(已死)或转协议。调试成本高。
- WebRTC:主打超低延迟(亚秒级),但适合实时通话,不适合点播,且服务端复杂度高。
- HLS:本质就是"一堆
.ts切片 + 一个.m3u8清单",任何普通 Web 服务器(nginx、甚至静态托管)都能托管,无需特殊协议。浏览器里<video>原生支持(Safari 直支持,Chrome 靠 hls.js 补一下)。踩坑面最小、心智负担最低。
对"内部分享 / 本地测试"这种场景,HLS 是性价比最高的选择。它的代价是延迟高(几秒到十几秒),但点播根本不在乎延迟。
二、第一步:切片
假设你有 input.mp4,用 ffmpeg 切:
ffmpeg -i input.mp4 \
-c:v h264 -c:a aac \
-hls_time 10 \
-hls_playlist_type vod \
-hls_segment_filename out/%03d.ts \
out/index.m3u8
参数细说:
- -hls_time 10:每片 10 秒。值越小切片越多(适合弱网切换),越大切片少(清单短)。我一般 6~10 秒。
- -hls_playlist_type vod:强制写 #EXT-X-ENDLIST,避免被当直播。如果不写,默认是 event/live 模式,播放器会一直等更新。
- -hls_segment_filename out/%03d.ts:指定切片命名,否则默认叫 index0.ts 之类,乱。
关键坑:编码归一。如果你的源是 HEVC(H.265),老 Safari 不支持 HLS 里的 HEVC,Chrome 的 hls.js 更别想。所以命令里我特意写 -c:v h264,先把视频编码归一,省得后面调试到怀疑人生。音频用 -c:a aac 也是同理——opus 在部分播放器 HLS 里支持差。
如果源本身已经符合要求想省时间,可以 -c copy 不重编码,但前提是源就是 H.264+AAC,否则播不了。
三、第二步:用 nginx 托管
别用 Python 的 http.server 凑合,它会把 .m3u8 的 Content-Type 设成 text/plain 或 application/octet-stream,某些播放器看到非标准类型直接拒绝播。nginx 加一段:
server {
listen 8080;
server_name localhost;
location /videos/ {
root /path/to; # 注意 root 是父目录,最终访问 /videos/out/index.m3u8
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
add_header Access-Control-Allow-Origin *;
add_header Cache-Control "no-cache";
}
}
两个致命细节:
1. add_header Access-Control-Allow-Origin *:跨域拉流必须有。前端页面跑在 localhost:3000 想播 localhost:8080 的流,浏览器同源策略会挡,没这个头直接红。我在这上面耗了半小时,最后就差这一行。
2. root 的语义:nginx 的 root /path/to 配合 location /videos/ 意味着实际文件路径是 /path/to/videos/...。我习惯把切片放 /path/to/videos/out/,访问 http://localhost:8080/videos/out/index.m3u8。路径对不上是最常见的 404。
改完 nginx -s reload 重载配置。
四、第三步:本地验证能不能播
先用 curl 确认清单和切片都能拿到:
curl -I http://localhost:8080/videos/out/index.m3u8
curl -I http://localhost:8080/videos/out/000.ts
HTTP 200、Content-Type 正确,才算托管 OK。别上来就开浏览器,先排除服务器层问题。
五、前端播放:Safari vs Chrome
Safari 原生支持 HLS,直接 <video src=".../index.m3u8"> 就能放。Chrome 需要 hls.js:
<video id="v" controls></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
const v = document.getElementById('v');
const url = 'http://localhost:8080/videos/out/index.m3u8';
if (Hls.isSupported()) {
const h = new Hls();
h.loadSource(url);
h.attachMedia(v);
h.on(Hls.Events.ERROR, (e, d) => console.error('hls error', d));
} else if (v.canPlayType('application/vnd.apple.mpegurl')) {
v.src = url; // Safari 原生
}
</script>
h.on(ERROR) 这行很有用:真出问题(比如跨域、清单 404)它会在控制台打出来,比盲调快十倍。
六、进阶:多码率自适应
真要模拟线上弱网体验,可以做多码率。ffmpeg 切两遍(720p、1080p),再用主清单串起来:
ffmpeg -i input.mp4 -vf scale=1280:720 -c:v h264 -b:v 2500k -c:a aac -hls_time 10 -hls_segment_filename out/720p_%03d.ts out/720p.m3u8
ffmpeg -i input.mp4 -vf scale=1920:1080 -c:v h264 -b:v 5000k -c:a aac -hls_time 10 -hls_segment_filename out/1080p_%03d.ts out/1080p.m3u8
然后手写一个 master.m3u8:
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=2600000,RESOLUTION=1280x720
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5100000,RESOLUTION=1920x1080
1080p.m3u8
播放器会根据带宽自动切档。但说实话,内部分享用单一码率够了,多码率是为公网弱网准备的,本地意义不大。
七、想加"防盗":AES-128 加密
本地分享一般不用,但如果你非要防止文件被直接拖走,可以给切片加 HLS AES-128:
ffmpeg -i input.mp4 -c:v h264 -c:a aac \
-hls_time 10 -hls_key_info_file keyinfo.ini \
out/index.m3u8
keyinfo.ini 里写密钥路径和 URI。播放器拉到清单后会去 URI 取密钥解密。注意:这只能防"随手下载",密钥还在客户端,真想防盗得用正经 DRM(那是另一篇文章的事)。
八、偷懒方案:Docker 一行起
不想装 nginx,可以用现成镜像:
docker run -d -p 8080:80 -v /path/to/videos:/usr/share/nginx/html/videos nginx
把切片放 /path/to/videos/out/,访问 http://localhost:8080/videos/out/index.m3u8。镜像自带正确的 m3u8 类型,但 CORS 头还是得自己加(改配置或换带 CORS 的镜像)。
九、常见报错速查
| 现象 | 原因 | 解法 |
|---|---|---|
| 跨域红字 | 缺 CORS 头 | nginx 加 Access-Control-Allow-Origin * |
| 清单 404 | root/location 路径错 | 核对实际文件路径 |
| --- 播放器不认 m3u8 | Content-Type 错 | nginx types 里配 m3u8/ts |
| Chrome 黑屏 | 没引 hls.js | 加 hls.js 或换 Safari |
| 画面绿/花 | 源是 HEVC | 切片时转成 H.264 |
| 一直转圈 | 当直播了(没 ENDLIST) | 加 -hls_playlist_type vod |
十、收尾
这套东西的灵魂是"简单":ffmpeg 切片、nginx 托管、hls.js 播放,三块各司其职,哪块出问题都能单独验证。别一上来就上 SRS、ZLMediaKit 这种全家桶,内部分享场景根本用不上。最后再啰嗦一句:这套只适合你 own 的内容或授权素材;拿它当"本地版爱奇艺"缓存别人版权内容,那就不归技术范畴了。