From d179b6d3d9514329490ce0fda701c5b8dcb45f62 Mon Sep 17 00:00:00 2001 From: xiaosi <2652281683@qq.com> Date: Mon, 21 Sep 2026 18:54:46 +0800 Subject: [PATCH] fix(live): keep HLS playback near the live edge Normal 6-12s HLS kept the hls.js defaults (3 fragments back, no max latency), so playback drifted far behind the edge. Target two fragments, cap at four, and add a latency probe that snaps back when past 20s. --- src/components/LivePlayer.vue | 36 ++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/LivePlayer.vue b/src/components/LivePlayer.vue index 2e81a9b..115a117 100644 --- a/src/components/LivePlayer.vue +++ b/src/components/LivePlayer.vue @@ -46,6 +46,11 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue' const STARTUP_RETRY_DELAYS_MS = [500, 1000, 2000, 4000, 6000, 8000] const STARTUP_WINDOW_MS = 30_000 +// 落后边缘的分片数上限;必须 > LIVE_SYNC_DURATION_COUNT,否则 hls.js 直接抛错 +const LIVE_SYNC_DURATION_COUNT = 2 +const LIVE_MAX_LATENCY_DURATION_COUNT = 4 +// 落后超过该秒数时强制跳到直播边缘(hls.js 内置 synchronizeToLiveEdge 的上限) +const LIVE_EDGE_HARD_LIMIT_SEC = 20 const props = defineProps({ playUrl: { type: String, default: '' }, @@ -199,6 +204,22 @@ function scheduleSameUrlRetry(url, seq, httpCode = 404) { return true } +function logLiveLatency() { + if (!hls) return + const latency = Number(hls.latency) + if (!Number.isFinite(latency) || latency <= 0) return + if (latency > LIVE_EDGE_HARD_LIMIT_SEC) { + console.warn(`[live-player] 落后直播边缘 ${latency.toFixed(1)}s,强制贴边`) + const video = videoEl.value + const syncPosition = hls.liveSyncPosition + if (video && Number.isFinite(syncPosition)) { + video.currentTime = syncPosition + } + return + } + console.debug(`[live-player] 落后直播边缘 ${latency.toFixed(1)}s`) +} + function bindMediaListeners(video, seq) { clearMediaListeners() const onPlay = () => { @@ -328,10 +349,18 @@ async function attachPlayer(url, { seq: existingSeq } = {}) { return } - // 现网为普通 6–12s HLS,不是 LL-HLS;放宽分片重试,启动 404 仍由同 URL 窗口控制 + // 现网为普通 6–12s HLS(非 LL-HLS):lowLatencyMode 无 partHoldBack 可用, + // 靠 liveSyncDurationCount/liveMaxLatencyDurationCount 追边,重建后也能贴近边缘。 hls = new Hls({ enableWorker: true, + // 非 LL-HLS 下 true 无收益(partHoldBack=0 时回落 holdBack),保持关闭 lowLatencyMode: false, + liveSyncDurationCount: LIVE_SYNC_DURATION_COUNT, + liveMaxLatencyDurationCount: LIVE_MAX_LATENCY_DURATION_COUNT, + maxLiveSyncPlaybackRate: 1.5, + maxBufferLength: 8, + // 0 会被内部抬到 1×targetduration,且卡顿后无法回退;留 30s 回退窗 + backBufferLength: 30, manifestLoadingMaxRetry: 2, manifestLoadingRetryDelay: 1000, levelLoadingMaxRetry: 2, @@ -409,6 +438,11 @@ async function attachPlayer(url, { seq: existingSeq } = {}) { video.muted = true tryPlay() }) + // 观测落后直播边缘的秒数;重建后按需强制贴边 + hls.on(Hls.Events.LEVEL_UPDATED, () => { + if (seq !== attachSeq) return + logLiveLatency() + }) } catch (_) { if (seq === attachSeq) emitFatal(buildPlayError({ message: 'hls init failed' })) }