From 8d730a2a7537fa8dd75dc59e75e8b5d94f3377ee Mon Sep 17 00:00:00 2001 From: whm <973418690@qq.com> Date: Thu, 26 Mar 2026 10:30:30 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=B4=E6=92=AD=E9=A1=B5=EF=BC=9A=E5=BC=B9?= =?UTF-8?q?=E5=B9=95=20WebSocket=20=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BF=9E?= =?UTF-8?q?=E4=B8=8E=E5=A4=B1=E8=B4=A5=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- web/src/views/LiveRoom.vue | 71 +++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/web/src/views/LiveRoom.vue b/web/src/views/LiveRoom.vue index 34f1422..3a96dbf 100644 --- a/web/src/views/LiveRoom.vue +++ b/web/src/views/LiveRoom.vue @@ -53,6 +53,7 @@ /> +

{{ dmHint }}

@@ -86,9 +87,13 @@ const qualityOptions = LIVE_QUALITY_OPTIONS const captureQualityPref = ref('source') const liveInfoLine = ref('') const dmDraft = ref('') +const dmHint = ref('') const dmItems = ref([]) let dmIdSeq = 0 let dmWs = null +let dmIntentionalClose = false +let dmReconnectTimer = null +let dmReconnectAttempt = 0 const enterUrl = computed(() => (rawLiveUrl.value || '').trim()) @@ -185,11 +190,42 @@ function pushDmLine(text) { }, 12000) } +function scheduleDmReconnect() { + if (dmIntentionalClose) return + if (dmReconnectTimer) return + const delay = Math.min(2500 * Math.pow(1.4, dmReconnectAttempt), 28000) + dmReconnectTimer = window.setTimeout(() => { + dmReconnectTimer = null + if (dmIntentionalClose) return + connectDanmaku() + }, delay) +} + function connectDanmaku() { + if (dmIntentionalClose) return try { dmWs?.close() } catch (_) {} - dmWs = new WebSocket(liveDanmakuWsURL()) + dmWs = null + const url = liveDanmakuWsURL() + dmHint.value = dmReconnectAttempt > 0 ? `弹幕重连中(第 ${dmReconnectAttempt + 1} 次)…` : '弹幕通道连接中…' + try { + dmWs = new WebSocket(url) + } catch (e) { + dmHint.value = '无法创建弹幕连接,请检查网络或地址配置' + dmReconnectAttempt += 1 + scheduleDmReconnect() + return + } + dmWs.onopen = () => { + dmReconnectAttempt = 0 + dmHint.value = '' + } + dmWs.onerror = () => { + if (!dmIntentionalClose) { + dmHint.value = '弹幕 WebSocket 异常(多为网关未放行,见下方说明)' + } + } dmWs.onmessage = (ev) => { let j try { @@ -203,18 +239,32 @@ function connectDanmaku() { } dmWs.onclose = () => { dmWs = null + if (dmIntentionalClose) return + dmHint.value = '弹幕已断开,自动重连中…' + dmReconnectAttempt += 1 + scheduleDmReconnect() } } function sendDm() { const t = dmDraft.value.trim() if (!t) return - if (!dmWs || dmWs.readyState !== WebSocket.OPEN) return - dmWs.send(JSON.stringify({ text: t })) - dmDraft.value = '' + if (!dmWs || dmWs.readyState !== WebSocket.OPEN) { + dmHint.value = + '弹幕未连接,无法发送。请确认:① 已部署含弹幕接口的 API;② Nginx 已为 /api/web/live/danmaku/ws 配置 WebSocket 反代(Upgrade);③ 与直播信令使用同一域名/网关。' + return + } + try { + dmWs.send(JSON.stringify({ text: t })) + dmDraft.value = '' + dmHint.value = '' + } catch (_) { + dmHint.value = '发送失败,请稍后重试或刷新页面' + } } onMounted(async () => { + dmIntentionalClose = false loadCaptureQualityPref() loadHomepage() await nextTick() @@ -230,6 +280,11 @@ onMounted(async () => { }) onUnmounted(() => { + dmIntentionalClose = true + if (dmReconnectTimer) { + clearTimeout(dmReconnectTimer) + dmReconnectTimer = null + } if (liveInfoTimer) { clearInterval(liveInfoTimer) liveInfoTimer = null @@ -397,6 +452,14 @@ onUnmounted(() => { .live-dm-send:hover { background: rgba(0, 212, 255, 0.25); } +.live-dm-hint { + max-width: 480px; + margin: 10px auto 0; + font-size: 12px; + line-height: 1.55; + color: #ffb86c; + text-align: left; +} .live-video-toolbar { display: flex; flex-wrap: wrap;