From 999fbf82fc36f059f7a7d12409eabfd4fcb6bc51 Mon Sep 17 00:00:00 2001 From: xiaosi <2652281683@qq.com> Date: Wed, 2 Sep 2026 15:24:02 +0800 Subject: [PATCH] fix: stop play-url retry storm on HLS TLS failures vpull HTTPS handshake fails with ERR_SSL_VERSION_OR_CIPHER_MISMATCH; refreshing auth_key cannot fix it and only floods Network. --- src/components/LivePlayer.vue | 26 +++++++++++++--- src/composables/useMonitorLive.js | 52 ++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/components/LivePlayer.vue b/src/components/LivePlayer.vue index 54b020e..cf4d2b5 100644 --- a/src/components/LivePlayer.vue +++ b/src/components/LivePlayer.vue @@ -99,7 +99,9 @@ async function attachPlayer(url) { video.addEventListener('error', () => { if (seq !== attachSeq) return - emit('error') + const err = new Error('media error') + err.name = 'HlsError' + emit('error', err) }, { once: true }) async function tryPlay() { @@ -109,7 +111,7 @@ async function attachPlayer(url) { } catch (error) { console.warn('[live-player] autoplay blocked', error) // NotAllowedError: autoplay policy. Do not storm play-url rebuilds. - if (error?.name !== 'NotAllowedError' && seq === attachSeq) emit('error') + if (error?.name !== 'NotAllowedError' && seq === attachSeq) emit('error', error) } } @@ -127,10 +129,26 @@ async function attachPlayer(url) { return } - hls = new Hls({ enableWorker: true, lowLatencyMode: true }) + hls = new Hls({ + enableWorker: true, + lowLatencyMode: true, + manifestLoadingMaxRetry: 1, + levelLoadingMaxRetry: 1, + fragLoadingMaxRetry: 1, + }) hls.on(Hls.Events.ERROR, (_event, data) => { if (seq !== attachSeq) return - if (data?.fatal) emit('error') + if (!data?.fatal) return + const detail = [data.type, data.details, data.response?.code, data.error?.message] + .filter(Boolean) + .join(' ') + const err = new Error(detail || 'hls fatal') + err.name = /ssl|tls|cipher|certificate|cors|mixed content|err_ssl/i.test(detail) + ? 'SecurityError' + : 'HlsError' + console.warn('[live-player] hls fatal', data) + destroyPlayer() + emit('error', err) }) hls.loadSource(url) hls.attachMedia(video) diff --git a/src/composables/useMonitorLive.js b/src/composables/useMonitorLive.js index 2cdc8b6..91decff 100644 --- a/src/composables/useMonitorLive.js +++ b/src/composables/useMonitorLive.js @@ -14,6 +14,21 @@ const PLAY_RETRY_MAX = 3 const PLAY_EXPIRE_FALLBACK_SEC = 270 const PLAY_REFRESH_LEAD_MS = 30_000 +function isFatalPlayMediaError(err) { + const msg = String(err?.message || err || '').toLowerCase() + const name = String(err?.name || '') + return ( + name === 'SecurityError' || + msg.includes('ssl') || + msg.includes('tls') || + msg.includes('cipher') || + msg.includes('err_ssl') || + msg.includes('mixed content') || + msg.includes('cors') || + msg.includes('certificate') + ) +} + export function useMonitorLive({ getDockId, ui, getLivePlayer }) { const live = reactive({ session: null, @@ -59,6 +74,15 @@ export function useMonitorLive({ getDockId, ui, getLivePlayer }) { live.phasePollStartedAt = 0 } + function markPlayFatal(message) { + window.clearTimeout(live.playRefreshTimer) + live.playRefreshTimer = null + live.playUrl = '' + live.playUrlExpiresAt = 0 + live.playRetryCount = PLAY_RETRY_MAX + ui.toast(message) + } + function toggleLive() { if (live.session) closeLive() else openLive() @@ -197,21 +221,35 @@ export function useMonitorLive({ getDockId, ui, getLivePlayer }) { async function onPlayError(err) { if (!live.session) return + + // 播放器媒体面失败(TLS/CORS/HLS network):换 auth_key 没用,禁止刷 play-url + if (isFatalPlayMediaError(err) || err?.name === 'HlsError') { + markPlayFatal( + isFatalPlayMediaError(err) + ? '播放域名 HTTPS 不可用(TLS/证书异常),请检查阿里云播放域名证书' + : (err?.message || '播放失败') + ) + return + } + if (live.playRetryCount >= PLAY_RETRY_MAX) { - ui.toast(err?.message || '播放失败') - live.playRetryCount = 0 + markPlayFatal(err?.message || '播放失败') return } + live.playRetryCount += 1 try { await refreshPlayURL({ fromRetry: true }) } catch (e) { - if (live.playRetryCount >= PLAY_RETRY_MAX) { - ui.toast(e?.message || err?.message || '播放失败') - live.playRetryCount = 0 - } else { - await onPlayError(e) + if (isFatalPlayMediaError(e) || e?.name === 'HlsError' || live.playRetryCount >= PLAY_RETRY_MAX) { + markPlayFatal( + isFatalPlayMediaError(e) + ? '播放域名 HTTPS 不可用(TLS/证书异常),请检查阿里云播放域名证书' + : (e?.message || err?.message || '播放失败') + ) + return } + await onPlayError(e) } }