直播页:弹幕 WebSocket 自动重连与失败提示

Made-with: Cursor
This commit is contained in:
whm
2026-03-26 10:30:30 +08:00
parent 10a842b4ef
commit 8d730a2a75

View File

@@ -53,6 +53,7 @@
/> />
<button type="button" class="live-dm-send" @click="sendDm">发送</button> <button type="button" class="live-dm-send" @click="sendDm">发送</button>
</div> </div>
<p v-if="dmHint" class="live-dm-hint">{{ dmHint }}</p>
</section> </section>
<section class="live-block live-block--divider" aria-label="外链直播间"> <section class="live-block live-block--divider" aria-label="外链直播间">
@@ -86,9 +87,13 @@ const qualityOptions = LIVE_QUALITY_OPTIONS
const captureQualityPref = ref('source') const captureQualityPref = ref('source')
const liveInfoLine = ref('') const liveInfoLine = ref('')
const dmDraft = ref('') const dmDraft = ref('')
const dmHint = ref('')
const dmItems = ref([]) const dmItems = ref([])
let dmIdSeq = 0 let dmIdSeq = 0
let dmWs = null let dmWs = null
let dmIntentionalClose = false
let dmReconnectTimer = null
let dmReconnectAttempt = 0
const enterUrl = computed(() => (rawLiveUrl.value || '').trim()) const enterUrl = computed(() => (rawLiveUrl.value || '').trim())
@@ -185,11 +190,42 @@ function pushDmLine(text) {
}, 12000) }, 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() { function connectDanmaku() {
if (dmIntentionalClose) return
try { try {
dmWs?.close() dmWs?.close()
} catch (_) {} } 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) => { dmWs.onmessage = (ev) => {
let j let j
try { try {
@@ -203,18 +239,32 @@ function connectDanmaku() {
} }
dmWs.onclose = () => { dmWs.onclose = () => {
dmWs = null dmWs = null
if (dmIntentionalClose) return
dmHint.value = '弹幕已断开,自动重连中…'
dmReconnectAttempt += 1
scheduleDmReconnect()
} }
} }
function sendDm() { function sendDm() {
const t = dmDraft.value.trim() const t = dmDraft.value.trim()
if (!t) return if (!t) return
if (!dmWs || dmWs.readyState !== WebSocket.OPEN) return 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 })) dmWs.send(JSON.stringify({ text: t }))
dmDraft.value = '' dmDraft.value = ''
dmHint.value = ''
} catch (_) {
dmHint.value = '发送失败,请稍后重试或刷新页面'
}
} }
onMounted(async () => { onMounted(async () => {
dmIntentionalClose = false
loadCaptureQualityPref() loadCaptureQualityPref()
loadHomepage() loadHomepage()
await nextTick() await nextTick()
@@ -230,6 +280,11 @@ onMounted(async () => {
}) })
onUnmounted(() => { onUnmounted(() => {
dmIntentionalClose = true
if (dmReconnectTimer) {
clearTimeout(dmReconnectTimer)
dmReconnectTimer = null
}
if (liveInfoTimer) { if (liveInfoTimer) {
clearInterval(liveInfoTimer) clearInterval(liveInfoTimer)
liveInfoTimer = null liveInfoTimer = null
@@ -397,6 +452,14 @@ onUnmounted(() => {
.live-dm-send:hover { .live-dm-send:hover {
background: rgba(0, 212, 255, 0.25); 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 { .live-video-toolbar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;