Browse Source
Specify stopping-gate behavior: keep old streamSessionId until phase=stopped, disable reopen controls meanwhile, and retry join on 57007.main
1 changed files with 130 additions and 0 deletions
@ -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…
Reference in new issue