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。
對直播而言,應在三種狀態中做出判斷:
- 儲存位置仍可拖曳:如果產品支援使用者主動回看 DVR,就保留該位置。
- 儲存位置已過期:加入目前範圍內經過驗證的直播同步目標。
- 還沒有可靠範圍:重新載入時間軸並等待,不要跳到憑空編造的位置。
對 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 等同於可以永遠自動重新開始:頁面隱藏時,使用者可能已經透過系統媒體控制暫停播放。
返回後:
- 只有播放器已銷毀或頁面曾被捨棄時,才重新建立播放器。
- 證據顯示授權過期時更新授權;不要對 401/403 循環重試。
- 載入目前播放清單並等待真實可拖曳範圍。
- 根據產品承諾選擇 VOD、DVR 或直播同步目標。
- 只有儲存的使用者意圖仍要求播放時才呼叫
play()。 - 處理回傳的 Promise;如果被拒絕,提供明確的「點選繼續」控制項。
- 只有收到
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 拒絕詳細資料和實體裝置身分。沒有對應證據時,不要聲稱作業系統終止、凍結或捨棄了頁面。
主要參考資料
- MDN:Page Visibility API
- MDN:
visibilitychange事件 - Chrome for Developers:Page Lifecycle API
- MDN:HTMLMediaElement
play() - MDN:HTMLMediaElement
currentTime - RFC 8216:HTTP Live Streaming
- hls.js API:liveSyncPosition、latency 和生命週期方法
從使用者實際執行的轉換開始,沿著第一項發生變化的狀態或要求繼續追蹤。頁面消失前儲存播放意圖,返回後重新建立目前時間軸,再復原到受支援的位置,而不是重播過期狀態。即使平台不保證持續背景播放,這套方法仍能讓前景復原變得可測試。