From e0a858f36b5ec889e0fadec0b4d69a8bba9b35ac Mon Sep 17 00:00:00 2001 From: xiaosi <2652281683@qq.com> Date: Wed, 2 Sep 2026 18:42:52 +0800 Subject: [PATCH] docs: add live stop phase gate design Specify stopping-gate behavior: keep old streamSessionId until phase=stopped, disable reopen controls meanwhile, and retry join on 57007. --- .../2026-09-02-live-stop-phase-gate-design.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-02-live-stop-phase-gate-design.md diff --git a/docs/superpowers/specs/2026-09-02-live-stop-phase-gate-design.md b/docs/superpowers/specs/2026-09-02-live-stop-phase-gate-design.md new file mode 100644 index 0000000..213d1ae --- /dev/null +++ b/docs/superpowers/specs/2026-09-02-live-stop-phase-gate-design.md @@ -0,0 +1,130 @@ +# Live Stop Phase Gate Design + +Date: 2026-09-02 +Status: Approved for implementation planning + +## Problem + +Stopping a live stream currently clears local state immediately after `POST /v1/live/{dockId}/stop`. The UI then allows another `POST /v1/live/{dockId}/sessions` while the previous session may still be `stopping`. That can race the backend and produce join failures (business code `57007`) or stale session polling against an old `streamSessionId`. + +## Scope + +In scope: + +- Monitor live stop/reopen flow in `useMonitorLive` +- Monitor detail panel / LivePlayer button disable + phase text +- Join retries for business code `57007` in Monitor and MediaView + +Out of scope: + +- Changing backend stop/join contracts +- Treating viewer leave (`DELETE .../viewers/me`) as a stop-phase gate +- Redesigning MediaView stop-push controls (MediaView has no stop-push button) + +## Goals + +1. After clicking stop, do not allow re-open until the old session reports `phase=stopped`. +2. Keep the old `streamSessionId` and poll it until stopped. +3. On reopen, always use the newly returned `streamSessionId` for phase poll and play-url. +4. If join returns `57007`, wait ~1.5s and retry a few times instead of immediate failure toast. +5. Keep heartbeat independent of play retries; stop path clears playback and stops heartbeat for the stopping session. + +## Non-Goals + +- Global dock cooldown timers unrelated to an actual stopping session +- Auto-stop on viewer leave +- Changing HLS 404 startup retry behavior + +## Approach + +Use a local **stopping gate** owned by `useMonitorLive`. + +Recommended over pure `57007` retries because the UI can disable controls before the user clicks again, and over a dock-wide cooldown because the gate is tied to a concrete previous session. + +## State Machine + +States relevant to this change: + +| Local condition | UI phase text | Play toggle | Stop button | +|---|---|---|---| +| stop request in flight / `stopping` | 正在停止 | disabled | disabled | +| old session `stopped` (gate cleared) | 已停止 | enabled | hidden | +| join in flight (`opening`) | 正在启动 | disabled | hidden | +| session `starting` / `reconnecting` | 启动中 / 重连中 | enabled (leave viewer) | enabled | +| session `streaming` | 直播中 | enabled (leave viewer) | enabled | + +### Stop path (`stopLiveStream`) + +1. Confirm dialog unchanged. +2. Set local `stopping=true`, clear `playUrl` and timers/heartbeat. +3. Keep `dockId` + old `session` (`streamSessionId`) for polling. +4. Call `POST /v1/live/{dockId}/stop`. +5. Optionally leave viewer for the old session. +6. Poll `GET /v1/live/{dockId}/sessions/{oldSessionId}` until: + - `phase === 'stopped'` → clear stopping gate, allow reopen + - timeout / hard failure → toast, keep controls conservative (do not silently reopen) +7. While gate is active, `openLive()` and `toggleLive()` open-path are no-ops / disabled. + +### Reopen path (`openLive`) + +1. Reject if `disposed`, `opening`, `stopping`, or an active non-stopped session exists. +2. `POST /v1/live/{dockId}/sessions`. +3. On success, replace local session with the **new** `result.session` only. +4. If new session `phase === 'streaming'`, request play-url with the **new** session id. +5. Otherwise start phase polling with the **new** session id. +6. On business code `57007`: + - wait 1500ms + - retry up to 3 additional attempts (4 total tries) + - if still failing, toast and exit opening + +### Leave viewer path (`closeLive` / play toggle off) + +Unchanged: invalidate generation, clear local state, leave viewer. No stop-phase polling. + +## API Error Handling + +| Code | Meaning for frontend | Action | +|---|---|---| +| `57006` | lease invalid | existing rejoin flow | +| `57007` | previous session still stopping / not ready to create | wait 1.5s and retry join a few times | +| other | normal failure | toast | + +`http.js` already attaches business `code` onto rejected errors; reuse that. + +## UI Binding Changes + +- `liveView.opening` true for join-in-flight +- add `liveView.stopping` (or derive from session phase + local flag) to disable LivePlayer control and hide/disable stop button +- `livePhaseText`: + - local stop request / phase `stopping` → `正在停止` + - phase `stopped` / idle after gate clear → `已停止` + - opening with no session yet → `正在启动` / `打开中` + +## MediaView + +No stop-push gate. Only add the same `57007` join retry helper so a dock still stopping from Monitor does not hard-fail MediaView open. + +## Risks + +- Stop poll timeout leaves user blocked: mitigate with explicit toast and a manual clear/reset on dock switch / dispose. +- Accidental reuse of old session id after reopen: mitigate by replacing `live.session` only from join response and scoping all polls/play-url/heartbeat to that id. +- Race: user switches selected dock while stopping: gate must be dock/session scoped and disposed with page leave. + +## Acceptance Criteria + +1. Click stop → play and stop controls disabled; UI shows 正在停止. +2. Frontend keeps polling the old `streamSessionId` until `stopped`. +3. Only after `stopped` can play start a new join. +4. New join uses new `streamSessionId` for subsequent poll and play-url. +5. Join `57007` auto-retries with short delay instead of immediate failure. +6. Viewer leave still clears immediately and does not enter the stopping gate. +7. MediaView join also retries on `57007`. + +## Implementation Touchpoints + +- `src/composables/useMonitorLive.js` +- `src/components/MonitorDetailPanel.vue` (button disable bindings) +- `src/components/LivePlayer.vue` (disable control while stopping) +- `src/views/MonitorView/MonitorView.vue` (pass stopping into liveView) +- `src/views/MediaView/MediaView.vue` (`57007` retry on open) +- optionally tiny shared helper for join-with-57007-retry near `src/api/live.js`