5.6 KiB
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
57007in 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
- After clicking stop, do not allow re-open until the old session reports
phase=stopped. - Keep the old
streamSessionIdand poll it until stopped. - On reopen, always use the newly returned
streamSessionIdfor phase poll and play-url. - If join returns
57007, wait ~1.5s and retry a few times instead of immediate failure toast. - 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)
- Confirm dialog unchanged.
- Set local
stopping=true, clearplayUrland timers/heartbeat. - Keep
dockId+ oldsession(streamSessionId) for polling. - Call
POST /v1/live/{dockId}/stop. - Optionally leave viewer for the old session.
- 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)
- While gate is active,
openLive()andtoggleLive()open-path are no-ops / disabled.
Reopen path (openLive)
- Reject if
disposed,opening,stopping, or an active non-stopped session exists. POST /v1/live/{dockId}/sessions.- On success, replace local session with the new
result.sessiononly. - If new session
phase === 'streaming', request play-url with the new session id. - Otherwise start phase polling with the new session id.
- 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.openingtrue 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 →
正在启动/打开中
- local stop request / phase
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.sessiononly 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
- Click stop → play and stop controls disabled; UI shows 正在停止.
- Frontend keeps polling the old
streamSessionIduntilstopped. - Only after
stoppedcan play start a new join. - New join uses new
streamSessionIdfor subsequent poll and play-url. - Join
57007auto-retries with short delay instead of immediate failure. - Viewer leave still clears immediately and does not enter the stopping gate.
- MediaView join also retries on
57007.
Implementation Touchpoints
src/composables/useMonitorLive.jssrc/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(57007retry on open)- optionally tiny shared helper for join-with-57007-retry near
src/api/live.js