Repository navigation
feat(ffi)!: byte-stream producers form a simulcast ladder - #7699
Conversation
Quest-Claim: dbbdafc675edcf75
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # dart/moq/MIGRATE.md # rs/moq-ffi/MIGRATE.md
…rter 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 1 minute. 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 fe803f3 against base 9ac02f9.
P2 — Preserve the AV1 sequence header for the resumed decoder (rs/moq-mux/src/import/track.rs:646–650). If the only sequence header arrives in a keyframe while disabled, configure updates catalog metadata but the gate discards that access unit. A later bare keyframe is published on resume, yet the AV1 splitter does not cache or reinsert the header, and the resulting catalog config has no decoder description containing it. A new or reset decoder therefore cannot decode the resumed rung even though the catalog says it is enabled. Preserve and attach the sequence header to the resumed keyframe, or wait for a keyframe carrying it, and test actual decoding of the existing dropped-header scenario.
P3 — Fix the Dart migration constructors (dart/moq/MIGRATE.md:9). The generated TrackMoqMediaProducer and StreamMoqMediaProducer constructors take positional arguments, so the documented producer: ... calls do not compile. Show TrackMoqMediaProducer(producer) and StreamMoqMediaProducer(producer) instead.
Verification: static GitHub review of the gate, codec configuration, decoder path, generated bindings and tests. I did not run a decoder, Dart compiler, builds or interoperability checks.
There was a problem hiding this comment.
ℹ️ No critical issues — one recurring wording nit inline.
Reviewed changes
MoqMediaProducerenum (rs/moq-ffi/src/media.rs):MoqVideoInit.lowerbecomesOption<MoqMediaProducer>(Track/Stream).video_priorityreads the lower track's priority, so either producer kind sits one rung below;MediaStreamProducerrecords the resolved priority andMoqMediaTrackStreamProducer::set_enabledis added.TrackStreamgate (rs/moq-mux/src/import/track.rs): a newGate(Open/Closed/Resuming) plus apublishedflag drops access units while disabled, emits adiscontinuityonce anything was published, keeps the splitter's partial NAL across a disable, and resumes at the first keyframe the splitter completes after re-enable. Dropped keyframes still runconfigure.- Codec importers (
h264/h265/av1): the inline keyframe-config block is extracted intoconfigure;write_framescalls it. Behavior-preserving. - Bindings and docs: Python
_lower, SwiftMedia.Producer, Go sealedmedia.Producer+SetEnabled, Kotlin/DartProduceraliases, C++/OBSkTrack, all sixdoc/lib/*/index.md, and everyMIGRATE.mdupdated. - Tests:
track_stream_disables_and_resumes_at_a_keyframeandtrack_stream_configures_from_a_dropped_keyframe(moq-mux),stream_rung_is_funded_after_its_lower_and_resumes_at_a_keyframe(moq-ffi).
I traced the gate state machine, the priority resolution for both variants, the configure-on-dropped-keyframe path, and the generated binding round-trips (Go/Dart/Python/Swift); the drop/resume/discontinuity transitions line up with the documented behavior and the tests.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run re-reviewed the delta since the prior Pullfrog review at fe803f3592, which is doc/comment wording only.
- Fixed the recurring doc typo:
drops what it is writtennow readsdrops what is written to itin all sixdoc/lib/{cpp,dart,go,kt,py,swift}/index.mdpages (ac1bc978d). - Fixed the test doc comment: the same wording in
rs/moq-ffi/src/test.rs'sstream_rung_is_funded_after_its_lower_and_resumes_at_a_keyframecomment. - Merged
origin/main(11c255000): conflict-free, touches none of this PR's code.
The doc sentence now matches the API docstrings in py/moq-rs/moq/media.py, go/wrapper/media/media.go, and swift/Sources/Moq/Media.swift. No behavioral surface changed.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
# Conflicts: # cpp/moq/MIGRATE.md # dart/moq/MIGRATE.md # go/wrapper/MIGRATE.md # kt/moq/MIGRATE.md # py/moq-rs/MIGRATE.md # rs/moq-ffi/MIGRATE.md # swift/MIGRATE.md
# Conflicts: # rs/moq-mux/src/import/track.rs
…before it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run re-reviewed the delta since the prior Pullfrog review at 11c2550003 (superseding 9582c2783), covering the AV1 resume fix and the merge of #7735.
- AV1 resumed keyframes carry the dropped sequence header (
rs/moq-mux/src/codec/av1/import.rs): a keyframe the gate drops now armsowed_seq; the next keyframe written without its own sequence header gets the last one inserted after its temporal delimiter, so a new or reset decoder can decode the resumed rung. droppedreplacesconfigureon the gate's dropped frames (h264/h265/av1importers): each importer exposespub(crate) fn dropped, keepingconfigureprivate while thepublish!macro still accounts for dropped access units. H.264/H.265 rely on the splitter re-injecting retained parameter sets; AV1 does not, henceowed_seq.- Merged
origin/main(#7735):TrackStream::initializeroutes the splitter's newinitialize(data)through the gate, andfind_sequence_headernow returns the last (applied) header. - Test tightened:
track_stream_configures_from_a_dropped_keyframeresumes throughfinish()and asserts the resumed payload equals the original temporal unit, sequence header included.
I traced the owed_seq lifecycle across disable / drop / resume / re-disable and its interaction with the last-wins find_sequence_header, plus the split.initialize gate integration. The sequence header is inserted in the correct AV1 temporal-unit position and cleared exactly when a keyframe carries its own header, so no stale or duplicate header is possible in the covered paths. No behavioral gap found in the delta.
deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏
|
quest land watch: the merge queue removed this pull request at 0910a5f because it conflicts with The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs. (written by quest land watch) |
|
codex / Codex land fixer note:
(written by codex / Codex) |
|
codex / Codex land fixer: Auto-merging cpp/moq/MIGRATE.md CONFLICT (content): Merge conflict in cpp/moq/MIGRATE.md Auto-merging dart/moq/MIGRATE.md CONFLICT (content): Merge conflict in dart/moq/MIGRATE.md Auto-merging dart/moq_ffi/lib/src/moq.dart Auto-merging doc/lib/dart/index.md Auto-merging doc/lib/go/index.md Auto-merging doc/lib/kt/index.md Auto-merging doc/lib/py/index.md Auto-merging doc/lib/swift/index.md Auto-merging go/wrapper/MIGRATE.md CONFLICT (content): Merge conflict in go/wrapper/MIGRATE.md Auto-merging kt/moq/MIGRATE.md CONFLICT (content): Merge conflict in kt/moq/MIGRATE.md Auto-merging py/moq-rs/MIGRATE.md CONFLICT (content): Merge conflict in py/moq-rs/MIGRATE.md Auto-merging quest/m1/README.md Auto-merging rs/moq-ffi/MIGRATE.md CONFLICT (content): Merge conflict in rs/moq-ffi/MIGRATE.md Auto-merging rs/moq-ffi/src/test.rs Auto-merging swift/MIGRATE.md CONFLICT (content): Merge conflict in swift/MIGRATE.md Automatic merge failed; fix conflicts and then commit the result. (written by codex / Codex) |
# Conflicts: # cpp/moq/MIGRATE.md # dart/moq/MIGRATE.md # go/wrapper/MIGRATE.md # kt/moq/MIGRATE.md # py/moq-rs/MIGRATE.md # rs/moq-ffi/MIGRATE.md # swift/MIGRATE.md

Quest: quest/m1/stream-ladder.md, deleted here.
Summary
A ladder of moq-ffi byte-stream producers (
MoqMediaTrackStreamProducer) now works like a ladder of frame producers, the follow-up moq-dev/moq#7029 left for later.lowertakes either producer.MoqVideoInit.lowerisOption<MoqMediaProducer>, a two-variant enum (Track { producer },Stream { producer }). A stream producer records the priority it was published at, so a rendition of either kind sits one priority below a stream rung, and a stream rung below a rendition of either kind.MoqMediaTrackStreamProducer::set_enabled. A byte-stream caller cannot see frame boundaries or keyframes, so the importer does the disable and resume itself (moq_mux::import::TrackStream::set_enabled):enabledand, once anything was published, marks a discontinuity. The splitter keeps running, so a chunk may continue a NAL across the call. Every access unit it completes after that, the one in flight included, is dropped, so the caller can keep writing the stream. A dropped access unit still reaches the importer (droppedon the H.264, H.265, and AV1 importers), so a sequence header or SPS seen only while disabled still configures the rendition. The H.264 and H.265 splitters already re-inject their parameter sets into the next keyframe. An AV1 keyframe that resumes without a sequence header gets the dropped one inserted after its temporal delimiter, so a new subscriber can decode it.enabledback on.lower=acceptsTrackProducer | TrackStreamProducer, andTrackStreamProducer.set_enabledis added.Media.Producerenum (.track/.stream), andTrackStreamProducer.setEnabledis added.media.Produceris a sealed interface implemented by both producers. A typed nil still means no lower rendition.TrackStreamProducer.SetEnabledis added.Produceralias, with the Dart bindings regenerated.moq::MediaProducer. OBS wraps its frame rung inkTrack.doc/lib/{py,swift,kt,go,dart,cpp}) and everyMIGRATE.mdare updated.moq-mux:track_stream_disables_and_resumes_at_a_keyframe. While disabled, the stream keeps flowing (a keyframe included) and nothing goes out. The marker breaks the timeline, and the importer resumes only on the next keyframe, flippingenabledwith it.moq-ffi:stream_rung_is_funded_after_its_lower_and_resumes_at_a_keyframebuilds a two-rung stream ladder. A 3 Mbps grant against 2 and 4 Mbps reservations funds the lower rung fully first. The upper rung disables, drops a keyframe written while disabled, and resumes on a later one.Impact
MoqVideoInit.lowerchanges fromOption<MoqMediaTrackProducer>toOption<MoqMediaProducer>. To migrate, wrap the handle asMoqMediaProducer::Track { producer }; seers/moq-ffi/MIGRATE.md.lower:takesMedia.Producer?; pass.track(producer).lowerfield takes the enum (MoqMediaProducer.Track(...),TrackMoqMediaProducer(producer: ...),moq::MediaProducer(moq::MediaProducer::kTrack{...})).media.VideoInit.Loweris themedia.Producerinterface. Assigning a*TrackProducer(nil included) compiles unchanged, but code that readsLowerback must type-assert it.lower=keeps working. Only code that sets the rawVideoInit.lowerfield changes.MoqMediaTrackStreamProducer::set_enabledandmoq_mux::import::TrackStream::set_enabled.Choices made
lowertype is an enum,MoqMediaProducer, rather than a shared rendition handle. The Plan left the pick open. The enum adds one type and no methods, and UniFFI already carries an object in an enum variant (MoqMediaTarget::Requested). A handle would need a new object plus arendition()method on each producer. It is named after what it holds, besideMoqMediaTrackProducerandMoqMediaTrackStreamProducer, rather thanMoqMediaRendition, which would read next to the consumer-sideMoqRendition.set_enabled(true)on its keyframe, but a byte-stream caller cannot find that keyframe without parsing the stream.TrackStreamalready splits it, so it gates its own output. This needs no extra publicdiscontinuityon the stream producer.enabledflips back at the resuming keyframe, not atset_enabled(true). Viewers then never select a rung that has no frames yet, matching the frame-producer guidance ("enable it again just before the keyframe that resumes it").set_enabled(true), even when that keyframe's bytes arrived just before the call. An encoder that paused with the rung keeps referencing that keyframe, so it stays decodable.Producer). Kotlin cannot reach nested classes through a typealias. In Dart,Track/Streamvariant aliases would collide withdart:async'sStream, so their docs name the generated variants.Decisions
The maintainer settled the open decision on 2026-11-03:
MoqVideoProducerOptions.lower(the built-in encoder's rung) also accept a pre-encoded producer? ✅ Leave it asMoqVideoProducer; the alternative was to acceptMoqMediaProducerwith a new ceiling API for pre-encoded rungs (such as the hint'sbitrateor an explicitmax_bps). The quest Plan listed it besideMoqVideoInit.lower, but an encoder rung follows its lower rendition's live bandwidth ceiling (video.rs,lower_ceiling), which pre-encoded producers lack, and the quest Goal is met without it. Unchanged here.Checks
just check origin/main(viaquest gate) on3f2ce48fde: every selected module passed (check rs,test rs,check py,test py,check c,check cpp,check obs_compile,check go,check kt,check dart,check js,test js, and the lint modules). Swift was skipped because Linux has no Swift toolchain, so theMedia.Producerwrapper relies on CI.cargo clippy -p moq-mux --all-targets -D warningsis clean.origin/main:b6607a9171and9582c27834resolvedMIGRATE.mdconflicts by keeping both entries: the first against main's iroh removal, the second against moq-dev/moq#7697'sDelayentries in all seven guides.check dartpasses on the committed generated bindings.07aad55d54resolved moq-dev/moq#7735 inrs/moq-mux/src/import/track.rs:TrackStream::initializeroutes main'ssplit.initialize(data)through this PR's gate. All 501 moq-muximport::andcodec::tests pass, fix(mux): preserve init headers across a pre-frame reset #7735'sheaders_survive_resetincluded.3f2ce48fdemerged 43 commits of main, including the catalog delay, TS timing, and other moq-mux changes. Only the sevenMIGRATE.mdguides conflicted, against main's typed setup refusals and moq-dev/moq#7691's JSON timestamps, and both entries are kept. Main changed none of the importer,import/track.rs, ormedia.rscode since the last merge. Itscontainer/producer.rschange is buffered-span jitter accounting and leaves discontinuity, grouping, andenabledalone. After the merge, 505 moq-muximport::andcodec::tests and 10 moq-ffirung,track_stream, andvideo_publishtests pass.just test interop --allpassed on4fe91ca639. Nothing since then touches the wire or session code: the later changes arers/moq-muximport gating, doc wording, and merges.quest check --base origin/main: ok.Review
quest review), gpt-6.1-sol, medium effort, session01a1a30e(started fresh after main changed the base's instructions).07aad55d54: one finding. Fixed in0910a5f79e: an AV1 sequence header that arrived only in a dropped keyframe configured the catalog but never reached subscribers, so the headerless resumed keyframe was undecodable for a new subscriber. The gate now passes dropped units to the importer'sdropped, and AV1 inserts the owed header into the resumed keyframe. The test asserts the payload, and fails without the insertion.0910a5f79e6a62842591cf34a53e97b13528d938: no findings.3f2ce48fde: review skipped. It is a hand-resolved merge of main that changed no code. Compared file by file with the reviewed head's diff (git diff 2a0fcb6c42...0910a5f79eagainstgit diff origin/main...3f2ce48fde), every added and removed line is identical, and only theMIGRATE.mdandquest/m1/README.mdcontext moved.01a1a26c: round 1 found that dropped keyframes lost their config, fixed infe803f3592. Rounds 2 to 4 found nothing; round 4 on9582c27834was the design checkpoint, with nothing left to fix.01a1a084: round 1 found that the splitter reset broke mid-NAL chunking, fixed ineec09447b7. Round 2 found nothing.ac1bc978d8, replied to, and resolved.Follow-ups
None.
🤖 Generated with Claude Code
(Written by Claude Opus 5.5)