You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

7.0 KiB

直播 play-url 按 session 缓存与合并

日期:2026-09-03
状态:已确认(待写实现计划)
范围:仅前端 laic-frontend(Monitor + Media)

1. 背景

开播进入 streaming 后,openLive、phase poll、heartbeat 都可能触发 GET /v1/live/{dockId}/play-url。现状:

位置 已有能力 缺口
useMonitorLive live.opening 防抖;playURLRequest 全局 in-flight 合并 不按 sessionId不复用未过期 playUrl;多入口仍可能在短窗口内各打一次
MediaView open 后单次 getLivePlayURLopenSeq 丢弃过期回写 无 opening 锁无 in-flight 合并无 TTL 缓存
LivePlayer 启动期 404 同 URL 退避,不上冒刷 play-url 已满足,本轮仅回归

后端建议前端按 session 做 ensurePlayUrl:合并同 session 的 in-flight,并在地址仍有效时直接复用。

2. 目标

  1. 同一 streamSessionId/play-url in-flight coalesce(并发只打一枪)。
  2. 当前 session 已有 playUrl,且 expiresAt * 1000 - Date.now() > 30_000直接复用,不重打。
  3. Monitor 的 openLive(streaming)、pollPhaseOnce、heartbeat(需补地址时)、临期续期、有限错误重建,统一ensurePlayUrl
  4. Media 同样按 session 缓存/合并;openLive 增加 opening 连点忽略锁。
  5. force、临期续期、或明确需要强制刷新的路径才重新 GET。
  6. 请求完成后再次确认当前 session 未变再写入;页面关闭/切换清空 promise 与缓存。

3. 非目标

  • 不改后端 /play-url 契约与鉴权。
  • 不抽 Monitor/Media 公共模块(两页各自实现同构逻辑)。
  • 不改 LivePlayer 启动 404 同 URL 策略、stop phase gate、57006 rejoin、TLS/403 致命不刷 URL。
  • 不改设备状态轮询等无关模块。

4. 方案

4.1 核心语义 ensurePlayUrl(sessionId, { force = false })

伪代码(两页各自落地,字段名可本地化):

let playUrlPromise = null
let playUrlSessionId = ''

async function ensurePlayUrl(sessionId, { force = false } = {}) {
  if (
    !force &&
    activeSessionId === sessionId &&
    playUrl &&
    playUrlExpiresAt * 1000 - Date.now() > 30_000
  ) {
    return { playUrl, expiresAt: playUrlExpiresAt } // 或直接 true / 沿用现有写入形态
  }

  if (
    !force &&
    activeSessionId === sessionId &&
    playUrl &&
    playUrlExpiresAt * 1000 - Date.now() > PLAY_URL_FRESH_MS
  ) {
    return { playUrl, expiresAt: playUrlExpiresAt }
  }

  if (playUrlPromise && playUrlSessionId === sessionId) {
    return playUrlPromise
  }

  playUrlSessionId = sessionId
  playUrlPromise = getLivePlayURL(dockId, sessionId)
    .then((play) => {
      if (activeSessionId !== sessionId) return play // 调用方仍须再校验后再写入本地状态
      return play
    })
    .finally(() => {
      if (playUrlSessionId === sessionId) playUrlPromise = null
    })

  return playUrlPromise
常量`PLAY_URL_FRESH_MS = 30_000`与现有临期续期 lead 对齐)。**写入** `playUrl` / `expiresAt` / `schedulePlayRefresh` 由调用方在 ensure resolve  session 仍匹配后执行Monitor 可把写入收进 ensure 内部但必须含二次 session 校验)。

常量:PLAY_URL_FRESH_MS = 30_000(与现有临期续期 lead 对齐)。

4.2 Monitor:useMonitorLive.js

  1. 将现有 refreshPlayURL / playURLRequest 升级为按 session 的 ensure 语义:
    • 默认 force=false(复用 + coalesce)
    • fromRetry、临期 schedulePlayRefresh、显式需要换址 → force=true
  2. 调用点统一:
    • openLivephase === 'streaming'
    • pollPhaseOnce 进入 streaming 后取址
    • heartbeat:仅当 phase === 'streaming' && !live.playUrl(现状),内部走 ensure(通常命中缓存或 in-flight)
    • schedulePlayRefresh 到期 → force
    • onPlayError 有限次重建 → force
  3. 保留 live.opening 锁(已有)。
  4. clearPlaybackOnly / resetLocal / dispose:清空 playUrlPromiseplayUrlSessionId,并清 playUrl / expiresAt(与现有一致)。
  5. ensure 返回后若 live.session.id !== sessionId 或 disposed/stopping → 不写入

可选:对外仍导出/保留函数名 refreshPlayURL({ force, fromRetry }),内部转 ensure,减少调用点改名噪音。

4.3 Media:MediaView.vue

  1. 增加 opening(或复用 busy 标志):openLive 入口若已 opening → 直接 return;finally 解锁。
  2. 增加与 Monitor 同构的 playUrlPromise / playUrlSessionId / playUrlExpiresAt(若尚未存 expiresAt,从 play-url 响应写入)。
  3. openLive 取址改为 ensurePlayUrl(sessionId);若后续 heartbeat 在无 playUrl 时补址,同样走 ensure。
  4. closeActiveLive / tab 切离 live / pagehide:清空 promise、session 键、playUrl、expiresAt(bump openSeq 逻辑保留)。
  5. 回写前校验 openSeq + activeLive.sessionId === sessionId

4.4 与 LivePlayer 的边界

  • 启动期 m3u8 404:仍由 LivePlayer 同 URL 窗口消化,recoverable 不上冒 → 触发 ensure/force。
  • TLS/403:Monitor 仍 markPlayFatal,禁止刷 play-url。
  • 仅非 HlsError 的有限重试 / 临期 / 显式 force 才重新 GET。

5. 错误与并发

场景 行为
同 session 并发 ensure 共享同一 promise
不同 session 不复用旧 promise;旧回写被 session 校验丢弃
TTL 内非 force 返回缓存,零 HTTP
force / 临期 新 GET;可与旧 in-flight 并存时以新 session 键为准,旧结果丢弃
ensure 失败 抛给现有 onPlayError / Media toast 路径;不清掉「仍有效」的旧地址,除非调用方 close
401 现有 http 拦截器

6. 验收

  1. Monitor 开播至出画:同一 session 短时间内 /play-url 不应被 open + phase + heartbeat 打成多份并发;未过期无故不重拉。
  2. Media 开播同样;连点 open 不会并行多个 join。
  3. 启动期 HLS 404 仍只同 URL 重载,Network 无因此刷出的 /play-url
  4. 临期(剩余 ≤30s)或 force 重建才会重新 GET;完成后 session 已变则不写入。
  5. 离开监控直播 / Media 直播 tab / 关页后,无过期 in-flight 回写到新会话。
  6. stop gate、57006、TLS/403 行为与改前一致。
  7. npm run build 通过。

7. 风险

  • 30s 新鲜阈值与临期续期 lead 相同;若后端 expiresAt 很短,force 会更频繁——可接受。
  • Monitor / Media 两份同构逻辑可能漂移;本轮不抽公共模块,实现计划里用同一段伪代码约束。

8. 实现落点(预告)

文件 变更
src/composables/useMonitorLive.js ensure 语义升级 refreshPlayURL;统一调用点;清理 promise
src/views/MediaView/MediaView.vue opening 锁 + ensurePlayUrl + expiresAt + 清理
src/components/LivePlayer.vue 不改;回归启动 404

完成后:实现计划 → 编码 → 构建部署 → Network 人工看开播 /play-url 次数。