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;