video: stop dropping the first press of the record-all toggle - #3109
rafaellehmkuhl wants to merge 7 commits into
Conversation
|
| # | Problem | What it means | Severity | Status |
|---|---|---|---|---|
| 7.1 | Wait-for-video loop added inside an already large recording function | The recording start routine got noticeably harder to follow and change safely, and it handles the same "wait for the video" problem differently from the resume code next to it. | major | ❌ |
| 1.1 | Cancelling a waiting start never closes the stream | If the operator presses record and then cancels before the video arrives, a camera no widget is showing keeps streaming in the background until Cockpit is restarted. | minor | ❌ |
| 1.2 | Checking the toggle state opens streams | Every joystick press opens every available camera stream just to check whether it is recording. On the web version that includes the RTSP cameras the PR meant to skip, which pops the "RTSP not supported" dialog. | minor | ❌ |
| 6.1 | Several timed-out starts replace each other's dialogs | When several cameras fail to start in one press, the operator only reads the error for the last one. | minor | ❌ |
| 6.2 | Recorder widget shows nothing while a start waits | Clicking record on the widget can do nothing visible for up to ten seconds, and clicking again is silently ignored, so it looks broken. | minor | ❌ |
| 7.2 | Two start checks can no longer fail | Two error messages in the start routine can never be shown any more, which leaves dead code that misleads readers. | minor | ❌ |
| 8.1 | Commits from the stacked PR are still in the branch | Merging as-is would bring in 14 commits that belong to another PR. | minor | ❌ |
Change map — what was established before judging
- Claims
- Symptom 1: the toggle does the opposite of what was asked after a recording was started or stopped from the widget — verified. Base
src/stores/video.ts:1494-1499reads onlyisRecordingAllStreams, which is written only at:1458and:1477. The widget path (src/components/mini-widgets/MiniVideoRecorder.vue:340,366) never touches it. - Symptom 2: a stream that is still connecting refuses the start, yet the batch reports success and sets the flag — verified. Base
video.ts:967-970refuses the start. Base:1460-1464pushes the stream intostreamsThatStartedwithout waiting for the result, and:1458sets the flag regardless. - Mechanism: the start waits up to 10 s, a stop cancels it, a second start is ignored, and the stream is held open while waiting and released on timeout — partly verified. The wait is at head
video.ts:1173-1192, the ignored second start at:1174, the stop cancel at:1183, and the release on timeout at:1189. A stop during the wait does not release the stream (1.1). - Success is reported only once recorders run — verified (
:1760-1764, read straight fromactiveStreams). - RTSP streams are left out on Lite — partly verified. The start batch filters them at
:1725-1727, but the toggle's probe at:1793and the stop loop at:1776still callisRecordingon them, and that activates them (1.2).
- Symptom 1: the toggle does the opposite of what was asked after a recording was started or stopped from the widget — verified. Base
- Failure site: base
src/stores/video.ts:1494-1499(the toggle reads a flag) and:954-970plus:1456-1472(refused start counted as started). Both are in the diff. - Entry points
| Function | Reached from | Frequency |
|---|---|---|
toggleRecordingAllStreams |
registerActionCallback(toggle_recording_all_streams, useThrottleFn(…, 3000)), base video.ts:1626 (joystick button) |
per user action |
startRecordingAllStreams |
the toggle above, and start_recording_all_streams action (base :1618) |
per user action |
stopRecordingAllStreams |
the toggle, and stop_recording_all_streams action (base :1622) |
per user action |
isRecordingOrAboutTo |
the toggle and stopRecordingAllStreams |
per user action |
startRecording |
MiniVideoRecorder.vue:366 click; startRecordingAllStreams; recordAgain in the resume watcher/unmute listener (#3060) |
per user action |
stopRecording |
MiniVideoRecorder.vue:340; stopRecordingAllStreams; the stream-config watcher (video.ts:~601) |
per user action |
deactivateStreamIfUnused |
unregisterStreamConsumer (widget unmount/switch); recorder onstop; the start timeout at :1189 |
per user action |
- Invariants
- A stream that a start's wait activated is released when the wait ends. There are three ways a wait can end. The timeout releases the stream (
:1189). The stop cancel returns at:1183without releasing it. ThestopRecordingno-recorder branch (:1126-1135) releases only when a resume was waiting. So 1 of 2 cancel paths is covered (1.1). - Probing whether a stream records must not activate it. The PR states this itself at
:1757-1759and readsactiveStreamsdirectly for the success check. Other probes still go throughisRecording→getStreamData→activateStream(base:735-740,:837-840):isRecordingOrAboutTo(:1714, reached from the toggle:1793and stop-all:1776) and the batch loop at:1732. So 1 of 4 sites is covered. The chokepoint isisRecordingOrAboutTo(1.2).
- A stream that a start's wait activated is released when the wait ends. There are three ways a wait can end. The timeout releases the stream (
1. Correctness & Implementation Bugs — 2 findings
1.1 minor — a stop during the wait leaves the stream it activated running.
Consequence: if the operator presses record on a camera no widget shows and then cancels before its video arrives, that camera keeps streaming over the tether until Cockpit is restarted.
The wait activates the stream through isStreamReadyToRecord → getStreamData (src/stores/video.ts:1173, base :735-740). deactivateStreamIfUnused then refuses to tear it down while the start is in recordingStartsWaitingForVideo (head :~727). When stopRecording deletes the entry (:~834), the loop exits and returns at :1183 with no release. The no-recorder branch of stopRecording (:1126-1135) calls deactivateStreamIfUnused only when wasWaitingToResume. The PR body says the stream is "released if the wait gives up", but that is only true for the timeout.
Fix: in stopRecording, record const wasWaitingToStart = recordingStartsWaitingForVideo.delete(streamName). Then call deactivateStreamIfUnused in the no-recorder branch for wasWaitingToResume || wasWaitingToStart. That is one place, and it also covers the PR's "widget releases the stream mid-wait, then the user cancels" case.
1.2 minor — the toggle's state probe activates every available stream, including RTSP on Lite.
Consequence: every joystick press opens a session for each camera nobody is watching, and on Lite the first press pops the "RTSP streams are not supported in Cockpit Lite" dialog that the new RTSP filter was meant to avoid.
toggleRecordingAllStreams runs namesAvailableStreams.value.some(isRecordingOrAboutTo) (:1793), and stopRecordingAllStreams loops the same predicate (:1776). isRecordingOrAboutTo calls isRecording (:1714), which calls getStreamData and so activateStream for any stream not in the map (base :837-840, :735-740). For an RTSP stream on Lite, that is the dialog at base :535-547. The isRecordable filter at :1725-1727 only covers the start batch. The PR's own comment at :1757-1759 warns that isRecording "would activate again a stream a timed-out start just released". The toggle then does exactly that on the next press.
Fix: close it at the chokepoint. isRecordingOrAboutTo should read activeStreams.value[streamName]?.timeRecordingStart !== undefined instead of isRecording(streamName). A stream that is not in the map is not recording, so nothing is lost. The batch loop at :1732 can use the same read.
6. UI / UX — 2 findings
6.1 minor — a batch whose streams all time out opens one dialog per stream, each replacing the last.
Consequence: with two or more cameras still connecting, the operator only sees "did not start sending video" for the last camera, and does not learn that the others failed too.
startRecordingAllStreams starts every stream in parallel (:1760). Each timed-out startRecording calls showDialog with its own stream name (:1187), and they all fire around the same 10 s mark. The PR's sibling reportUnexpectedStop (:~932-936) already handles this pattern: it names each stream in an alert and shows one generic dialog. The guideline on dialog spam (section 6, and the AGENTS.md "User feedback" rule) applies here.
Fix: push the per-stream message as an Alert and show a stream-agnostic dialog. Alternatively, have startRecordingAllStreams show one dialog listing the streams that are not in streamsThatStarted.
6.2 minor — a start from the recorder widget that has to wait gives no feedback, and a second click is silently dropped.
Consequence: clicking record on the widget can look like it did nothing for up to ten seconds, and clicking again really does nothing, so the button feels broken.
MiniVideoRecorder.vue:359 only checks connected, so a stream that is connected but has no first frame yet enters the new wait (:1173). isRecording stays false, so the widget keeps showing idle. A second click goes to startRecording, which returns at :1174 without a word. The snackbar at :1753-1755 is only in the batch path. The AGENTS.md "User feedback" rule requires visible feedback for every discrete action.
Fix: move the "Starting to record…" snackbar into startRecording, on the branch that adds the stream to recordingStartsWaitingForVideo. Both entry points then get it, and the batch's copy can go.
7. Code Quality & Style — 2 findings
7.1 major — the wait-for-video loop raises startRecording to complexity 23, up from 15 (per complexity-report.json, trigger gained-8-while-already-above-12).
Consequence: the routine that starts every recording now also polls, cancels and releases streams, so a change to any one of those has to be read against the others.
The added lines at src/stores/video.ts:1173-1192 bring a concern that startRecording did not carry before: polling for readiness against a deadline, cancellation through a shared set, and releasing the stream on timeout. They sit ahead of a function that already builds the recorder, monitors and finalization. The entry point is per user action, but the shape is the problem. #3060's resumeRecordingWhenStreamReturns (:~720-824) already waits for the same isStreamReadyToRecord condition with a watch plus an unmute listener, so the file now has two mechanisms for one wait.
Fix: extract the one cohesive unit with a real name, waitForStreamReadyToRecord(streamName): Promise<boolean>. It owns the set entry, the deadline and the release on failure, and ideally reuses the watch/unmute approach from the resume path instead of a 100 ms poll. startRecording then reduces to if (!isStreamReadyToRecord(streamName) && !(await waitForStreamReadyToRecord(streamName))) return.
7.2 minor — the "Media stream not defined" and "not yet active" guards are now unreachable.
Consequence: two user-facing error paths can never fire, which suggests a failure mode that no longer exists.
To reach :1194 at all, isStreamReadyToRecord must have returned true (:1173 or :1184), and there is no await between that check and :1194-1203. isStreamReadyToRecord already requires mediaStream?.active === true (:~676). So streamData?.mediaStream === undefined (:1195) and !isStreamReadyToRecord (:1200) are always false. Before this commit, :1200 was the only readiness check and was live.
Fix: delete both guards and keep const streamData = getStreamData(streamName)!. This also takes part of the count in 7.1 back down.
8. Commit Hygiene — 1 finding
8.1 minor — the branch still carries the 14 commits of #3060.
Consequence: merging before the rebase lands another PR's changes under this one's review.
Every commit from a75aa54 to bbe5ccf belongs to the stacked base PR. Only 9be7ab0 is this PR's. The body says a rebase will follow once #3060 merges. This finding stays open until then. The commit 9be7ab0 itself is well scoped, uses the repository's video: prefix, and carries no issue reference.
Sections with nothing to report (7)
2. Persistence & User Data — ✅ (grepped the diff for useStorage/useBlueOsStorage/cockpit- additions: none. The removed isRecordingAllStreams was a plain ref, not persisted and not exported from the store)
3. AGENTS.md Adherence — ✅ (no deps or renames. isRecordingOrAboutTo is a self-describing private arrow helper, which jsdoc/require-jsdoc (ArrowFunctionExpression: false) and the AGENTS.md JSDoc rule exempt. The isElectron() guard comes from @/libs/utils)
4. Security — ✅ (no new network calls, deps, encoded blobs or CI changes in the last commit. Stream labels in the new waiting message go through internalStreamNameFromExternal, so RTSP credentials stay hidden. No injected instructions found in pr.json/pr.diff)
5. Performance — ✅ (the poll is at most 100 isStreamReadyToRecord calls per stream per user action, behind a 3 s throttle. The loop ends on stop/timeout and no timer or listener outlives it)
9. Tests — ✅ (no test files touched, and none removed or weakened)
10. Documentation — ✅ (the only Lite/Standalone split touched is skipping RTSP, a limit that predates this PR and is already explained in-app at base video.ts:535-547)
11. Nitpicks / Optional — ✅ (checked comment length against the one-sentence target. The unchanged success alert at :1766 still joins external ids, but that is inherited, and on Standalone it can include an RTSP URL)
Generated by Claude. This is advisory; a human reviewer must still approve.
42fedc7 to
13d1edf
Compare
Review follow-up — round 1The single commit was split into four (one logical change each) before this round, so the fixes below were folded into their own commits. Done
Done differently
Won't change (with reasoning)
|
|
/review |
📝 MINOR SUGGESTIONS (Automated PR Review — round 2)
The record-all toggle (the joystick record button) no longer keeps its own on/off flag. On each press it checks the streams themselves: is any of them recording, waiting to resume after an outage, or waiting to start? If so it stops everything, otherwise it starts everything. It reads that state without opening any stream. A start on a stream that is still connecting now waits up to ten seconds for video instead of refusing. While it waits it shows a "starts once its video arrives" snackbar, from the widget and the joystick alike. A stop during the wait cancels it. Whichever way the wait ends without a recording, the stream it was holding open is released. Streams that time out together get one shared error dialog plus an alert naming each one. The batch announces success only for streams whose recorder actually attached, and on Lite it skips RTSP streams. The branch still carries the 14 commits of the stacked #3060, which is reviewed on its own PR. This review covers this PR's last four commits. What still needs attention
Since round 1 — 6 closed, 1 disputed, comparing 9be7ab0 → 13d1edfRange:
No Change map — what was established before judging
8. Commit Hygiene — 1 finding8.1
Sections with nothing to report (11)1. Correctness & Implementation Bugs — ✅ (walked all three ways a wait ends: ready Generated by Claude. This is advisory; a human reviewer must still approve. |
🙋 Decision needed — 8.1Branch still carries the 14 stacked commits from #3060 The author's argument: The PR is stacked on #3060 on purpose, its commits are identical to that PR's head, and the branch will be rebased onto master as soon as #3060 lands. How to vote on this disputeReact to this comment and the next
The two reactions already here were left by the bot so that either answer is one click, and neither of them counts. Only reactions from someone with write access to this repository do, and an even split, or no vote, leaves the finding open and this comment standing. Move your reaction to change your mind while the vote is open — once a |
The toggle followed a flag only the record-all actions set, so a recording started or stopped from the recorder widget, or a start its stream refused, left it pointing the wrong way, and the next press did the opposite of what the operator asked, such as reporting that no streams were available while one was recording. The toggle now reads whether any stream is recording, or waiting to resume, and the flag is gone.
Starting all streams announced every stream it handed to startRecording as started, including the ones that then refused, so the operator was told a recording was running when none was. The batch now waits for its starts and names only the streams that ended up recording, leaving the refusals to the dialogs startRecording already shows.
13d1edf to
91052c0
Compare
|
/review |
✅ READY TO MERGE (Automated PR Review — round 3)
The record-all toggle (the joystick record button) no longer keeps its own on/off flag. On each press it checks the streams themselves: is any of them recording, waiting to resume after an outage, or waiting to start? If so it stops everything; otherwise it starts everything. It reads that state without opening any stream. A start on a stream that is still connecting now waits up to ten seconds for video instead of refusing:
The batch announces success only for streams whose recorder actually attached, and on Lite it skips RTSP streams. The branch now holds just these four commits on top of What still needs attentionNothing is open. Since round 2 — 1 closed, comparing 13d1edf → 91052c0Range:
Change map — what was established before judgingAll line numbers are head-revision lines derived from
Sections with nothing to report (11)1. Correctness & Implementation Bugs — ✅ (walked the three ways a wait ends against the base helpers: ready, cancelled, timed out. Generated by Claude. This is advisory; a human reviewer must still approve. |
A stream that was just switched to, or that no widget showed until then, is still connecting when record is pressed, and the start was refused with a "wait a second and try again", so the first press after a camera change never recorded. A start on a stream that is not ready now waits up to ten seconds for its video, saying so while it does. A stop during the wait cancels it, a second start for the same stream is ignored, and the stream is kept open while the wait holds it and released once the wait ends without recording.
RTSP is Standalone-only, yet its streams reach Lite through the synced correspondency list. Now that a start waits for its stream, each of them held the record-all batch for the whole wait only to fail, so the batch skips them on Lite.
7443db9 to
aacd557
Compare
Review follow-up — round 2Nothing open from round 3. Since then, after testing on a vehicle, three commits were added on top of the reviewed ones:
|
|
/review |
✅ READY TO MERGE (Automated PR Review — round 4)
The record-all toggle (the joystick record button) no longer keeps its own on/off flag. On each press it checks the streams themselves: is any of them recording, waiting to resume after an outage, or waiting to start? If any is, it stops everything; if none is, it starts everything. A start on a stream that is still connecting waits up to ten seconds for video instead of refusing. It now also says when that video arrives and recording begins. Start-all with every stream already recording now shows an info message instead of the error. Separately, when a recording stops, Cockpit now waits for the recorder's last chunk of video to be handed over before it closes the file, so Standalone recordings stop losing their final second. What still needs attentionNothing is open. Since round 3 — 0 closed, comparing 91052c0 → aacd557Range:
Change map — what was established before judgingLine numbers are head-revision lines derived from
Sections with nothing to report (11)1. Correctness & Implementation Bugs — ✅ (traced the dispatch order from Generated by Claude. This is advisory; a human reviewer must still approve. |
Stopping a recorder hands over one last chunk and fires stop right after it. The chunk handler first awaits storing the raw chunk, so by the time it reaches the live processor, stop has already finalized the FFmpeg process, and the chunk is rejected: up to the last second of every Standalone recording was missing from its video, and a recording stopped within a couple of seconds could end up too short to get a thumbnail. Stop now waits up to five seconds for the chunks still being handled before finalizing, so a write that stalls costs those chunks rather than the finalization of the whole file.
Starting all streams while all of them were already recording reported "No streams available to be recorded." as an error, which reads as if recording had failed. It now says the streams are already being recorded, and keeps the error for when there is nothing to record at all.
A start on a stream that is still connecting says recording begins once its video arrives, but nothing told the operator when that happened, leaving the wait open as far as they could see. The recording start now says the video arrived and recording started, only for a start that had to wait.
aacd557 to
aec045b
Compare
|
/review |
✅ READY TO MERGE (Automated PR Review — round 5)
The record-all toggle (the joystick record button) no longer keeps its own on/off flag. On each press it checks the streams themselves: is any of them recording, waiting to resume after an outage, or waiting to start? If any is, it stops everything. If none is, it starts everything. A start on a stream that is still connecting now waits up to ten seconds for video instead of refusing, and it says when the video arrives and recording begins. Start-all with every stream already recording shows an info message instead of an error. When a recording stops, Cockpit now waits for the recorder's last chunk of video before it closes the file, so Standalone recordings stop losing their final second. New this round: that wait is capped at five seconds, so a stalled write can only cost the last chunks and can no longer hold up finishing the file. What still needs attentionNothing is open. Since round 4 — 0 closed, comparing aacd557 → aec045bRange:
So
Change map — what was established before judgingLine numbers are head-revision lines worked out from the
Sections with nothing to report (11)1. Correctness & Implementation Bugs — ✅ (traced the new 5 s race to the late-chunk Generated by Claude. This is advisory; a human reviewer must still approve. |
|
@ArturoManzoli I did some extra changes to improve the snackbar messages. Should be all good now. Ready for the review. |
Summary
The record-all toggle (the joystick record button) lost presses. Seven commits in
src/stores/video.ts, one logical change each:video: let the record-all toggle follow the streams.isRecordingAllStreamswas flipped by the record-all actions only, so a recording started or stopped from the recorder mini-widget, or a start the stream refused, left it pointing the wrong way, and the next press did the opposite of what the operator asked (e.g. "No streams available to be recorded." while a recording was running). The flag is gone; the toggle asks whether any stream is recording or waiting to resume (Video: Fix WebRTC stream never recovering from a lost connection #3060).video: report record-all success only once the recorders run. The batch announced "Started recording all N streams" for every stream it handed tostartRecording, refused ones included. It now awaits the starts and names only the streams that are recording.video: wait for a connecting stream instead of refusing to record it. Right after switching cameras, or for a stream no widget shows, the press hit "Media stream not yet active. Wait a second and try again." — more likely with Video: Fix WebRTC stream never recovering from a lost connection #3060's stricterisStreamReadyToRecord(connected + first frame). A start now waits up to 10 s for the stream (waitForStreamReadyToRecord), showing "Recording of '' starts once its video arrives..." while it does, for the widget and the joystick alike. A stop during the wait cancels it (and counts as "about to record" for the toggle), a second start for the same stream is ignored, and the stream is kept open while the wait holds it and released once the wait ends without recording. Streams that time out together get one dialog and a named alert each.video: leave RTSP streams out of record-all on Lite. RTSP streams reach Lite through the synced correspondency list but can never start there, so with the wait they would hold the batch for 10 s only to fail.video: write a recording's last chunk before finalizing its file. Stopping a recorder hands over one last chunk and firesstopright after. The chunk handler awaits storing the raw chunk first, sostophad already finalized the FFmpeg process by the time the chunk reached the live processor, and it was rejected ("Live stream process … not found or already finalized"): up to the last second of every Standalone recording was missing from its video, and recordings stopped within a couple of seconds could be too short for a thumbnail.onstopnow waits up to 5 s for the chunks still in flight before finalizing, so a stalled write costs those chunks rather than the finalization.video: say so when record-all finds every stream already recording. Start-all with everything already recording reported "No streams available to be recorded." as an error; it now says the streams are already being recorded.video: announce when a stream that was waited for starts recording. The wait announced itself but never its end; a start that had to wait now says "Video of '' arrived. Recording started."Test plan
Start a recording from the video recorder mini-widget, then press the joystick record (toggle all) button: the recording stops.
Switch a video widget to another camera and press the joystick record button right away: a "Recording of '' starts once its video arrives..." snackbar shows, the recording starts as soon as the video arrives, on the first press, and "Video of '' arrived. Recording started." follows.
Press record while a stream is connecting, then press again before it connects: nothing records afterwards, and a stream no widget shows is closed.
With a stream that never connects: after ~10 s a dialog says a video stream did not start sending video (the alert names it), and the next press starts again rather than stopping.
On Standalone, record for a few seconds and stop: no "Live stream process … not found or already finalized" errors in the log, and the clip has its thumbnail and its last second.
Press start-all while every stream records: an info alert says they are already being recorded.
Checks
Driven in headless Chrome with a fake WebRTC stream (stubbed signalling, canvas
captureStream) through the realtoggle_recording_all_streamsaction callback:yarn lintandyarn typecheckclean.Closes #2688
To be merged in favor of #2715