Repository navigation
Conversation
The stall watchdog ends a track that has run dry while another backs up behind it. Until now the track never came back, and the recording stayed without it until the flow stopped. Ending a track now parks its input: it is unlinked from its parser, the chain behind it gets the EOS, and a probe takes its buffers off the stream, keeping everything from the first keyframe on (up to 32 MiB). That keyframe marks a resume, and the watchdog starts a new file. It closes the current one (catching its EOS before the pipeline sees it), takes splitmuxsink to NULL, requests fresh pads for every track carrying data, and relinks them with new parsers and queues. Each track then replays what it kept before live data passes, on its own thread because splitmuxsink can hold one track's queue until another catches up. A new file is the only way back in: splitmuxsink keeps a pad's EOS across splits and state changes, and mp4mux refuses pads once a file has started. The parked input drops the RECONFIGURE that linking the new chain sends upstream. An encoder answers it with an ALLOCATION query that waits in the new queue, which then reads as drained to the watchdog. Past FILE_FINISH_TIMEOUT the switch waits until the file sink is idle before closing the file as it is. Stopping the muxer mid-write makes mp4mux fail the flow. A track that stays quiet, or whose source really ended, stays out of the new file. A real EOS still ends the track and the recording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47b89a7 to
6871cf9
Compare
…during replay Starting a new file parked every track going over at once. A video track still being recorded was cut mid-GOP, and its part of the new file began at its next keyframe: the frames in between reached neither file. From a remote encoder with a GOP longer than the stall timeout, the new file then waited on that track until the watchdog ended it, and was left empty. Such a track is now parked at its next keyframe, after a key-unit request. The current file keeps it up to there, and the keyframe starts the next file. With no keyframe within KEYFRAME_CUT_TIMEOUT the track is ended instead, since splitmuxsink opens no file without its primary video, and a later file takes it back like any ended track. The stall check also runs while tracks replay into the new file. A replay larger than its queue waits on the other tracks, so one that died during the switch held the watchdog, and the recording, until it came back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e while waiting Parking, the keyframe cut and the stash take a frame without DELTA_UNIT as a keyframe. tsdemux output, which SRT passthrough feeds the recorder, flags no frame as a delta. A new file then started mid-GOP, its parser dropped everything up to the next IDR, and with a GOP over the stall timeout the track was ended into a parser that had output nothing, which fails the flow. Each video track now has an h264parse or h265parse that lives for the whole recording, ahead of a second identity where the recorder parks and cuts it. It marks keyframes and repeats SPS/PPS before each one, and is replaced if the codec changes. That parser drains on EOS, so a track that takes an EOS while parked can now have frames to replay. The parked input refuses to push its caps and segment once it is EOS, so the new chain gets them directly. start_next_file took the pipeline at the start and held it through every wait. A flow stopped during a switch then outlived the drop that the flow's leak check runs right after. It now takes the pipeline only to relink, and only while it is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A video input that changes codec is refused by its keyframe parser in the ACCEPT_CAPS query, so the caps event never reaches the probe that was to swap the parser. A codec change fails the flow as it did before; the recorder now handles only the first caps, and the parser is created once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Noted as a draft, so I am holding off on a full review until it is marked ready for review. If you want one before then, say so here or request me as a reviewer. |
A source that sent a keyframe just past the stall timeout, then nothing, started a new file on every keyframe. Each time the video then stopped, it froze the other tracks again for the stall timeout: 4.5 s of every 6 s, and a file per cycle. A parked input now counts as back only once its data spans 3 s with no gap longer than the stall timeout, so a source slow enough to be recorded before a stall is recorded after one, and one that sends less often stays out. A longer gap starts its stash over, so the new file starts on the first keyframe of the run and the wait loses no video. Once the switch has taken a track for the next file, a gap no longer drops its stash, which the new file starts with, and the run starts again. Frames that arrive while the stash is replayed are kept after it even if the first is not a keyframe, so the replay leaves no hole in the GOP. A full stash drops its oldest GOP instead of everything, and once it has dropped the frames a delta depends on, waits for a keyframe again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
srperens
left a comment
There was a problem hiding this comment.
Verdict: Request changes. Recovering in a new file is the right approach; the body rules out each in-file alternative. The healthy-path probes keep to CLAUDE.md, and all 32 recorder tests ran in CI. But one claim in the body fails for a stop that lands in step 3 of the switch, and the new tests multiply a GStreamer CRITICAL by about 130 that nobody has attributed.
Requested changes
- Close the step-3 window, or narrow the claim and guard it. In
start_next_file,backend/src/blocks/builtin/recorder.rs:1106—let Some(bin) = splitmuxsinktakes the pipeline after a single state check,backend/src/blocks/builtin/recorder.rs:1109—.filter(|b| b.current_state() >= gst::State::Paused). It holds the pipeline untilbackend/src/blocks/builtin/recorder.rs:1217—drop(bin);, across splitmuxsink's NULL and the relinking. Astop_flowwhose drop lands in that window fails its leak check,backend/src/state/mod.rs:1526—let pipeline_survived = pipeline_weak.upgrade().is_some();. That logs the ERROR CLAUDE.md treats as P0, and the relink then runs against a pipeline that is going to NULL. Either make stop wait for an in-flight switch before the drop, or state the window and add a test that stops inside step 3. - Attribute the CRITICALs.
Check (Linux)at125e7c1printsgst_mini_object_unref: assertion 'mini_object != NULL' failed130 times inrecorder_test, mostly in bursts of 7–8 whiletrack_resumeruns. The same binary on #944, which is based on main, prints it once. Say whether these come from the switch or from the test sources. If they come from the switch, fix them.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| A flow stopped mid-switch is not kept alive past the leak check | CONTRADICTED |
Holds during the waits. Fails in step 3 (item 1). The guarding test covers only the keyframe wait: backend/tests/recorder_test.rs:2519 — /// reports a leak if anything still holds it. A stop while a switch waits for |
| The healthy path adds no locking probe | CONFIRMED |
The only permanent BUFFER probes are the retired-input probe and the intake probe, and both use atomics only. The park, cut and write-tracking probes exist only while a track is out or a switch runs |
| The new closures hold no strong element references | CONFIRMED |
The caps probes, the keyframe park and WatchedTrack hold WeakRefs. The replay threads are scoped and joined before start_next_file returns |
| The resume tests ran | CONFIRMED |
Check (Linux), run 36870108266: test result: ok. 32 passed, 183 s, under STROM_REQUIRE_GST_PLUGINS: 1 |
Lifetime: the watchdog thread lives as long as the flow. It holds a strong splitmuxsink for a whole switch, from backend/src/blocks/builtin/recorder.rs:1391 — let Some(sink) = splitmuxsink.upgrade() else {; that can be 15 s or more, but it does not keep the pipeline alive. A stop during a wait makes the next two warnings report timeouts that never happened.
Radius: LOCAL. One block. Every recorder flow with two or more tracks runs the new watchdog paths.
Overlaps: #762 also changes end_stalled_track, and the body asks to land first. Both conflict textually. #944 assumes that only the first file can start before its reference track. A switch here restarts splitmuxsink, which breaks that assumption, so whichever lands second must treat every restart as a first file (see my review there). #967 changes the stop path that item 1 touches.
Tests & CI: all checks green at 125e7c1, which is 25 commits behind main. macOS and Windows were skipped, and nothing here is platform-specific. recorder.rs is now 2927 lines. CLAUDE.md asks for a split above 1500 lines, best done after this and #944 land.
Confidence: MED
A stop that landed while start_next_file was relinking found the pipeline still held, tripping the leak check, and relinked a pipeline already going to NULL. Blocks can now register a pre-stop hook, which the pipeline manager runs before setting NULL. The recorder's closes a gate: a relink under way is waited for (10 s at most), and none starts after it. The watchdog and every wait in the switch give up once the gate is closed. Waiting for the relink exposed a splitmuxsink hang: opening a file overwrites the STOPPED state that PAUSED->READY sets, so a queue thread waits forever and setting NULL never returns. The hook sends FLUSH_START into splitmuxsink, which releases that wait. Before GStreamer 1.24.8 a probe that drops an event frees it twice and logs a CRITICAL. The park probe and the switch's EOS catch now discard events with Handled; buffers are still dropped. A replay whose segment push is refused now sends the stream's sticky events to the new chain directly, not only when the input is at EOS: buffers without a segment leave splitmuxsink unable to place them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before GStreamer 1.24.8 a probe that drops an event frees it twice and logs a CRITICAL. The stand-ins for an encoder that ignores key-unit requests and a source whose EOS never arrives logged CI's remaining ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Written by Claude. Both addressed, and 1. A stop during step 3. Stop now waits for an in-flight switch. Blocks can register a pre-stop hook, which the pipeline manager runs before setting NULL (in Waiting for the relink surfaced a splitmuxsink hang: opening a file overwrites the STOPPED state that PAUSED→READY sets, so a queue thread goes back to waiting and setting NULL never returns. The hook sends FLUSH_START into splitmuxsink, which releases it. New test, 2. The CRITICALs. All of them were probes returning Also changed: a replay whose segment push is refused for any reason now sends the sticky events to the new chain directly, not only after EOS. I saw the refusal once without EOS, as a hang at stop, and could not reproduce it, so that path has no guard. The PR body covers it. |
srperens
left a comment
There was a problem hiding this comment.
Supersedes my review of 2026-10-05 at 125e7c1. That Request changes verdict does not stand: both requested changes are done.
Verdict: Comment. The stop gate closes the step-3 window, and the CRITICALs are gone from recorder_test. But #967 adds a second "block work before NULL" hook at the same line of stop(). Merged in the wrong order, the two cancel each other out, so a maintainer has to choose the shape.
Requested changes (or a maintainer decision instead)
-
Agree one ordered stop hook with #967 before either PR lands. This PR adds
self.run_pre_stop_hooks();where #967 addsself.run_stop_drains();, directly afterself.volume_ramps.clear();inbackend/src/gst/pipeline/lifecycle.rs. That is a textual conflict, and the order you resolve it in changes behaviour:- Flush first, then drain. The hook's
FLUSH_STARTputs splitmuxsink's sink pads into flushing. #967's EOS, which it sends into the queue in front of each pad, is then discarded. Every recorder stop waits out #967's 5 s timeout and the file is still unfinished, which is the bug #967 fixes. - Drain first, then gate. A drain can run during a relink, and #967 aims its EOS at whatever queue
peer()finds while this PR is replacing those queues.
The order that works for both is: close the gate, then drain, then flush. Neither PR can express that on its own. Option: land this first and give #967's drain a slot between the gate and the flush. Or land #967 first and rebase this onto
register_stop_drain. Or wait, and merge the two APIs into one. - Flush first, then drain. The hook's
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| A stop no longer lands inside the relink | CONFIRMED |
bin is taken only after the gate is entered and released before the gate is left (citations below). The gate's SeqCst store-then-load on each side rules out both sides missing each other |
| The new tests run in CI and guard the stop window | CONFIRMED |
Check (Linux) at e5c4ab3: recorder_test reported 30 passed in 173 s, including a_flow_stopped_while_the_switch_relinks_leaves_nothing_holding_its_pipeline. The two RelinkGate unit tests and pipeline_lifecycle_test also ran |
The CRITICALs came from the switch, and Handled removes them |
CONFIRMED |
The same job printed gst_mini_object_unref 0 times in recorder_test, against 130 at 125e7c1. 125 of the 129 in this job are in output_refusal_test, which this diff does not touch. I have not attributed those 125 |
FLUSH_START releases a splitmuxsink queue that NULL cannot |
EXTERNAL |
Depends on splitmuxsink internals. The assumption is the author's observed hang without it. |
Lines on this head:
backend/src/blocks/builtin/recorder.rs:1220 — let Some(relinking) = gate.enter() else {
backend/src/blocks/builtin/recorder.rs:1334 — drop(bin);
backend/src/blocks/builtin/recorder.rs:1341 — drop(relinking);
backend/src/blocks/builtin/recorder.rs:2318 — pad.send_event(gst::event::FlushStart::new());
backend/src/gst/pipeline/lifecycle.rs:274 — self.run_pre_stop_hooks();
Radius: SHARED. This PR now adds a BlockBuildContext API and a step to PipelineManager::stop() and Drop. Both are no-ops for flows without a recorder, because the hook list is empty. PreStopFn is FnOnce, and a flow restart builds a new PipelineManager (start_flow), so a closed gate never outlives its flow.
Overlaps: #967 (the same code path, and the same design decision, described above). #944: the restart-as-first-file conflict from my previous review still stands. #762: this PR's body still asks to land first.
Tests & CI: Linux, WASM and API checks are green at e5c4ab3. macOS and Windows were skipped, and nothing here is platform-specific.
Confidence: HIGH
Superseded by my re-review at e5c4ab3: both requested changes were addressed.
|
Written by Claude. Agreed on the order: close the gate, then drain, then flush. The plan is to land this PR first and resolve the order in #967's rebase:
This PR does not change. The rebase also needs a test for a stop that comes after a switch while one track is still out. A retired track may never carry EOS in the current file, so the drain's completion check could wait out its timeout. |
|
Comment by Claude (AI), on behalf of @srperens. Thank you for this, and for the adversarial tests behind it: #931, #932 and #939 came out of them, and they shaped what we built next. The diagnosis is right. The trouble is In #1034 a stalled track comes back in the same file, with a gap where it was missing. We measured Where we're heading. We're aiming to deprecate the Recorder in favour of a new block, Live Recorder ( We'd really like your help with this:
|
…ataless track held back From @wagenet's run of the #927 stall cases on #1034: - Frames lost upstream of the recorder leave the first frames back as deltas against frames the file never got; with a split, a file could open on them. When the keepalive first fills a video track's gap, the track now drops until a keyframe and asks upstream for one with a force-key-unit. An encoder that went on encoding through the stall used that request up, so it is asked again when frames start arriving and are still being dropped. A Strom encoder then answers with its next frame instead of a GOP later. - A stop before a connected but dataless track was released wrote no file: the drain only ran once a file was open. It now always sends EOS, which also ends the muxer's wait for the dataless track. For that drain to get its data through, the fragment sink is now a plain element with a sink pad rather than a BaseSink. A BaseSink with no buffer by PLAYING still counts as not prerolled, and a first buffer arriving while the pipeline heads for PAUSED waited in preroll for a PLAYING that never came. The sink never synced to the clock, so nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1034) * feat(live recorder): prototype a recorder where a quiet track holds nothing up A new block, builtin.liverecorder, next to the Recorder. It records to fragmented MP4 through isofmp4mux, a live aggregator, instead of splitmuxsink: - A track that stops does not stop the others or anything upstream, and continues in the same file when it returns, with a gap where it was missing. No watchdog, no ended tracks, no new file per stall. - A quiet track gets GAP events, so the muxer keeps cutting fragments while the video is gone and the audio keeps reaching the disk. - A connected track that carries nothing for 20 s is released from the muxer. Until then the other tracks wait in their queues, which answer ALLOCATION queries themselves, so an encoder is never held up. - A small BaseSink writes the fragments and starts a new file at a fragment boundary, with the init segment first. A split due at the muxer's last fragment keeps that fragment in the current file. - The sink is not async, so a late first buffer does not pull the flow out of PLAYING. Experimental: mp4 only, and a track that arrives after the 20 s release is not recorded until the flow restarts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(live recorder): start each file at zero and finish the file on stop Each split file now starts at zero. The muxer numbers time through the whole recording, so the second file said it started at 38 s and players showed a 46 s file holding 8 s. The sink moves every tfdt in a file back by the file's start, the same instant for every track, in each track's own timescale. Values change in place; no box changes size. A flow stop no longer drops the fragment the muxer is still holding. When the pipeline is on its way to READY or NULL, the sink sends EOS into each track's queue at its PLAYING -> PAUSED step, while it still renders, and waits up to 3 s for the muxer to hand the rest over. A pause does not trigger it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(live recorder): write MKV, MPEG-TS and TS passthrough as well The Live Recorder now takes the Recorder's container property: - mkv: matroskamux, streamable. A file can start at a cluster that starts on a keyframe; the header comes from the caps' streamheader. The sink moves every cluster timestamp back so each file starts at zero. - mpegts: mpegtsmux with default alignment, so the keyframe's first packet is flagged and a file can start there after PAT and PMT from the streamheader. - ts_passthrough: the Recorder's own passthrough, unchanged. Both muxers are live aggregators, so a quiet track holds nothing up and comes back in the same file, as with MP4. Fragmented MP4 cannot carry MP3 or DTS; the refusal says to pick mkv or mpegts for those. The sink now buffers its writes and flushes at every cut point and at least every 500 ms, so the audio keeps reaching the disk while a stalled video provides no cut points. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(live recorder): split now starts the next file at the next cut point A requested split was held back a fragment like a timed one, so the new file, and its name in the editor, showed up to two GOPs after the click. Only a split the policy makes can coincide with a stop and leave the muxer's tail alone in a file; one the operator asks for goes at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build: take gst-plugin-isobmff from a fork that builds on GStreamer 1.24 gst-plugin-isobmff 0.15 builds gstreamer-audio with v1_26 for a single function, gst_audio::reorder_channels_with_reorder_map(), so Strom stopped building on Ubuntu 24.04 (GStreamer 1.24.2): Linux CI and the Linux release builds, whose binaries install.sh puts on the distro's GStreamer. The fork (srperens/gst-plugin-isobmff, tag v0.15.4-strom1) is the published 0.15.4 source plus one commit that does that reordering in Rust and builds gstreamer-audio with v1_18. Nothing in the dependency tree enables v1_26 any more. Drop the patch once upstream no longer needs 1.26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(live recorder): refuse mkv before GStreamer 1.26, and stop dropping events Linux CI (GStreamer 1.24.2) showed two things the macOS runs (1.28) could not: - matroskamux became a GstAggregator only in 1.26. Before that it is GstCollectPads-based and waits for every track, so a stalled video kept the audio out of the file, the freeze this block exists to avoid. The block now refuses mkv on older GStreamer with an error that names what is needed and to use mp4 or mpegts. The mkv tests check that refusal on older GStreamer and the behaviour on 1.26 and later. - The retired-track probe dropped events. Before GStreamer 1.24.8 a dropped event is freed twice, which logged gst_mini_object_unref CRITICALs. Events are now discarded with Handled; buffers are still dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(live recorder): accept audio-only Matroska matroskamux labels a recording without video audio/x-matroska, which the fragment sink's template refused, so an mkv Live Recorder with no video tracks failed with not-negotiated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(live recorder): resume video on a keyframe, and finish a file a dataless track held back From @wagenet's run of the #927 stall cases on #1034: - Frames lost upstream of the recorder leave the first frames back as deltas against frames the file never got; with a split, a file could open on them. When the keepalive first fills a video track's gap, the track now drops until a keyframe and asks upstream for one with a force-key-unit. An encoder that went on encoding through the stall used that request up, so it is asked again when frames start arriving and are still being dropped. A Strom encoder then answers with its next frame instead of a GOP later. - A stop before a connected but dataless track was released wrote no file: the drain only ran once a file was open. It now always sends EOS, which also ends the muxer's wait for the dataless track. For that drain to get its data through, the fragment sink is now a plain element with a sink pad rather than a BaseSink. A BaseSink with no buffer by PLAYING still counts as not prerolled, and a first buffer arriving while the pipeline heads for PAUSED waited in preroll for a PLAYING that never came. The sink never synced to the clock, so nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(live recorder): carry the MPEG-TS PCR on a PID of its own mpegtsmux puts the PCR on the first video stream, so a video stall left a hole in the clock reference as long as the stall. The file's PTS were right, but tsdemux rebases across a PCR gap: in @wagenet's run a 10 s recording with a 3 s video stall read as 7.4 s, audio included, for any GStreamer-based reader, Strom's media player among them. prog-map PCR_1=(int)0x100 gives the program a PCR-only PID, which mpegtsmux keeps writing whatever the tracks do. Supported since before GStreamer 1.24. The mpegts stall test now checks the gaps and coverage through tsdemux like the other containers; without the PCR PID it reads a largest video gap of 0.47 s for a 4 s stall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Peter Wagenet <peter.wagenet@gmail.com>
|
Comment by Claude (AI), on behalf of @wagenet. Closing. Live Recorder ( Thanks @srperens for the pointer to #1034 and for the earlier reviews. |
Important
This PR can probably be closed. Note added by Claude (Claude Code), on behalf of @wagenet, 2026-10-08.
Live Recorder (
builtin.liverecorder, #1034, merged 2026-10-08) is set to replace this block, and it does not have the problem this PR fixes: a track that stalls does not hold up the others, and comes back in the same file (an_audio_stall_holds_nothing_up_and_audio_returns_in_the_same_file,a_video_that_comes_back_resumes_on_a_keyframe_without_waiting_a_gop). #967 and #762 are stacked on this PR and carry the same note.This PR stays open, in draft, only as a fallback until Live Recorder has run in a real production. It needs no review in the meantime.
Description
Written by Claude.
A recorder track that stalls and then resumes is now recorded again, in a new file, without restarting the flow.
The stall watchdog ends a track that has run dry while another backs up behind it, so one dead input cannot freeze the recording. Until now that was permanent. On a live rig the program video stalled for about 7 s, the watchdog ended it, and the recording stayed audio-only for three hours.
Why a new file
The current file cannot take the track back. splitmuxsink copies a pad's EOS into every later fragment and keeps it across state changes, mp4mux refuses request pads once a file has started, GAP events do not release splitmuxsink's wait, and repeated-frame filler is per codec. A new file works for every track and container, at one extra file per recovery.
What changed
start_next_fileswitches files:start-indexkeeps the_%05dnumbering.stop()and on drop. The recorder's hook closes a gate: a relink under way is waited for (10 s at most, then the stop goes ahead with a warning), and none starts after it. The watchdog and every wait in the switch give up once the gate is closed, so a stop during a wait logs no timeout warnings. The switch still holds the pipeline only while relinking, and the stop now waits for it to let go.stop()gives up after its 30 s deadline rather than hanging; the new stop-during-relink test, which sets NULL directly, hung on it.Handledrather thanDrop: before GStreamer 1.24.8 (CI has 1.24.2) a dropped event is freed twice and logs a CRITICAL. That was the park probe's RECONFIGURE and the switch's EOS catch, seven per switch, plus two test stand-ins (an encoder ignoring key-unit requests, a source whose EOS is swallowed). Buffers are still dropped.Costs and gaps
mainthat happens once, since the track never comes back. Here a source that returns for 3 s or more and drops out again costs it once per dropout, and a new file each time.main(Recorder: a video track that changes codec fails the flow #931). Withmax_size_time_secsset, an mp4 recording still fails the flow on a video stall longer than about one split, as onmain(Recorder: mp4 with max_size_time fails the flow on a long video stall #932).async-handlingis untested; live pipelines ignore a child's ASYNC_START. A connected track whose first caps arrive during a restart could reactivate splitmuxsink early.end_stalled_track. This should land first; fix(recorder): retry an EOS the stalled track refused #762 then needs a rebase, since its test file moved in test: consolidate the integration tests and share their helpers #912.Related Issue
None. Found on a live rig recording.
Type of Change
Checklist
cargo fmt --allcargo clippy --workspace --all-targets -- -D warningscargo test --workspaceTests
Ran locally on macOS with GStreamer 1.28.6:
A
track_resumemodule inbackend/tests/recorder_test.rsstalls and resumes real x264enc and avenc_aac sources throughRecorderBuilder: video and audio resumes, two stalls in a row, a 10 s GOP that ignores key-unit requests (returning and still recording), a track back for one frame, a keyframe every 6 s, a frame every 4 s, a track that stops again as the new file takes it, a 5.5 s break while the switch closes the old file, a track that dies during the switch, a video that stops before the keyframe the switch waits for, storage blocked for 14 s, video with no keyframe flags (returning, cut, and no loss at a short GOP), a stop during a switch, a stop while the switch relinks, a stop while a track is out, a real EOS, and an EOS during the switch. After mergingmain:recorder_test30/30,recorder_stall_test3/3,pipeline_lifecycle_test4/4.Linux, in an Ubuntu 24.04 container with CI's package list (GStreamer 1.24.2):
recorder_test30/30 andrecorder_stall_test3/3, with no CRITICALs. At 125e7c1 the same run logged 131, matching CI's 130.Unit tests: the stop gate (a stop waits for a relink and none starts after it; a stop gives up on a relink that never ends),
plan_next_file, the stash's keyframe, overflow, run and replay rules, and the parked input answering OK upstream.Revert checks, each failing its test:
cargo test --workspace: 1335 passed, 0 failed, 8 ignored.Adversarial scratch tests that held: resumes in mpegts and mkv, three tracks with one finished, two video tracks, five stalls in a row, a resolution change while out, real tsdemux output, a real EOS during the keyframe cut, a source flapping 4 s on and 6 s off, a stop during a blocked replay, and a track ended during the replay that comes back later. The codec change and splitting cases above fail as on
main.🤖 Generated with Claude Code