Browse Source

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.
main
xiaosi 2 weeks ago
parent
commit
e0a858f36b
  1. 130
      docs/superpowers/specs/2026-09-02-live-stop-phase-gate-design.md

130
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`
Loading…
Cancel
Save