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
stopLiveStreamto poll until stopped
Behavior:
- confirm
- capture
dockId+session live.stopping = true- clear playUrl + heartbeat/play timers (keep session/dockId)
await stopLive(dockId)thensafeLeave- poll
getLiveSession(dockId, session.id)every 1.5s - on
phase === 'stopped':openGen++,resetLocal(), toast直播已停止 - on timeout/error: toast, keep
stopping=trueuntil dispose/dock leave; do not allow silent reopen - 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
stoppingindetailLiveView
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
joinLiveWithRetryinopenLive
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
- stop disables play/stop, shows 正在停止
- polls old session until stopped
- reopen blocked until stopped
- new session id used after join
- 57007 retries
- viewer leave still immediate
- MediaView join retries 57007