HLS 切到后台或锁屏后停止:以恢复为先的诊断指南

诊断 HLS 在切换标签页、切换应用、手机锁屏、页面冻结或浏览器丢弃后出现暂停、延迟或恢复失败的问题。

HLS 播放器原本运行正常,用户切换标签页、切到其他应用、锁定手机或让设备闲置后,问题才出现。返回页面时,画面可能停住、声音消失、播放位置远远落后直播,或者下一次播放列表请求失败。这些现象看起来相似,起因却可能分属不同层级:应用代码、浏览器生命周期管理、操作系统媒体策略、已经过期的直播窗口、授权,或媒体管线。

不要承诺网页在所有设备上隐藏后都能继续播放视频。应把“持续后台播放”和“可靠恢复前台播放”视为两个独立的产品要求。请只测试自己拥有或获准检查的流,并从共享日志中删除签名 URL、Cookie、令牌、设备标识、IP 地址和私有主机名。

*验证方法 — 2026 年 9 月 24 日:*我们使用 Node.js 24.12.0 运行了一个确定性的恢复策略测试。构造的直播窗口从媒体时间 40–70 推进到 58–88。保存位置 42 已经过期,因此测试选择了已声明的直播同步点 76;保存位置 62 仍可拖动,因此得到保留。0–300 范围内的 VOD 位置 120 得到保留;HTTP 403 选择重新授权,而不是反复重试媒体;wasDiscarded: true 则选择重新初始化直播。这只验证测试中的范围与决策计算。它没有测试浏览器生命周期转换、HLS 播放器、媒体解码、网络、手机锁屏、操作系统后台策略或实体设备。测试脚本位于 scripts/test-hls-background-recovery.cjs。

准确命名正在测试的转换

“切到后台就停了”不是可复现的报告。修改代码前,应记录准确的转换动作和预期行为。

转换页面可能观察到什么不能据此证明什么
另一个浏览器标签页变为活动状态visibilitychange,且 document.visibilityState === "hidden"页面已被冻结、丢弃,或获准继续播放音频
浏览器或应用进入后台可见性变化可能是最后一个可靠事件后续卸载类事件一定会执行
屏幕锁定文档可以变成隐藏状态每种浏览器和操作系统都会保留相同的媒体或网络策略
页面冻结可冻结任务停止运行,包括许多计时器和回调渲染进程已经销毁
页面被丢弃页面为节省资源被移除,稍后重新加载JavaScript 在被丢弃时收到了事件
组件卸载或路由变化应用可能销毁播放器或清空媒体源浏览器强制执行了后台策略

MDN 说明,切换标签页、最小化窗口和在移动端切换应用都会触发 visibilitychange。Chrome 的页面生命周期指南区分隐藏、冻结、终止和丢弃状态,并提醒在移动端,进入隐藏状态通常是页面最后一个能够可靠观察到的转换。这些是有用的信号,但不是媒体一定能够跨平台继续播放的保证。

在页面消失前采集证据

在受控版本中加入临时诊断记录器。记录单调时间、墙上时钟时间、可见性、媒体状态、当前位置、全部可拖动和已缓冲范围,以及最后一个播放器/网络事件。不要记录完整签名 URL。

function ranges(value) {
  return Array.from(
    { length: value.length },
    (_, index) => ({ start: value.start(index), end: value.end(index) })
  );
}

function snapshot(video, event) {
  console.info('media-lifecycle', {
    event,
    monotonicMs: performance.now(),
    visibility: document.visibilityState,
    paused: video.paused,
    ended: video.ended,
    currentTime: video.currentTime,
    readyState: video.readyState,
    networkState: video.networkState,
    seekable: ranges(video.seekable),
    buffered: ranges(video.buffered),
  });
}

document.addEventListener('visibilitychange', () => snapshot(video, 'visibilitychange'));
for (const name of ['pause', 'play', 'playing', 'waiting', 'stalled', 'suspend', 'emptied', 'error']) {
  video.addEventListener(name, () => snapshot(video, name));
}

如果播放器提供清单、分段、缓冲、层级和致命错误事件,也应把它们记录在同一条单调时间线上。在支持的环境中采集 pagehide、pageshow、freeze 和 resume,但绝不能把 unload 当作保存状态或遥测数据的唯一位置。

先排除应用主动造成的停止

很多后台故障其实由应用自己造成。搜索每一处 pause()、媒体源移除、播放器 destroy() 或 detachMedia()、路由清理、状态重置和 visibilitychange 处理程序。视图隐藏时,框架组件可能被卸载。节能处理程序可能有意停止加载,却没有配套的恢复路径。两个生命周期监听器也可能产生竞态:一个恢复播放,另一个又写回旧的暂停状态。

为每一次主动转换记录原因,不要只记“已暂停”。诊断版本中的堆栈记录可以找出 pause() 包装函数或销毁流程的调用方。使用同一条获准测试的流,对比最小播放器页面和完整应用。如果最小页面可以恢复而完整应用不行,应先检查应用对播放器的管理,再归咎于 HLS 或 CDN。

返回后的第一项证据可能的边界下一项受控检查
播放器实例不存在或媒体源为空组件生命周期或销毁流程跟踪卸载、destroy、媒体源赋值和状态恢复
媒体暂停,但没有失败请求主动暂停、平台策略或用户意图丢失记录事件序列并测试用户明确触发的继续播放
play() 被拒绝浏览器播放策略或媒体状态不可用记录 Promise 拒绝的名称和消息,不要吞掉错误
播放列表恢复加载,但正文仍旧CDN 缓存或暂停的重载循环比较时间戳、媒体序列号、Age 和重复正文
第一个请求返回 401/403授权过期或签名子资源 URL 过期通过批准的流程刷新授权
旧分段返回 404/410保存的直播位置已经离开保留窗口对比新的播放列表窗口和可拖动范围
字节已到达,但解码没有恢复媒体管线、时间戳、编解码器或不连续点保留播放器错误并检查返回后的第一段媒体

返回后重新读取时间轴

不要假定进入后台前保存的位置仍然存在。加载被限速或停止时,滑动直播播放列表仍可能继续推进。读取新的媒体播放列表,再采集 video.seekable、播放器直播同步点、所选呈现版本、不连续点以及保存的 currentTime。

对于直播,应在三种状态中做出判断:

  1. 保存位置仍可拖动:如果产品支持用户主动回看 DVR,就保留该位置。
  2. 保存位置已过期:加入当前范围内经过验证的直播同步目标。
  3. 还没有可靠范围:重新加载时间轴并等待,不要跳到凭空编造的位置。

对于 VOD,只有保存位置落在当前可拖动范围内时才恢复它。无论直播还是 VOD,都应把目标限制在真实范围内。MDN 指出,直播媒体可能失去较早内容,所请求的 currentTime 也可能被调整到媒体支持的位置。

function contains(ranges, time) {
  return ranges.some(range => time >= range.start && time <= range.end);
}

function chooseLiveTarget({ savedTime, ranges, liveSyncPosition }) {
  if (!ranges.length) return null;
  if (contains(ranges, savedTime)) return savedTime;

  const latest = ranges.at(-1);
  return Math.min(latest.end, Math.max(latest.start, liveSyncPosition));
}

可用时,应采用播放器文档说明的直播同步点,而不是数学意义上的末端。直播延迟与“回到直播”指南解释了为什么安全目标通常要落后最后一个已公布瞬间。如果拖动后向前或向后跳转,请参考 HLS 拖动与 DVR 诊断。

把播放意图当作状态,不要靠猜测

页面变为隐藏状态前,保存播放是否由用户请求、当前位置、内容标识、流类型、所选轨道、音量和经过脱敏的授权版本。不要把 paused === false 等同于可以永远自动重启:页面隐藏时,用户可能已经通过系统媒体控件暂停播放。

返回后:

  1. 只有播放器已销毁或页面曾被丢弃时,才重新创建播放器。
  2. 证据显示授权过期时刷新授权;不要对 401/403 循环重试。
  3. 加载当前播放列表并等待真实可拖动范围。
  4. 根据产品承诺选择 VOD、DVR 或直播同步目标。
  5. 只有保存的用户意图仍要求播放时才调用 play()。
  6. 处理返回的 Promise;如果被拒绝,提供明确的“点击继续”控件。
  7. 只有收到 playing 且 currentTime 持续前进后,才把恢复标记为完成,而不是在方法调用后立即完成。

这样可以让恢复操作保持幂等。反复触发 visibilitychange 或框架重新渲染,不应创建多个 HLS 实例、重复监听器、互相竞争的播放列表重载或循环的播放/暂停调用。

区分授权失败和媒体失败

设备闲置时,短期签名经常过期。主播放列表可能仍在缓存中,但下一个媒体播放列表、密钥、初始化段或分段使用的子 URL 已经过期。记录返回后的第一个请求所对应的资源类型和脱敏状态码。

拖动或清空媒体缓冲无法修复 401 或 403。应通过服务支持的流程刷新会话,或取得新授权的播放 URL。绝不能把一个来源的凭据附加到另一个来源、公开签名 URL,或为让后台恢复看似成功而削弱访问控制。

如果播放列表是新的,但已公布对象返回 404,请使用陈旧播放列表与缺失分段判断路径。如果返回后只是请求太慢,HLS 卡顿诊断会分别检查吞吐量、分段可用性、解码和播放器状态。

为真实恢复状态设计界面

界面应区分“已暂停”“正在重新连接”“正在返回直播”“落后直播”和“点击继续”。页面仍在使用旧播放列表,或播放位置尚未前进时,不要显示“直播中”。用户主动选择的 DVR 位置只要仍然受支持就应保留;另行提供“回到直播”,不要悄悄覆盖它。

如果后台音频是产品要求,应明确记录受支持的浏览器、安装模式、操作系统、内容类型、用户手势要求、锁屏控件和中断行为。一个桌面标签页测试成功,并不能证明 iOS Safari、已安装 PWA、Android Chrome、WebView 或原生应用也能成功。

在实体设备上执行转换测试矩阵

自动化能够验证决策代码和页面渲染,但平台策略需要实体设备。使用同一条获准测试的流,并让每次转换持续足够久,使直播窗口或签名确实发生推进。

测试维度至少记录的情况
设备路径按产品支持范围测试手机、平板、桌面、已安装 PWA、WebView 或原生封装
转换切换标签页、切换应用、锁屏、解锁、来电等中断、浏览器进程回收
持续时间短时间返回、超过一个目标时长周期、超过 DVR 窗口、超过 URL 有效期
媒体VOD、滑动直播、EVENT/DVR、带音频的视频,以及受支持时的纯音频
网络网络不变、隐藏期间离线、Wi-Fi 切换蜂窝网络、强制门户重新认证
预期结果继续播放、安全暂停、恢复保存位置、加入直播同步点,或要求用户操作

每个测试都应记录设备与操作系统版本、浏览器或 WebView 版本、播放器版本、转换时间、实际收到的可见性/生命周期事件、保存的意图、前后播放列表序列、可拖动范围、返回后的第一个请求、播放 Promise 结果,以及确认进入 playing 所需的时间。没有实体测试的平台必须标为仅查阅文档或未测试。

围绕一次返回过程编写错误报告

一份可执行的报告可以写成:“媒体时间 42 时,测试手机在直播范围 40–70 内锁屏。返回后当前范围为 58–88,因此 42 已经过期。刷新后的播放列表请求成功,播放器给出的直播同步点为 76;用户授权的一次播放请求后,播放在 76 进入 playing。”这能指出时间轴过期,而不是只说“移动端停止播放”。

报告应包含预期后台行为、保存的播放意图、准确的转换动作、实际触发的生命周期事件、转换前后时间轴、脱敏网络证据、播放器所有权变化、Promise 拒绝详情和实体设备身份。没有对应证据时,不要声称操作系统杀死、冻结或丢弃了页面。

主要参考资料

从用户实际执行的转换开始,沿着第一项发生变化的状态或请求继续追踪。页面消失前保存播放意图,返回后重新建立当前时间轴,再恢复到受支持的位置,而不是重放陈旧状态。即使平台不保证持续后台播放,这套方法仍能让前台恢复变得可测试。