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.
 
 
 
 

6.8 KiB

Live Stop Phase Gate Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: After stop, keep the old streamSessionId, poll until phase=stopped, disable reopen until then, and retry join on business code 57007.

Architecture: Add a stopping gate inside useMonitorLive: stop clears playback/heartbeat but retains dock+session for stop-phase polling; reopen is blocked until stopped. Shared joinLiveWithRetry handles 57007 for Monitor and MediaView. UI gets a stopping flag to disable play/stop controls.

Tech Stack: Vue 3, Pinia-free composable, axios /api live endpoints

Spec: docs/superpowers/specs/2026-09-02-live-stop-phase-gate-design.md


Task 1: Shared join retry helper

Files:

  • Modify: src/api/live.js

  • Step 1: Add constants + joinLiveWithRetry

const JOIN_BUSY_CODE = 57007
const JOIN_BUSY_RETRY_DELAY_MS = 1500
const JOIN_BUSY_MAX_ATTEMPTS = 4

function sleep(ms) {
  return new Promise((resolve) => window.setTimeout(resolve, ms))
}

export function isJoinBusyError(err) {
  return Number(err?.code) === JOIN_BUSY_CODE
}

/** join with short backoff when previous session is still stopping (57007) */
export async function joinLiveWithRetry(dockId, payload = {}, { shouldContinue } = {}) {
  let lastError
  for (let attempt = 1; attempt <= JOIN_BUSY_MAX_ATTEMPTS; attempt += 1) {
    if (shouldContinue && !shouldContinue()) {
      const err = new Error('join aborted')
      err.code = 'ABORTED'
      throw err
    }
    try {
      return await joinLive(dockId, payload)
    } catch (e) {
      lastError = e
      if (!isJoinBusyError(e) || attempt >= JOIN_BUSY_MAX_ATTEMPTS) throw e
      await sleep(JOIN_BUSY_RETRY_DELAY_MS)
    }
  }
  throw lastError
}
  • Step 2: Commit
git add src/api/live.js
git commit -m "feat: retry live join on 57007"

Task 2: Stopping gate in useMonitorLive

Files:

  • Modify: src/composables/useMonitorLive.js

  • Step 1: Extend live reactive state

Add:

  • live.stopping = false
  • constants: STOP_POLL_MS = 1500, STOP_TIMEOUT_MS = 60_000
  • import/use joinLiveWithRetry, isJoinBusyError

In resetLocal(), also set live.stopping = false.

  • Step 2: Update phase text / guards
const livePhaseText = computed(() => {
  if (live.stopping) return '正在停止'
  if (live.opening && !live.session) return '正在启动'
  if (!live.session) return '待机'
  // existing phase mapping...
})

function toggleLive() {
  if (disposed || live.opening || live.stopping) return
  if (live.session) closeLive()
  else openLive()
}

openLive must reject when live.stopping or active session exists. Replace joinLive with joinLiveWithRetry(..., { shouldContinue: () => isCurrentOpen(gen) }). Ignore aborted join errors silently.

  • Step 3: Rewrite stopLiveStream to poll until stopped

Behavior:

  1. confirm
  2. capture dockId + session
  3. live.stopping = true
  4. clear playUrl + heartbeat/play timers (keep session/dockId)
  5. await stopLive(dockId) then safeLeave
  6. poll getLiveSession(dockId, session.id) every 1.5s
  7. on phase === 'stopped': openGen++, resetLocal(), toast 直播已停止
  8. on timeout/error: toast, keep stopping=true until dispose/dock leave; do not allow silent reopen
  9. while stopping, stop button/open are disabled via guards

Helper sketch:

function clearPlaybackOnly() {
  window.clearTimeout(live.heartbeatTimer)
  window.clearTimeout(live.playRefreshTimer)
  live.heartbeatTimer = null
  live.playRefreshTimer = null
  playURLRequest = null
  live.playUrl = ''
  live.playUrlExpiresAt = 0
  live.playRetryCount = 0
}

async function waitUntilStopped(dockId, sessionId, gen) {
  const startedAt = Date.now()
  while (isCurrentOpen(gen) && live.stopping) {
    if (Date.now() - startedAt > STOP_TIMEOUT_MS) {
      throw new Error('停止超时,请稍后重试')
    }
    const session = await getLiveSession(dockId, sessionId)
    if (!isCurrentOpen(gen)) return false
    live.session = session
    if (session.phase === 'stopped') return true
    await new Promise((r) => window.setTimeout(r, STOP_POLL_MS))
  }
  return false
}
  • Step 4: Dispose/close clear stopping gate

closeLive / dispose must clear stopping and invalidate generation as today.

  • Step 5: Smoke-check via node syntax + commit
node --check src/composables/useMonitorLive.js
git add src/composables/useMonitorLive.js
git commit -m "feat: gate reopen until live session stopped"

Task 3: Wire UI disable flags

Files:

  • Modify: src/views/MonitorView/MonitorView.vue

  • Modify: src/components/MonitorDetailPanel.vue

  • Modify: src/components/LivePlayer.vue

  • Step 1: Pass stopping in detailLiveView

const detailLiveView = computed(() => ({
  playUrl: live.playUrl,
  phase: live.session?.phase,
  active: !!live.session && !live.stopping,
  opening: !!live.opening,
  stopping: !!live.stopping,
  phaseText: livePhaseText.value,
  canFullscreen: canFullscreenLive.value,
  cameraLabel: isDrone.value ? '无人机图传' : '机巢摄像头',
}))
  • Step 2: Detail panel stop button

Show stop only when liveView.active && !liveView.stopping; disable when stopping.

  • Step 3: LivePlayer disable while stopping

Add prop stopping: Boolean, disable control when opening || stopping, show status 正在停止.

  • Step 4: Commit
git add src/views/MonitorView/MonitorView.vue src/components/MonitorDetailPanel.vue src/components/LivePlayer.vue
git commit -m "feat: disable live controls while stopping"

Task 4: MediaView 57007 retry

Files:

  • Modify: src/views/MediaView/MediaView.vue

  • Step 1: Use joinLiveWithRetry in openLive

Import joinLiveWithRetry and call it with shouldContinue: () => seq === openSeq && !disposed.

  • Step 2: Commit
git add src/views/MediaView/MediaView.vue
git commit -m "feat: media join retries when live still stopping"

Task 5: Build, push, deploy

  • Step 1: Build
npm run build
  • Step 2: Push + deploy dist
git push origin main
tar -C dist -czf - . | ssh -o BatchMode=yes jg-serv1 'rm -rf /usr/share/nginx/laic-frontend/dist/* && tar -C /usr/share/nginx/laic-frontend/dist -xzf - && cat /usr/share/nginx/laic-frontend/dist/index.html'
  • Step 3: Verify acceptance mentally against spec checklist
  1. stop disables play/stop, shows 正在停止
  2. polls old session until stopped
  3. reopen blocked until stopped
  4. new session id used after join
  5. 57007 retries
  6. viewer leave still immediate
  7. MediaView join retries 57007