直播上线后收到的第一个需求,往往不是画质也不是延迟,而是"能不能往回拖一点"。切片文件明明还躺在磁盘上,看起来改个配置就能搞定。真动手才发现,这是一次横跨三个系统的配置变更,外加六十行左右的播放器代码。

需要的基础环境并不复杂:ffmpeg 7.0+、hls.js 1.6+、node 22.x。先用两条命令确认版本,再往下走。

打开网易新闻 查看精彩图片

窗口长度就是两个数字相乘

DVR 窗口的长度等于 hls_time × hls_list_size,公式就这么简单,剩下的全是记账工作。想要一个十分钟的滑动窗口,就是 6 秒切片乘以 100 条记录。

打包命令里几个参数各司其职:-g 120 在 60fps 下给出 2 秒的 GOP,正好能整除 6 秒的切片时长;-hls_flags delete_segments+independent_segments 负责滚动删除并标记独立切片;-hls_delete_threshold 5 则让磁盘上多留几个切片作为缓冲。

这里有个容易被忽略的坑:hls_list_size 的默认值是 5。很多人第一次部署直播时压根没设这个参数,结果意外得到了一个三十秒的 DVR 窗口——不是没做,是短到没人发现。

验证窗口是否真的在滑动,看清单文件里的 EXT-X-MEDIA-SEQUENCE。每次刷新它都在增长,而条目数稳定在 100,说明滑动窗口工作正常。

滑动窗口和事件播放列表是两种设计

还有一种形态是完整事件播放列表:只追加、不删除,用 -hls_playlist_type event 开启。它会把 hls_list_size 强制设为 0,于是从开播到现在的每一个切片都留在清单里。

代价是有的。部分播放器在 EVENT 播放列表上拒绝拖动,直到 EXT-X-ENDLIST 出现——而那个标记只在直播结束时才写入。这个问题在 video.js 上有长期未解决的 issue,同类现象也在别处出现过。所以选 EVENT 之前,务必在真实的目标播放器上测试,不能只看桌面 Chrome。

没人做的那项检查:存储跟得上吗

播放列表对外承诺了一个可拖动范围,但背后要有三套系统同时兜底,而它们往往由不同的人在不同的时间配置:

  • 播放列表:hls_time × hls_list_size = 600 秒
  • 源站磁盘:(hls_list_size + hls_delete_threshold) × hls_time = 630 秒
  • 对象存储生命周期:取决于桶策略怎么写的

如果第三项比第一项短,进度条就会给出一个必然 404 的范围。解决办法是把它写进 CI 断言:读取配置里的存储 TTL,分别和播放列表承诺的时长、源站实际保留的时长做比较,任何一项不满足就退出并报错。

九行断言,挡住的是客服工单里最常见的那类故障。

播放器侧:读窗口,而不是读时长

hls.js 会给出直播边缘和可拖动范围,剩下的工作是把它们变成界面。初始化时用 liveSyncDurationCount 控制落后直播边缘的距离,默认是 3,也就是 3 倍的 EXT-X-TARGETDURATION;也可以用秒为单位的 liveSyncDuration,它的优先级更高。backBufferLength 则决定往回保留多少缓冲。

关键数据是 details.totalduration。在直播层级上,它就是当前播放列表的长度,也就是 DVR 窗口的大小。窗口会滑动,所以每次 LEVEL_LOADED 事件都要重新读取,不能只缓存一次。

进度条要跟着移动的窗口走

这里有个陷阱:直播流里 video.duration 是 Infinity,而 seekable.start(0) 会随时间增长。一个绑定 currentTime / duration 的普通 range 输入框在这里毫无用处。

正确做法是映射 seekable 范围:取 seekable.start(0) 和 seekable.end(seekable.length - 1) 得到窗口的起止,再用 (currentTime - start) / span 算出 0 到 1 之间的位置。拖动时如果用户没有正在操作进度条,就把这个值写回控件。

回到直播:别拖到末尾

一个"回到直播"按钮,直觉上应该 seek 到 seekable 范围的末尾。但这样做会卡住。正确目标是 liveSyncPosition,也就是 hls.js 认为的直播同步点,而不是 seekable.end。

按钮的显隐也要跟着状态走:当播放位置落后直播边缘超过一定距离时显示,回到边缘附近时隐藏。这样用户既能往回拖,也能一键回到直播。