视频加载失败

背景视频播放优化实录:从mp4卡顿到HLS分片秒开

3290 字
16 分钟
背景视频播放优化实录:从mp4卡顿到HLS分片秒开

一、问题现象:三种设备三种病#

博客首页有一段 245 秒的樱花飘落背景视频(firefly.mp4,14.94MB)。上线后发现同一个视频在不同入口表现完全不同,没有一种让人满意:

场景现象一句话根因
手机浏览器 / 微信点播放变”下载界面”;微信里 dev 链接点播放完全放不了CF Pages 不支持 Range,浏览器把视频当文件下载
电脑 · dev(Cloudflare Pages)点播放十几秒没反应,视频放不出来 / 加载极慢15MB 整包下载,加载路径长
电脑 · io(GitHub Pages)能播但明显卡顿,拖动进度条基本没法用GitHub 国内线路慢(实测约 22KB/s),边下边播跟不上播放速度

需求非常明确:完整 245 秒不截取、双平台都能播、加载快。压缩治标不治本,截取视频被直接否决,最终落在 HLS 分片方案上。

二、根因定位:Range 请求与平台能力差异#

浏览器播放 mp4 时,“边下边播”依赖 HTTP 的 Range 请求:浏览器先请求文件开头一小段(Range: bytes=0-1048575),服务器返回 206 Partial Content 和这一段的字节,浏览器解码开头就能开始播放,播放的同时继续请求后续分段。这个机制不需要服务器支持,只需要它响应 Range 头。

对本站两个托管平台做了实测:

平台带 Range 请求的结果含义
Cloudflare Pages(dev)返回 200 + 完整 15.66MB不支持 Range,只能整包下载完才能播
GitHub Pages(io)返回 206 分段响应支持 Range,但国内线路慢(实测渐进下载约 22KB/s)

于是 mp4 单文件在两条路上都受限:

mp4 单文件与 HLS 分片的本质区别
mp4 单文件与 HLS 分片的本质区别

  • dev 上:不支持 Range → 15MB 必须整体下载完 → 手机上就表现为”下载界面/放不了”,电脑上点播放后长时间无反应;
  • io 上:支持 Range 但线路慢 → 下载跟不上播放速度 → 卡顿。

三、视频压缩的取舍:五个版本的迭代#

在动播放方案之前,先尝试把视频压小。245 秒的视频,Cloudflare Pages 单文件上限 25MB,可用码率预算约等于 25MB × 8 / 245s ≈ 816kbps,动画画面(樱花飘落 + 人物)对这个码率非常敏感:

版本参数体积 / 码率结果
v1crf 3513MB / 448kbps动态细节全糊,弃
v2crf 2730.3MB超 25MB 限制,弃
v32-pass 900k29.3MBABR 码率有偏差,仍超限,弃
v4crf 3024.27MB(720p)压在限制内,本地备份,未上线
上线版crf 3014.94MB(480p)清晰度可接受,但单文件播放问题依旧

压缩只能把体积从 30MB 压到 15MB,“服务器不支持 Range”这个硬伤不会因为文件变小而消失——问题不在文件大小,在下载方式。这条路走到头了,必须换播放架构。

四、HLS 分片:行业标准做法#

HLS(HTTP Live Streaming) 是 Apple 提出的流媒体协议,也是当前事实上的行业标准:把视频切成若干 6 秒左右的小分片(.ts 文件)+ 一个播放清单(playlist.m3u8),播放器按需逐片拉取。YouTube、B 站、Netflix 的网页播放都是 HLS / DASH(DASH 是另一套类似标准)的思路——“分片传输”本身就是行业标准做法,而不是什么 hack。

为什么分片能绕开”不支持 Range”?因为每个分片是完整的小文件(约 355KB),服务器直接返回整个文件即可,根本不需要 Range;播放器一次只拉几片,播到哪拉到哪,总下载量随播放进度增长,而不是一次性拉完 15MB。

1. 分片怎么用:三步走,以及和 mp4 / DASH 的区别#

HLS 分片不是黑魔法,用起来就三步:

  1. 切片:ffmpeg 把视频按 6 秒一段切成几十个 .ts 小文件,同时生成一个 playlist.m3u8 清单(记录每片的地址、时长、顺序);
  2. 上传:把 m3u8 + 所有 .ts 分片一起传到托管平台——每个分片都是独立小文件,普通静态托管就能直接提供;
  3. 播放:播放器(hls.js)先拉清单,再按播放进度逐片拉取对应分片,播到哪拉到哪,不需要一次下载整部视频。

YouTube、B 站、Netflix、抖音这些主流平台的网页播放,底层都是这个思路(HLS 或 DASH 分片流)——所以”分片”是行业标准做法,不是 hack。

那 HLS 和”mp4 渐进式播放”、DASH 到底有什么区别?一张表说清:

对比项mp4 渐进式播放HLS 分片DASH 分片
文件形态单个 mp4 文件几十个 .ts 分片 + 1 个 m3u8 清单多个 MP4 分片 + 1 个 MPD 清单
播放方式浏览器靠 Range 请求边下边播播放器读清单、按需逐片拉取同 HLS,按清单逐段拉取
依赖服务器支持 Range?是(不支持就整包下载完才能播)否(每个分片是完整小文件,直接返回)否
浏览器原生支持所有浏览器Safari 原生支持;Chrome/Firefox 靠 hls.js 补需浏览器 MSE 能力,兼容性较散
谁在用老站/小站常见YouTube、B 站、Netflix、Apple、抖音部分平台自选(如部分安卓端)
本项目备用链路✅ 主链路未用

区别的核心就一行:mp4 渐进式靠”服务器配合分段返回”(Range),HLS 靠”文件本身就切成段”——所以 HLS 对不支持 Range 的托管(如 Cloudflare Pages)天然免疫。

2. 切片命令#

用 ffmpeg 把 firefly.mp4 切成 41 片 + VOD 清单(-hls_playlist_type vod 表示点播、非直播):

Terminal window
ffmpeg -y -i firefly.mp4 \
-c:v libx264 -preset medium -crf 30 -pix_fmt yuv420p \
-c:a aac -b:a 96k \
-force_key_frames expr:gte(t,n_forced*6) \
-hls_time 6 -hls_playlist_type vod \
-hls_segment_filename seg_%04d.ts \
playlist.m3u8

产物:playlist.m3u8 + seg_0000.ts ~ seg_0040.ts(41 片),总大小 14.56MB,与原始 mp4 相当(质量没有额外损失)。

3. 播放链路设计#

背景视频播放链路(最终形态)
背景视频播放链路(最终形态)

播放器逻辑(BackgroundPlayer.astro):

function doPlay() {
var url = getCurrentUrl();
// 单视频 + 有 HLS 配置 + 未启用备用源时优先走 HLS
if (hlsUrl && !isMultiple && !fallbackTried) {
playHls(hlsUrl, function () { playMp4(url); });
} else {
playMp4(url);
}
}
  • playHls():点击播放时才动态加载 hls.min.js(约 605KB,懒加载不拖慢首屏),初始化 Hls 实例,MANIFEST_PARSED 后 video.play();
  • HLS 致命错误(data.fatal)自动 destroyHls() 并回退 mp4;
  • mp4 再失败进备用源,全部失败才显示图片壁纸。
4. 两个值得记录的坑#

坑一:is:inline 脚本里 import('hls.js') 是无效的。 播放器脚本是 Astro 的 is:inline(原样输出、不走打包),里面的裸模块名 hls.js 浏览器根本无法解析,会静默走 mp4 分支。修复:把 hls.min.js 放到 public/assets/js/,用站点根绝对路径动态导入:

import('/assets/js/hls.min.js')
.then(function (mod) {
var Hls = mod.default || window.Hls;
if (!Hls) { onFail(); return; }
startWith(Hls);
})
.catch(function () { onFail(); });

坑二:补丁把 doPlay 打成了嵌套空壳。 第一次集成时新旧 doPlay 嵌套,外层函数只声明内部函数、什么都不执行——点击播放毫无反应,但手动调用内部逻辑又正常,非常隐蔽。构建产物里 minifier 把内层函数重命名为 doPlay2 才暴露出来。修复:把 hlsInstance / destroyHls / playMp4 / playHls / doPlay 全部提升到同一作用域。

5. 双平台实测#
站点实测结果
io(GitHub Pages)readyState=4、854×480、播放中、跨域拉取 dev 分片正常
dev(Cloudflare Pages)readyState=4、854×480、播放中、buffered 达 54s

点击播放后 1-2 秒出画面,手机/微信不再出现下载界面,电脑端不再卡顿。分片约 355KB/片,在 dev 上实测约 0.4 秒/片下载。

五、双域名分工与自定义域名的真相#

双域名分工与自定义域名的真相
双域名分工与自定义域名的真相

  • io(GitHub Pages):主站,用户访问和微信分享的域名;页面跨域拉取 dev 的分片;
  • dev(Cloudflare Pages):视频仓库,存放 m3u8 + 41 个分片;国内下载快;_headers 配置 Access-Control-Allow-Origin: * 允许跨域。

关于自定义域名,一个常见误区是”绑了自定义域名视频就不卡了”。实际上自定义域名只是换门牌号:把 lycaonide.github.io 绑成 lycaonide.com,访问时底层还是同一台 GitHub/Cloudflare 服务器,是否支持 Range、国内线路快慢完全不变。把”域名无关”这件事说透——视频卡顿/下载界面的根因是两个平台能力问题,跟域名无关:

根因出在哪自定义域名能改吗
CF Pages 不支持 Range(15MB 必须整包下载)dev 的服务器❌ 域名还是指到 CF 的服务器
GitHub Pages 国内线路慢io 的服务器/线路❌ 域名还是指到 GitHub 的服务器

真正能让 mp4 直接边下边播的条件是:同时满足”有自定义域名 + 视频托管到支持 Range 且国内快的平台”(如阿里云/腾讯云 OSS/COS + CDN,或支持 Range 的国内托管)。这种情况下浏览器直接拉 mp4 就能边下边播,不需要分片;光有域名、托管还在 GitHub/CF 上,等于白绑。HLS 分片方案则对”不支持 Range”天然免疫,与域名无关。

六、CF Pages 不支持 Range 的替代方案#

先交代为什么需要替代方案:CF Pages 对视频不支持 Range 请求(HTTP Range 是浏览器”边下边播”的基础,服务器按需返回字节区间)。浏览器拿不到 Range 响应,只能整包下载 15MB 视频——这就是”下载界面”和”加载慢”的直接根源。不想用 HLS 的话,还有几条路,按推荐度排序:

① Cloudflare R2(最推荐)

  • 原理:CF 自家对象存储,原生支持 Range,mp4 单文件直接边下边播;
  • 怎么用:CF 控制台开 R2 → 新建 bucket → 上传视频 → 开启公共访问(Public Access)拿到直链 → 播放器改回一行 video.src = mp4 链接;
  • 成本:免费额度 10GB 存储 + 100 万次读/月,本项目 14.94MB 的视频随便用;
  • 优点:和 CF 生态一体,以后有自定义域名直接绑;国内访问速度比 GitHub 好;播放器代码回到最简单的单 mp4;
  • 缺点:需要去 CF 开 R2(新用户开 R2 可能要绑卡,超出免费额度才计费);R2 国内速度不算顶级,但明显好于 GitHub。

② jsDelivr CDN(免费捷径)

  • 原理:GitHub 仓库文件走 jsDelivr 免费 CDN,支持 Range、国内节点速度快;
  • 怎么用:视频传到 GitHub release → 用 https://cdn.jsdelivr.net/gh/用户名/仓库名@版本号/路径/视频.mp4 格式的链接;
  • 限制:单文件 20MB 上限(本项目 14.94MB 刚好卡线);jsDelivr 国内稳定性一般(2022 年出过 DNS 故障,曾大面积失效);
  • 结论:作为免费捷径可以试,但不建议当主方案——稳定性和服务政策都是外部不可控因素。

③ Workers 代理模拟 Range(不推荐)

  • 原理:写一个 Worker 拦截视频请求,手动解析 Range 请求头、按字节区间切片返回;
  • 问题:每次请求都要回源拉全量视频再切片,带宽浪费严重、首次延迟高;代码和调试成本不小,效果还受 Worker 执行时间限制(免费档 10ms CPU);
  • 结论:只有”必须用 CF Pages 域名 + 必须保持单文件 mp4”这种极端约束下才值得考虑。

④ 保持 HLS(本项目现状)

  • 原理:ffmpeg 把视频切成 41 个 .ts 分片 + 一个 playlist.m3u8 清单,播放器按清单逐片拉取;
  • 优势:对”不支持 Range”天然免疫——每个分片都是完整小文件,服务器直接返回即可,Range 根本不在依赖链里;行业标准做法(YouTube、B 站、Netflix 都是分片流);已双平台实测正常;
  • 缺点:代码比单 mp4 复杂一点(多一个 hls.js 和一批分片文件),但对本项目来说这个复杂度完全可接受。

我的建议:

  • 想彻底简化代码 → 方案①(R2):视频传上去开公共访问,播放器改回一行 video.src = mp4,hls.js 和 41 个分片全删;R2 在 Cloudflare 生态内,以后自定义域名直接绑,国内速度比 GitHub 好;
  • 不想动任何东西 → 维持 HLS 现状:已经解决你的问题,是行业标准做法,零成本;
  • jsDelivr 当免费捷径可以试,但稳定性要打个问号,别当主方案;Workers 模拟 Range 复杂度高收益低,直接跳过。

七、总结#

  1. 问题本质:mp4 单文件播放依赖服务器支持 Range;CF Pages 不支持、GitHub Pages 线路慢,两条路都受限;
  2. 压缩解决不了播放架构问题,但压缩本身依然重要——480p / 14.94MB 是清晰度与体积的平衡点;
  3. HLS 分片是行业标准做法:把视频切成小分片,播放器按需拉取,绕开 Range 限制,YouTube / B 站 / Netflix 都是这个思路;
  4. 域名只管门面:自定义域名不改变底层服务器能力,除非同时换到支持 Range 且国内快的托管;
  5. 兜底链路:HLS → mp4 → 备用源 → 图片壁纸,任何网络下都有内容可播。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

背景视频播放优化实录:从mp4卡顿到HLS分片秒开
https://lycaonide.github.io/posts/背景视频播放优化实录/
作者
lycaonide
发布于
2026-09-17
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
lycaonide
ROS2 / AI / Web 开发,学习中的学生开发者,记录踩坑与成长。
公告
本站已从 Hexo 迁移至 Astro,启用了 Giscus 评论和友链页。2028 考研备考中,更新随缘,欢迎交流。
分类
标签
站点统计
文章
6
分类
1
标签
15
总字数
23,810
运行时长
0 天
最后活动
0 天前
站点信息
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录