Browse Source

feat: poll live phase and renew HLS play URLs

main
xiaosi 2 weeks ago
parent
commit
3e41f56f72
  1. 185
      src/composables/useMonitorLive.js

185
src/composables/useMonitorLive.js

@ -1,8 +1,31 @@
import { computed, reactive } from 'vue' import { computed, reactive } from 'vue'
import { getLivePlayURL, heartbeatLive, joinLive, leaveLive } from '@/api/live'
import {
getLivePlayURL,
getLiveSession,
heartbeatLive,
joinLive,
leaveLive,
stopLive,
} from '@/api/live'
const PHASE_POLL_MS = 2500
const PHASE_TIMEOUT_MS = 60_000
const PLAY_RETRY_MAX = 3
const PLAY_EXPIRE_FALLBACK_SEC = 270
const PLAY_REFRESH_LEAD_MS = 30_000
export function useMonitorLive({ getDockId, ui, getLivePlayer }) { export function useMonitorLive({ getDockId, ui, getLivePlayer }) {
const live = reactive({ session: null, dockId: '', playUrl: '', heartbeatTimer: null })
const live = reactive({
session: null,
dockId: '',
playUrl: '',
playUrlExpiresAt: 0,
heartbeatTimer: null,
phasePollTimer: null,
playRefreshTimer: null,
playRetryCount: 0,
phasePollStartedAt: 0,
})
const livePhaseText = computed(() => { const livePhaseText = computed(() => {
if (!live.session) return '待机' if (!live.session) return '待机'
@ -17,6 +40,25 @@ export function useMonitorLive({ getDockId, ui, getLivePlayer }) {
const canFullscreenLive = computed(() => !!live.playUrl && !live.playUrl.startsWith('fake://')) const canFullscreenLive = computed(() => !!live.playUrl && !live.playUrl.startsWith('fake://'))
function clearTimers() {
window.clearTimeout(live.heartbeatTimer)
window.clearTimeout(live.phasePollTimer)
window.clearTimeout(live.playRefreshTimer)
live.heartbeatTimer = null
live.phasePollTimer = null
live.playRefreshTimer = null
}
function resetLocal() {
clearTimers()
live.session = null
live.dockId = ''
live.playUrl = ''
live.playUrlExpiresAt = 0
live.playRetryCount = 0
live.phasePollStartedAt = 0
}
function toggleLive() { function toggleLive() {
if (live.session) closeLive() if (live.session) closeLive()
else openLive() else openLive()
@ -38,28 +80,110 @@ export function useMonitorLive({ getDockId, ui, getLivePlayer }) {
const result = await joinLive(dockId) const result = await joinLive(dockId)
live.session = result.session live.session = result.session
live.dockId = dockId live.dockId = dockId
live.playUrl = ''
live.playUrlExpiresAt = 0
live.playRetryCount = 0
scheduleLiveHeartbeat(result.leaseExpiresAt) scheduleLiveHeartbeat(result.leaseExpiresAt)
if (result.session.phase === 'streaming') await refreshPlayURL()
if (result.session.phase === 'streaming') {
try {
await refreshPlayURL()
} catch (e) {
await onPlayError(e)
}
} else {
startPhasePolling()
}
} catch (e) { } catch (e) {
ui.toast(e.message || '打开直播失败') ui.toast(e.message || '打开直播失败')
} }
} }
async function refreshPlayURL() {
function startPhasePolling() {
window.clearTimeout(live.phasePollTimer)
live.phasePollStartedAt = Date.now()
live.phasePollTimer = window.setTimeout(pollPhaseOnce, PHASE_POLL_MS)
}
async function pollPhaseOnce() {
if (!live.dockId || !live.session?.id) return if (!live.dockId || !live.session?.id) return
const play = await getLivePlayURL(live.dockId, live.session.id)
if (Date.now() - live.phasePollStartedAt > PHASE_TIMEOUT_MS) {
ui.toast('直播启动超时')
await closeLive(false)
return
}
const sessionId = live.session.id
try {
// http 解包后是 LiveSession 本体,不是 { session }
const session = await getLiveSession(live.dockId, sessionId)
if (!live.session || live.session.id !== sessionId) return
live.session = session
if (session.phase === 'streaming') {
window.clearTimeout(live.phasePollTimer)
live.phasePollTimer = null
try {
await refreshPlayURL()
} catch (e) {
await onPlayError(e)
}
return
}
if (session.phase === 'failed' || session.phase === 'stopped') {
ui.toast(session.phase === 'failed' ? '直播启动失败' : '直播已停止')
await closeLive(false)
return
}
} catch (e) {
ui.toast(e.message || '查询直播状态失败')
await closeLive(false)
return
}
live.phasePollTimer = window.setTimeout(pollPhaseOnce, PHASE_POLL_MS)
}
// 成功返回 true;失败抛错,由调用方决定是否 onPlayError(避免互相递归)
async function refreshPlayURL() {
if (!live.dockId || !live.session?.id) return false
const sessionId = live.session.id
const play = await getLivePlayURL(live.dockId, sessionId)
if (!live.session || live.session.id !== sessionId) return false
live.playUrl = play.playUrl || '' live.playUrl = play.playUrl || ''
const nowSec = Math.floor(Date.now() / 1000)
live.playUrlExpiresAt = Number(play.expiresAt) || nowSec + PLAY_EXPIRE_FALLBACK_SEC
live.playRetryCount = 0
schedulePlayRefresh()
return true
}
function schedulePlayRefresh() {
window.clearTimeout(live.playRefreshTimer)
if (!live.playUrl || live.playUrl.startsWith('fake://') || !live.playUrlExpiresAt) return
const delay = Math.max(5000, live.playUrlExpiresAt * 1000 - Date.now() - PLAY_REFRESH_LEAD_MS)
live.playRefreshTimer = window.setTimeout(async () => {
try {
await refreshPlayURL()
} catch (e) {
await onPlayError(e)
}
}, delay)
} }
function scheduleLiveHeartbeat(expiresAt) { function scheduleLiveHeartbeat(expiresAt) {
window.clearTimeout(live.heartbeatTimer) window.clearTimeout(live.heartbeatTimer)
const delay = Math.max(5000, (Number(expiresAt) * 1000 - Date.now()) / 2) const delay = Math.max(5000, (Number(expiresAt) * 1000 - Date.now()) / 2)
live.heartbeatTimer = window.setTimeout(async () => { live.heartbeatTimer = window.setTimeout(async () => {
if (!live.dockId || !live.session?.id) return
try { try {
const result = await heartbeatLive(live.dockId, live.session.id) const result = await heartbeatLive(live.dockId, live.session.id)
if (!live.session) return
live.session = result.session live.session = result.session
scheduleLiveHeartbeat(result.leaseExpiresAt) scheduleLiveHeartbeat(result.leaseExpiresAt)
if (result.session.phase === 'streaming' && !live.playUrl) await refreshPlayURL()
if (result.session.phase === 'streaming' && !live.playUrl) {
try {
await refreshPlayURL()
} catch (e) {
await onPlayError(e)
}
}
} catch (e) { } catch (e) {
ui.toast(e.message || '直播观看已结束') ui.toast(e.message || '直播观看已结束')
await closeLive(false) await closeLive(false)
@ -67,13 +191,30 @@ export function useMonitorLive({ getDockId, ui, getLivePlayer }) {
}, delay) }, delay)
} }
async function onPlayError(err) {
if (!live.session) return
if (live.playRetryCount >= PLAY_RETRY_MAX) {
ui.toast(err?.message || '播放失败')
live.playRetryCount = 0
return
}
live.playRetryCount += 1
try {
await refreshPlayURL()
} catch (e) {
if (live.playRetryCount >= PLAY_RETRY_MAX) {
ui.toast(e?.message || err?.message || '播放失败')
live.playRetryCount = 0
} else {
await onPlayError(e)
}
}
}
async function closeLive(sendRequest = true) { async function closeLive(sendRequest = true) {
window.clearTimeout(live.heartbeatTimer)
const session = live.session const session = live.session
const dockId = live.dockId const dockId = live.dockId
live.session = null
live.dockId = ''
live.playUrl = ''
resetLocal()
if (sendRequest && session && dockId) { if (sendRequest && session && dockId) {
try { try {
await leaveLive(dockId, session.id) await leaveLive(dockId, session.id)
@ -81,6 +222,28 @@ export function useMonitorLive({ getDockId, ui, getLivePlayer }) {
} }
} }
async function stopLiveStream() {
if (!live.session || !live.dockId) return
const dockId = live.dockId
const session = live.session
const ok = await ui.confirm(
'确认停止直播?',
'停止后所有正在观看的用户都会失去画面。',
['确认停止推流', '影响当前所有观看者']
)
if (!ok) return
try {
await stopLive(dockId)
resetLocal()
try {
await leaveLive(dockId, session.id)
} catch (_) {}
ui.toast('直播已停止')
} catch (e) {
ui.toast(e.message || '停止直播失败')
}
}
return { return {
live, live,
livePhaseText, livePhaseText,
@ -89,5 +252,7 @@ export function useMonitorLive({ getDockId, ui, getLivePlayer }) {
fullscreenLive, fullscreenLive,
openLive, openLive,
closeLive, closeLive,
stopLiveStream,
onPlayError,
} }
} }

Loading…
Cancel
Save