|
|
@ -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, |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|