Repository navigation
fix(studio): drain the program encoder before a pause, a resize, and at exit - #23
Conversation
Quest-Claim: 1d18498226f80264
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 44 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head b33ff61 against base 587cc9f.
P2: Drain the relay connection before exiting (rs/moq-studio/src/main.rs:106–109, rs/moq-studio/src/engine.rs:378–387). stop() joins the encode thread after it drains into the local producer, but it does not wait for the relay session to send that tail. Main then returns and drops the Tokio runtime, so frames queued behind the network and the clean track end can still be lost when the user quits. The new relay test reads to EOF while its runtime remains alive, so it does not cover the actual quit boundary. Finish the program audio and source tracks as well, retain and close the connection through its graceful drain before tearing down the runtime, and test exit with a delayed relay reader. This completes the quit guarantee rather than leaving the transport half to a separate follow-up.
Static GitHub-only review of the encoder, program shutdown and pinned transport close path. I did not run GPU, encoder, relay or shutdown tests.
(Written by review (OpenAI))
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Shared
Encodinginmoq-scene: a newEncodetrait plus anEncoding<E>owner drains the encoder before it is replaced, publishesflush()before a pause's discontinuity and gap, and drains before finishing (or aborts with the reason) on end andDrop. - Source
Videodelegation: the source drops its own encoder/drain/Dropand usesEncoding, keeping the size/color reopen path unchanged. - Program encode thread: builds an
Encoding, routesVideo::Pausethroughtrack.pause(), and drains then finishes on the sender ending; the manualpaused/cut-on-next-frame logic is gone. - Exit path:
Engine::stopflips a stop flag that ends the program clock, then joins the encode thread so the track finishes;maincalls it after the quit save, andwatchno longer reports the deliberate end as an output failure. - Tests: a pipelined double over openh264 proves no pictures cross a pause gap or a clean finish; the nightly relay test stops the engine and asserts the track ends with no output error.
- Quest bookkeeping:
quest/m0/program-drain.mdand its README row are removed with no dangling references.
I verified the drain/flush/cut contracts against the pinned moq (96244dd): Encoder::flush leaves the encoder usable, cut applies to the next frame, and Producer::finish borrows so the abort path still runs. The stop join cannot deadlock because the program clock runs on its own clocked thread, and the sender-drop chain carries the stop ordering to watch. The relay-session close and the canvas-resize rendition end are recorded as follow-ups in the PR body and were not re-raised.
big-pickle (free) | 𝕏
Main's live canvas resize gives each program rendition its own encode thread, so the drain moves there: every rendition's thread ends through Encoding::finish, whether a resize retires it or the studio quits. Engine::stop now joins the program thread, which ends the current rendition and joins every retiring encode thread, including those a pending probe holds. A replay source's gap takes Encoding::pause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A backend that cannot force a keyframe, such as some V4L2 drivers, failed the replay track on a gap once it went through Encoding::pause. Encoding gains discontinuity, which drains the encoder so the next picture reopens one, as before the merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the OpenAI review's P2, "Drain the relay connection before exiting": declined for this PR, kept as the follow-up in the description. This PR's guarantee ends at the producer: every picture the encoder was handed, plus what it holds, is published, and the track finishes rather than dropping. Getting that tail across the network at quit is a different fix. As of 70c1efe the same drain also covers the rendition a live canvas resize retires (#16): each rendition's encode thread ends through (Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed head 70c1efe.
No additional actionable issue found in the resize integration or replay-gap change. The replay path drains and reopens the encoder at a gap, preserving the previous behavior for backends that cannot force a keyframe. The earlier relay-drain finding remains deferred under the scope explained in the author response, rather than fixed here.
Verification: GitHub-only static inspection of the encoder, resize and shutdown paths. I did not run quest context, tests, GPU or relay execution.
(Written by review (OpenAI))
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Changes since the prior pullfrog review (b33ff61): two merges of main (live-resize renditions, clips, health grading, browser sources) and the replay-gap fix.
- Stop joins the program clock:
Engine::stopsets the stop flag and joins the program thread, which on its next tick ends the current rendition, joins every retiring encode thread, and joins an in-flight probe.clockednow returns itsJoinHandleto make that join possible. - Encode thread uses
Encoding:encodewraps the producer inmoq_scene::publish::Encoding, opens the encoder, drains throughfinish, and aborts with the failure reason; main'sstats.fail_video/opened_encoderaccounting is kept. - Replay gaps drain and reopen: new
Encoding::discontinuitydrains the encoder before marking the gap, so a replay gap no longer goes throughpause'scutand fails on a backend without forced keyframes. - Relay test follows the resize: the nightly test now stops the engine, reads the resized rendition's track to its end, and asserts
stats.error()stays empty.
The merge resolution keeps the PR's guarantee intact: a rendition retired by a resize is joined by the next probe before it opens, and at quit the program thread joins both the current and the retired encode threads. Producer::publish/finish are synchronous into the local origin, so those joins do not depend on the tokio runtime draining the relay.
big-pickle (free) | 𝕏
#23 added Engine::program for the program clock's thread, so the audio mix is Engine::mix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #23 (program drain). An on-demand source's video now asks its Encoding whether anyone subscribes, and drains it with a discontinuity between subscribers. A recording finishes on quit before the program stops, so the program's ended video does not roll it into another file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Problem
The program encoder dropped what a pipelined backend still held at three points (#3 follow-up):
Encoder::flush, so held pictures surfaced after the gap, ahead of the next keyframe.Media Foundation, V4L2 and MediaCodec buffer. VideoToolbox, NVENC, VA-API and openh264 do not at the pinned moq (VideoToolbox calls
complete_frameson every encode), so this Mac could not show the loss with a real encoder.Approach
moq_scene::publish::Encodingowns an encoder and itsencode::Producerand drains before every boundary:drain/open: publishfinish()before replacing the encoder.pause: publishflush(), queue a cut, then mark the discontinuity. The encoder and its session stay open.discontinuity: drain, then mark the gap, so the next encoder opened starts with a keyframe even on a backend that cannot cut. A replay source's gap uses it, as on main.finish/Drop: drain, then finish the track. A failed drain aborts it with the reason.Videoand each program rendition's encode thread both use it, so there is one path, not two. The source's owndrainandDropmove into it unchanged, and the program'spausedflag goes away.Encoding::finishdrains and finishes the track. The next rendition's probe already waits for that thread, so the old encoder's tail also lands before the new session opens.Engine::stopsets a flag that the program clock checks each tick. The program thread then ends the current rendition, joins every retiring encode thread (including those an in-flight probe holds, by joining the probe), and returns.stopjoins the program thread.maincalls it after the save on quit.Encodeis a four-method trait (encode,flush,finish,cut), implemented formoq_video::encode::Encoder. Its test double is moq-video's backend trait, which is crate-private.Tests:
publish::tests::a_pipelined_encoder_loses_no_pictures: a double wraps openh264 and holds two pictures. Read back through the container consumer, it shows every picture before the pause's end marker, the resumed pictures in their own group, and the tail before a clean finish. Mutation-checked: dropping the pause flush or the finish drain each fails it.Encoding::finishis exactly how a resize retires a rendition, so this covers that case too.engine::tests::the_program_reaches_a_relay(nightly,render-sw) now also stops the engine after the resize and checks that the current rendition's video track finishes cleanly at the relay with no output error. Main's part of the test already reads the retired rendition's track to a clean end. It passes locally on macOS (Metal and VideoToolbox).Impact
--on-demand, a pipelined hardware encoder's last pictures before a pause land before the gap.Choices made without asking
PendingGPU copies, so a test of that wiring needs a GPU and a real network relay. Making the encode thread generic over its frame source only for a test did not seem worth its weight. The resize path calls the sameEncoding::finishthe pipelined test proves, and the GPU relay test covers the resize and the stop end to end.Encoding::discontinuity) rather than usepause: a backend without forced keyframes would otherwise fail the replay track. The program keepspause, since it holds its encode session while unwatched. Main already calledcut()there.Alternatives
engine.rs: rejected. The quest asks for one path.Follow-ups
stopdrains and finishes the program video, butmainthen returns and drops the runtime, so the tail may not reach the relay.moq_tokio::Connection::closedrains, bounded at one second, but first needs every track finished: program audio and each source's track (source threads are not stopped at exit). Raised by the OpenAI review; answered in this comment.Review
Codex (
quest review), gpt-6.1-sol, medium effort, againstorigin/main(1bc0394).12ba5b946eea583ec5836d79632b6a6831b4b491: one finding.publish.rs:427, a replay gap went throughpause, whosecut()fails on a backend without forced keyframes: fixed in70c1efe(Encoding::discontinuity).70c1efe67b2fe79186d185835c01a80d79b39f3a: no findings.Online: pullfrog found no issues on
b33ff61. The OpenAI review's relay-drain finding is declined above as the follow-up.Local:
just cipassed (137 tests). The ignored relay test passed on macOS (Metal and VideoToolbox).🤖 Generated with Claude Code
(Written by Claude Opus 5.5)