|
|
@ -102,13 +102,19 @@ const activeLive = reactive({ |
|
|
dockId: '', |
|
|
dockId: '', |
|
|
sessionId: '', |
|
|
sessionId: '', |
|
|
playUrl: '', |
|
|
playUrl: '', |
|
|
|
|
|
playUrlExpiresAt: 0, |
|
|
phase: '', |
|
|
phase: '', |
|
|
leaseExpiresAt: 0, |
|
|
leaseExpiresAt: 0, |
|
|
}) |
|
|
}) |
|
|
|
|
|
const PLAY_URL_FRESH_MS = 30_000 |
|
|
|
|
|
const PLAY_EXPIRE_FALLBACK_SEC = 270 |
|
|
let heartbeatTimer = null |
|
|
let heartbeatTimer = null |
|
|
let openSeq = 0 |
|
|
let openSeq = 0 |
|
|
let disposed = false |
|
|
let disposed = false |
|
|
let rejoinBusy = false |
|
|
let rejoinBusy = false |
|
|
|
|
|
let playUrlPromise = null |
|
|
|
|
|
let playUrlSessionId = '' |
|
|
|
|
|
let opening = false |
|
|
|
|
|
|
|
|
const LEASE_INVALID_CODE = 57006 |
|
|
const LEASE_INVALID_CODE = 57006 |
|
|
|
|
|
|
|
|
@ -144,14 +150,59 @@ function clearHeartbeat() { |
|
|
|
|
|
|
|
|
function resetActiveLive() { |
|
|
function resetActiveLive() { |
|
|
clearHeartbeat() |
|
|
clearHeartbeat() |
|
|
|
|
|
playUrlPromise = null |
|
|
|
|
|
playUrlSessionId = '' |
|
|
activeLive.id = '' |
|
|
activeLive.id = '' |
|
|
activeLive.dockId = '' |
|
|
activeLive.dockId = '' |
|
|
activeLive.sessionId = '' |
|
|
activeLive.sessionId = '' |
|
|
activeLive.playUrl = '' |
|
|
activeLive.playUrl = '' |
|
|
|
|
|
activeLive.playUrlExpiresAt = 0 |
|
|
activeLive.phase = '' |
|
|
activeLive.phase = '' |
|
|
activeLive.leaseExpiresAt = 0 |
|
|
activeLive.leaseExpiresAt = 0 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function ensurePlayUrl(dockId, sessionId, { force = false } = {}) { |
|
|
|
|
|
if (!dockId || !sessionId) return null |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
!force && |
|
|
|
|
|
activeLive.sessionId === sessionId && |
|
|
|
|
|
activeLive.playUrl && |
|
|
|
|
|
activeLive.playUrlExpiresAt * 1000 - Date.now() > PLAY_URL_FRESH_MS |
|
|
|
|
|
) { |
|
|
|
|
|
return { |
|
|
|
|
|
playUrl: activeLive.playUrl, |
|
|
|
|
|
expiresAt: activeLive.playUrlExpiresAt, |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (playUrlPromise && playUrlSessionId === sessionId) { |
|
|
|
|
|
return playUrlPromise |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const seq = openSeq |
|
|
|
|
|
playUrlSessionId = sessionId |
|
|
|
|
|
playUrlPromise = getLivePlayURL(dockId, sessionId) |
|
|
|
|
|
.then((play) => { |
|
|
|
|
|
if (disposed || seq !== openSeq || activeLive.sessionId !== sessionId) return play |
|
|
|
|
|
const nowSec = Math.floor(Date.now() / 1000) |
|
|
|
|
|
const expiresAt = Number(play?.expiresAt) || nowSec + PLAY_EXPIRE_FALLBACK_SEC |
|
|
|
|
|
if (play?.playUrl) { |
|
|
|
|
|
activeLive.playUrl = play.playUrl |
|
|
|
|
|
activeLive.playUrlExpiresAt = expiresAt |
|
|
|
|
|
} |
|
|
|
|
|
return play |
|
|
|
|
|
}) |
|
|
|
|
|
.finally(() => { |
|
|
|
|
|
if (playUrlSessionId === sessionId) { |
|
|
|
|
|
playUrlPromise = null |
|
|
|
|
|
playUrlSessionId = '' |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
return playUrlPromise |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function scheduleHeartbeat(expiresAt) { |
|
|
function scheduleHeartbeat(expiresAt) { |
|
|
clearHeartbeat() |
|
|
clearHeartbeat() |
|
|
if (disposed || !activeLive.dockId || !activeLive.sessionId) return |
|
|
if (disposed || !activeLive.dockId || !activeLive.sessionId) return |
|
|
@ -165,6 +216,18 @@ function scheduleHeartbeat(expiresAt) { |
|
|
const result = await heartbeatLive(dockId, sessionId) |
|
|
const result = await heartbeatLive(dockId, sessionId) |
|
|
if (disposed || activeLive.sessionId !== sessionId || activeLive.dockId !== dockId) return |
|
|
if (disposed || activeLive.sessionId !== sessionId || activeLive.dockId !== dockId) return |
|
|
if (result?.session?.phase) activeLive.phase = result.session.phase |
|
|
if (result?.session?.phase) activeLive.phase = result.session.phase |
|
|
|
|
|
if (result?.session?.phase === 'streaming' && !activeLive.playUrl) { |
|
|
|
|
|
try { |
|
|
|
|
|
await ensurePlayUrl(dockId, sessionId) |
|
|
|
|
|
} catch (playErr) { |
|
|
|
|
|
if (disposed || activeLive.sessionId !== sessionId) return |
|
|
|
|
|
if (isLeaseInvalidError(playErr)) { |
|
|
|
|
|
await rejoinAfterLeaseInvalid(streamId, dockId) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
console.warn('[media] ensure play-url after heartbeat failed', playErr) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
scheduleHeartbeat(result.leaseExpiresAt) |
|
|
scheduleHeartbeat(result.leaseExpiresAt) |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
if (disposed || activeLive.sessionId !== sessionId || activeLive.dockId !== dockId) return |
|
|
if (disposed || activeLive.sessionId !== sessionId || activeLive.dockId !== dockId) return |
|
|
@ -236,24 +299,23 @@ async function load() { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function openLive(stream) { |
|
|
async function openLive(stream) { |
|
|
if (disposed || !stream?.dockId) return |
|
|
|
|
|
|
|
|
if (disposed || !stream?.dockId || opening) return |
|
|
if (activeLive.id === stream.id && activeLive.playUrl) { |
|
|
if (activeLive.id === stream.id && activeLive.playUrl) { |
|
|
await closeActiveLive() |
|
|
await closeActiveLive() |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// closeActiveLive 会 bump openSeq,使旧 in-flight join 失效;之后再取本次 seq |
|
|
|
|
|
await closeActiveLive() |
|
|
await closeActiveLive() |
|
|
|
|
|
if (disposed) return |
|
|
|
|
|
opening = true |
|
|
const seq = ++openSeq |
|
|
const seq = ++openSeq |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
const result = await joinLiveWithRetry(stream.dockId, {}, { |
|
|
const result = await joinLiveWithRetry(stream.dockId, {}, { |
|
|
shouldContinue: () => seq === openSeq && !disposed, |
|
|
|
|
|
|
|
|
shouldContinue: () => seq === openSeq && !disposed && opening, |
|
|
}) |
|
|
}) |
|
|
const session = result?.session |
|
|
const session = result?.session |
|
|
const sessionId = session?.id |
|
|
const sessionId = session?.id |
|
|
|
|
|
|
|
|
// 页面已切走/关闭:不要写入状态,立刻释放刚拿到的 viewer lease |
|
|
|
|
|
if (seq !== openSeq || disposed) { |
|
|
if (seq !== openSeq || disposed) { |
|
|
if (sessionId) await safeLeave(stream.dockId, sessionId) |
|
|
if (sessionId) await safeLeave(stream.dockId, sessionId) |
|
|
return |
|
|
return |
|
|
@ -269,6 +331,7 @@ async function openLive(stream) { |
|
|
activeLive.sessionId = sessionId |
|
|
activeLive.sessionId = sessionId |
|
|
activeLive.phase = session.phase || '' |
|
|
activeLive.phase = session.phase || '' |
|
|
activeLive.playUrl = '' |
|
|
activeLive.playUrl = '' |
|
|
|
|
|
activeLive.playUrlExpiresAt = 0 |
|
|
scheduleHeartbeat(result.leaseExpiresAt) |
|
|
scheduleHeartbeat(result.leaseExpiresAt) |
|
|
|
|
|
|
|
|
if (session.phase !== 'streaming') { |
|
|
if (session.phase !== 'streaming') { |
|
|
@ -276,11 +339,13 @@ async function openLive(stream) { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const play = await getLivePlayURL(stream.dockId, sessionId) |
|
|
|
|
|
|
|
|
const play = await ensurePlayUrl(stream.dockId, sessionId) |
|
|
if (seq !== openSeq || disposed || activeLive.sessionId !== sessionId) return |
|
|
if (seq !== openSeq || disposed || activeLive.sessionId !== sessionId) return |
|
|
if (play?.playUrl && !play.playUrl.startsWith('fake://')) { |
|
|
if (play?.playUrl && !play.playUrl.startsWith('fake://')) { |
|
|
|
|
|
if (!activeLive.playUrl) { |
|
|
activeLive.playUrl = play.playUrl |
|
|
activeLive.playUrl = play.playUrl |
|
|
} else { |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
} else if (!activeLive.playUrl) { |
|
|
ui.toast('控制面已验证,当前未配置可播放媒体流') |
|
|
ui.toast('控制面已验证,当前未配置可播放媒体流') |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
@ -291,6 +356,8 @@ async function openLive(stream) { |
|
|
} |
|
|
} |
|
|
await closeActiveLive() |
|
|
await closeActiveLive() |
|
|
ui.toast(e.message || '打开直播失败') |
|
|
ui.toast(e.message || '打开直播失败') |
|
|
|
|
|
} finally { |
|
|
|
|
|
opening = false |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|