自托管播客 / 课程视频:怎么把 MP4 切成 HLS(m3u8 + ts)流式播放?2026 实战指南
把一段 1.5 GB 的课程录屏直接 <video src="course.mp4"> 丢到自己的 VPS 上,然后看着用户在国内移动 4G 下”加载视频中”转了 12 秒还没首帧,完播率掉到 30% 以下——这是几乎所有想自建播客 / 课程站的独立开发者第一次踩的坑。
解法是 HLS(HTTP Live Streaming):把一段大 MP4 切成几百个 2-6 秒的 .ts 分段,配一个 .m3u8 索引文件,浏览器按需下载、按需播放。CDN 友好、断点续传、首帧秒开、Range 请求都不用管。
但传统方案的问题是:FFmpeg 命令一长串、nginx 还要配 MIME 和 CORS、出了问题没法直观验证分段是否正确。本文把整条链路拆开讲一遍:浏览器一键切片(mp4-to-hls)、FFmpeg 命令行(含多码率自适应)、Cloudflare R2 / OSS / nginx 部署、hls.js 嵌入。每个环节都有可直接复制的代码。
为什么自托管要用 HLS 而不是直接扔 MP4
直接放 MP4 有三个隐性成本:CDN 计费按整文件下载量算(用户看 30% 就走也要扣 100% 流量)、移动端浏览器内存压力大(整段缓冲会被系统回收触发重新加载)、Safari 在 iOS 上对长 MP4 的 Range 请求处理时不时抽风。
HLS 把这三件事一次性解决:分段下载意味着实际只为用户看过的部分付 CDN 流量、.ts 单段 2-6 秒不会撑爆移动端内存、所有主流浏览器(Safari 原生、Chrome/Firefox 通过 hls.js)都对 HLS 做了首帧优化。
更重要的一点是 SEO 与社交分享:HLS 配合 <video> 标签可以让 Googlebot 拿到第一段 .ts 直接生成预览缩略图,而 1.5 GB 的 MP4 在 Twitter / Facebook 抓取时大概率超时。
| 维度 | MP4 直链 | HLS 分段 |
|---|---|---|
| 首帧时间(4G) | 8-15 秒 | 1-3 秒 |
| CDN 流量计费 | 按完整文件 | 按实际播放分段 |
| 断点续传 | Range header(部分浏览器抽风) | 天然支持 |
| 移动端内存 | 高(整段缓冲) | 低(按段释放) |
| 自适应码率 | ❌ | ✅(多 rendition) |
| Googlebot 预览 | 大概率超时 | ✅ |
| 实施复杂度 | 低(直接传文件) | 中(要切片 + 配置 MIME) |
唯一真正的成本是 多了一步切片——而这一步现在已经可以在浏览器里完成,不用装 FFmpeg。
HLS 切片原理速通
一个完整的 HLS 包结构是这样的:
output/
├── master.m3u8 # 主播放列表(声明有几条码率流)
├── media.m3u8 # 媒体播放列表(声明有几个分段、每段时长)
├── segment_000.ts # 分段 0
├── segment_001.ts # 分段 1
└── segment_002.ts # ...
master.m3u8 是给 ABR(自适应码率)用的,单码率场景甚至可以省略,播放器直接读 media.m3u8 也能跑。media.m3u8 长这样:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:5.997,
segment_000.ts
#EXTINF:5.997,
segment_001.ts
#EXTINF:4.234,
segment_002.ts
#EXT-X-ENDLIST
目标分段时长(#EXT-X-TARGETDURATION)通常选 4-6 秒。太短(1-2 秒)会让 CDN 请求数暴涨、首帧反而变慢;太长(10 秒+)则失去了”按需加载”的优势。Apple HLS 规范早期推荐 10 秒,2018 年之后默认值改成了 6 秒,2026 年的实战经验是 VOD 用 6 秒,直播用 4 秒。
最关键的细节是 关键帧间隔(GOP)必须能被分段时长整除。如果你的视频 GOP 是 10 秒、分段切 6 秒,那每个 .ts 的开头就不是关键帧,播放器跳转时会出现黑屏或花屏。FFmpeg 切片时必须强制 -g <fps*duration>,浏览器方案则需要工具自动对齐——这一点后面 cutfa.st 部分会展开。
三种生成 HLS 的方案对比
| 方案 | 上传文件 | 学习成本 | 多码率 | 加密 | 字幕 | 单次成本 | 适合谁 |
|---|---|---|---|---|---|---|---|
| FFmpeg 命令行 | 否(本地跑) | 高(命令一长串) | ✅ | ✅(AES-128/SAMPLE-AES) | ✅(WebVTT) | 0 | 已装 FFmpeg、需多码率/加密 |
| cutfa.st 浏览器 | 否(零上传) | 极低 | ❌(单 rendition) | ❌ | ❌ | 0 | 单码率够用、不想装 FFmpeg |
| 商业 SaaS(Mux / Cloudflare Stream) | ✅(要上传) | 低 | ✅ | ✅ | ✅ | $0.005/分钟存储 + $0.0005/分钟播放 | 不想自己管 CDN、有预算 |
大白话版本:
- 如果你只是想给自己的播客或课程视频做单码率流式播放、最大可能是 1080p 一档,用 mp4-to-hls 在浏览器里切完直接传 R2,5 分钟搞定,零成本;
- 如果你是教育平台、需要 480p / 720p / 1080p 多档自适应给不同网络的用户,那用 FFmpeg 命令行,下面给出完整命令;
- 如果你不想碰任何基础设施、愿意每月付几十刀给 Mux / Cloudflare Stream,那本文不适合你——你可以直接关掉。
cutfa.st 实战:浏览器一键导出 zip 包
cutfa.st 基于 Mediabunny + WebCodecs,所有切片在浏览器里完成,文件不上传。流程是这样的:
- 打开 mp4-to-hls(或 video-to-hls 适配 webm/mov/mkv 等格式、audio-to-hls 适配纯音频播客)
- 拖入 MP4 文件
- 选目标分段时长(默认 6 秒)和分段格式(MPEG-TS 兼容性最好 / fMP4 文件更小 / CMAF 同时支持 HLS+DASH)
- 点导出,下载 zip 包
zip 包解压后就是上一节展示的目录结构——一个 master.m3u8 + 一个 media.m3u8 + 一堆 .ts 或 .m4s 分段。直接传 CDN 即可。
适合 cutfa.st 的场景:
- 单码率(1080p 或 720p 一档够用)
- 课程录屏 / 播客 mp3 / 自媒体长视频
- 不需要 DRM 加密
- 不需要 WebVTT 字幕混流
不适合的场景(必须用 FFmpeg):
- 多码率自适应(ABR)
- 需要 AES-128 加密保护内容
- 字幕需要切片混流而不是 sidecar 形式
- 低延迟 HLS(LL-HLS)直播
诚实地说:cutfa.st 当前版本是单 rendition only。如果你的用户分布在 4G / WiFi / 千兆光纤三种网络环境,强烈建议直接跳到下一节用 FFmpeg。
补充一个被低估的优势:因为整个切片在浏览器里完成、文件不离开你的电脑,对于内部培训视频、付费课程母带、未发布的播客 demo 这类敏感内容,零上传方案天然合规——不用担心 SaaS 服务商的数据访问条款、不用过法务、不用签 DPA。
FFmpeg 命令实战
装 FFmpeg:macOS 用 brew install ffmpeg,Ubuntu 用 sudo apt install ffmpeg,Windows 去官网下静态构建。
单码率最简版
ffmpeg -i input.mp4 \
-c:v libx264 -preset veryfast -crf 23 \
-c:a aac -b:a 128k \
-g 60 -keyint_min 60 -sc_threshold 0 \
-hls_time 6 \
-hls_playlist_type vod \
-hls_segment_filename "segment_%03d.ts" \
output.m3u8
关键参数解释:
-g 60 -keyint_min 60 -sc_threshold 0:强制每 60 帧(30fps × 2 秒)一个关键帧,关闭场景检测,确保 GOP 与分段对齐-hls_time 6:目标分段时长 6 秒-hls_playlist_type vod:点播模式(直播用event)-c:v libx264 -preset veryfast:x264 编码、veryfast 预设(速度优先)
多码率自适应版(ABR)
这是 cutfa.st 当前版本做不了、必须 FFmpeg 的场景。一次切出 360p / 720p / 1080p 三档:
ffmpeg -i input.mp4 \
-filter_complex "[0:v]split=3[v1][v2][v3]; \
[v1]scale=w=640:h=360[v1out]; \
[v2]scale=w=1280:h=720[v2out]; \
[v3]scale=w=1920:h=1080[v3out]" \
-map "[v1out]" -c:v:0 libx264 -b:v:0 800k -maxrate:v:0 856k -bufsize:v:0 1200k \
-map "[v2out]" -c:v:1 libx264 -b:v:1 2800k -maxrate:v:1 2996k -bufsize:v:1 4200k \
-map "[v3out]" -c:v:2 libx264 -b:v:2 5000k -maxrate:v:2 5350k -bufsize:v:2 7500k \
-map a:0 -c:a aac -b:a 128k -ac 2 \
-map a:0 -c:a aac -b:a 128k -ac 2 \
-map a:0 -c:a aac -b:a 128k -ac 2 \
-g 60 -keyint_min 60 -sc_threshold 0 \
-hls_time 6 \
-hls_playlist_type vod \
-hls_segment_filename "stream_%v/segment_%03d.ts" \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
stream_%v/playlist.m3u8
切完之后目录结构是:
output/
├── master.m3u8
├── stream_0/playlist.m3u8 + segment_*.ts # 360p
├── stream_1/playlist.m3u8 + segment_*.ts # 720p
└── stream_2/playlist.m3u8 + segment_*.ts # 1080p
AES-128 加密版
防止用户直接下载 .ts 文件离线观看,可以加 AES-128 加密:
# 1. 生成 16 字节密钥
openssl rand 16 > enc.key
# 2. 创建 keyinfo 文件
cat > enc.keyinfo <<EOF
https://yoursite.com/keys/enc.key
enc.key
$(openssl rand -hex 16)
EOF
# 3. 切片 + 加密
ffmpeg -i input.mp4 \
-c:v libx264 -c:a aac \
-g 60 -keyint_min 60 -sc_threshold 0 \
-hls_time 6 \
-hls_key_info_file enc.keyinfo \
-hls_playlist_type vod \
output.m3u8
注意密钥分发逻辑要自己写:用户登录后才返回 enc.key,否则直接 403。这是个完整的鉴权系统,本文不展开。
CDN 部署
切片完成后,把整个目录传到任意支持 HTTP Range 的对象存储或 nginx 即可。
Cloudflare R2(推荐,零出口费用)
# 安装 wrangler
bun add -g wrangler
# 创建 bucket
wrangler r2 bucket create my-podcast
# 批量上传,注意 Content-Type 必须正确
wrangler r2 object put my-podcast/master.m3u8 \
--file=./output/master.m3u8 \
--content-type="application/vnd.apple.mpegurl"
# 用 shell 循环传所有分段
for f in ./output/*.ts; do
wrangler r2 object put "my-podcast/$(basename $f)" \
--file="$f" \
--content-type="video/mp2t"
done
绑定自定义域名 cdn.yoursite.com 之后,CORS 设置(在 R2 控制台 → Settings → CORS Policy):
[
{
"AllowedOrigins": ["https://yoursite.com"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["Range", "Origin"],
"ExposeHeaders": ["Content-Length", "Content-Range"],
"MaxAgeSeconds": 3600
}
]
R2 的核心优势是 出口流量零费用——存储 $0.015/GB/月,下行不收钱。一个 5 GB 的播客课程,月成本不到 $0.1,比阿里云 OSS / 腾讯云 COS 便宜两个数量级。
阿里云 OSS / 腾讯云 COS
阿里云用 ossutil,腾讯云用 coscmd。两者的 MIME 配置都在控制台 → 数据处理 → 文件 HTTP 头管理:
*.m3u8 → Content-Type: application/vnd.apple.mpegurl
*.ts → Content-Type: video/mp2t
*.m4s → Content-Type: video/iso.segment
CORS 在控制台 → 权限管理 → 跨域设置,规则同上。注意:阿里云 OSS 默认 5 GB 单 bucket 流量月限额 0.5 元/GB,建议挂 CDN 加速(CDN 回源 OSS)才能算合理价格。
nginx 自建
server {
listen 443 ssl http2;
server_name cdn.yoursite.com;
root /var/www/hls;
# MIME 类型
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
video/iso.segment m4s;
}
location ~ \.m3u8$ {
# m3u8 不缓存太久(VOD 可以长缓存,直播必须短缓存)
add_header Cache-Control "public, max-age=300";
add_header Access-Control-Allow-Origin "https://yoursite.com";
add_header Access-Control-Allow-Methods "GET, HEAD";
add_header Access-Control-Allow-Headers "Range, Origin";
}
location ~ \.(ts|m4s)$ {
# 分段可以长缓存(文件名带哈希就 immutable)
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "https://yoursite.com";
add_header Access-Control-Allow-Methods "GET, HEAD";
add_header Access-Control-Allow-Headers "Range, Origin";
}
# gzip 不要开启 .ts(已经压缩过的二进制开 gzip 是负优化)
gzip on;
gzip_types application/vnd.apple.mpegurl;
}
.m3u8 短缓存(防止你重新切片后用户拿到旧索引)、.ts 长缓存(分段内容不会变,可以 immutable)——这是最容易踩的坑。
在自己网站嵌入 HLS 播放器
方案 A:原生 video 标签(最简,Safari 友好)
<video controls width="800" poster="cover.jpg">
<source src="https://cdn.yoursite.com/master.m3u8"
type="application/vnd.apple.mpegurl" />
您的浏览器不支持 HLS。
</video>
Safari(macOS / iOS)原生支持。Chrome / Firefox / Edge 不支持原生 HLS,会显示 fallback 文字——所以生产环境必须配 hls.js。
方案 B:hls.js(覆盖所有现代浏览器)
<video id="video" controls width="800" poster="cover.jpg"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js"></script>
<script>
const video = document.getElementById('video');
const src = 'https://cdn.yoursite.com/master.m3u8';
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生
video.src = src;
} else if (Hls.isSupported()) {
// Chrome / Firefox / Edge
const hls = new Hls({
maxBufferLength: 30, // 最多缓冲 30 秒
maxMaxBufferLength: 60,
capLevelToPlayerSize: true, // 自动限制码率到播放器尺寸
});
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) console.error('HLS fatal error:', data);
});
}
</script>
方案 C:Plyr(带美化 UI,开源免费)
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
<video id="player" controls></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js"></script>
<script src="https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"></script>
<script>
const video = document.getElementById('player');
const src = 'https://cdn.yoursite.com/master.m3u8';
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(src);
hls.attachMedia(video);
} else {
video.src = src;
}
const player = new Plyr(video, {
controls: ['play', 'progress', 'current-time', 'mute', 'volume', 'settings', 'fullscreen'],
settings: ['quality', 'speed'],
});
</script>
Plyr 的好处是开箱即用的倍速 / 画质切换 UI,对课程站 / 播客站的用户体验提升明显。video.js 也是同类选择,但包体积更大(200KB+),个人项目首推 Plyr。
FAQ
Q1:单 rendition(cutfa.st 浏览器版)真的够用吗?
绝大多数自媒体 / 课程场景够用。如果你的内容是 1080p、目标用户在 WiFi 环境为主、不打算覆盖 4G 弱网用户,那单 rendition 是 80/20 解法。判断标准:用 Cloudflare Analytics 看用户的 P50 网络速度,如果 P50 > 5 Mbps 那 1080p 单档没问题。
Q2:什么时候必须上 ABR(多码率自适应)?
三种情况:(1)目标用户跨网络环境严重(教育产品覆盖三四线城市);(2)单期内容 30 分钟以上(流量浪费成本变高);(3)有海外用户(跨境带宽不稳定)。这三种都建议直接 FFmpeg 切多档。
Q3:字幕怎么加?
cutfa.st 当前版本不支持 WebVTT 混流,但可以用 sidecar 方式:
<video controls>
<source src="master.m3u8" type="application/vnd.apple.mpegurl" />
<track kind="subtitles" srclang="zh" src="captions.vtt" default />
</video>
如果一定要切片混流(比如要让 hls.js 切换不同语言字幕轨道),目前只能 FFmpeg。
Q4:能不能加付费会员墙?
加密用 AES-128(上面给了命令),鉴权用签名 URL:用户登录后服务端返回带 ?token=<jwt> 的 m3u8 URL,CDN(Cloudflare Workers / 阿里云 CDN 都支持)验签后再返回内容。复杂度中等,一个晚上能搞定。
Q5:怎么接播放统计?
hls.js 暴露了完整事件流。最简版本:
hls.on(Hls.Events.FRAG_LOADED, (event, data) => {
// 上报分段加载(精确到秒级播放进度)
fetch('/api/play-event', {
method: 'POST',
body: JSON.stringify({
videoId: 'xxx',
fragSn: data.frag.sn,
duration: data.frag.duration,
}),
});
});
Mux Data 是商业方案,开源可以用 video.js 的统计插件或自己写。
Q6:Cloudflare R2 + 自建播放器,每月最低成本是多少?
实际算过一笔:5 GB 内容(约 10 集播客 + 5 节课程录屏)、月活 500 用户、平均观看 30 分钟。R2 存储 $0.075、Class A 操作(上传切片)一次性 $0.05、Class B 操作(用户拉取分段)约 $0.20、出口流量 $0。总成本约 $0.30/月——比 Mux ($30+) 便宜两个数量级,比 Cloudflare Stream ($1/1000 分钟) 也便宜一半以上。这是为什么 R2 + 浏览器切片方案值得推荐的核心原因。
到这一步整条链路就跑通了:浏览器切片(mp4-to-hls / video-to-hls / audio-to-hls)或 FFmpeg 命令 → R2 / OSS / nginx 部署 → hls.js 嵌入。如果你只是给自己的播客或课程站做单码率流式播放,浏览器方案 5 分钟就能跑完整套;如果要做严肃的多码率商业产品,FFmpeg 命令行加 R2 是 2026 年最有性价比的组合。HLS 不是火箭科学,挡在前面的只是一堆配置细节——这篇文章应该把绝大部分细节给你扫清了。