提示

返回博客列表

从零搭一个本地 HLS 点播服务器(nginx + ffmpeg 全细节)

从零搭一个本地 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/plainapplication/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 的内容或授权素材;拿它当"本地版爱奇艺"缓存别人版权内容,那就不归技术范畴了。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部