直播页:弹幕 WebSocket 自动重连与失败提示
Made-with: Cursor
This commit is contained in:
@@ -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) {
|
||||||
dmWs.send(JSON.stringify({ text: t }))
|
dmHint.value =
|
||||||
dmDraft.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 () => {
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user