Skip to content

feat(ffi)!: byte-stream producers form a simulcast ladder - #7699

Merged
kixelated merged 13 commits into
mainfrom
quest/m1/stream-ladder
Nov 4, 2026
Merged

kixelated merged 13 commits into
mainfrom
quest/m1/stream-ladder

Conversation

@kixelated

@kixelated kixelated commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

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.

  • lower takes either producer. MoqVideoInit.lower is Option<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):
    • Disabling flips the catalog enabled and, 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 (dropped on 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.
    • After it is enabled again, access units are still dropped until the next keyframe. That keyframe opens the resumed group and flips enabled back on.
  • Bindings:
    • Python: lower= accepts TrackProducer | TrackStreamProducer, and TrackStreamProducer.set_enabled is added.
    • Swift: Media.Producer enum (.track / .stream), and TrackStreamProducer.setEnabled is added.
    • Go: media.Producer is a sealed interface implemented by both producers. A typed nil still means no lower rendition. TrackStreamProducer.SetEnabled is added.
    • Kotlin and Dart: a Producer alias, with the Dart bindings regenerated.
    • C++: the generated moq::MediaProducer. OBS wraps its frame rung in kTrack.
  • Docs (doc/lib/{py,swift,kt,go,dart,cpp}) and every MIGRATE.md are updated.
  • Tests:
    • 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, flipping enabled with it.
    • moq-ffi: stream_rung_is_funded_after_its_lower_and_resumes_at_a_keyframe builds 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

  • Wire: none.
  • Breaking, moq-ffi (UniFFI): MoqVideoInit.lower changes from Option<MoqMediaTrackProducer> to Option<MoqMediaProducer>. To migrate, wrap the handle as MoqMediaProducer::Track { producer }; see rs/moq-ffi/MIGRATE.md.
  • Breaking, Swift: lower: takes Media.Producer?; pass .track(producer).
  • Breaking, Kotlin / Dart / C++: the generated lower field takes the enum (MoqMediaProducer.Track(...), TrackMoqMediaProducer(producer: ...), moq::MediaProducer(moq::MediaProducer::kTrack{...})).
  • Go: media.VideoInit.Lower is the media.Producer interface. Assigning a *TrackProducer (nil included) compiles unchanged, but code that reads Lower back must type-assert it.
  • Python: lower= keeps working. Only code that sets the raw VideoInit.lower field changes.
  • Additive: MoqMediaTrackStreamProducer::set_enabled and moq_mux::import::TrackStream::set_enabled.
  • Not changed: the C ABI. moq-c has no byte-stream producer, and its pre-encoded rung disable is quest/m1/c/api-gaps.md.

Choices made

  • The lower type 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 a rendition() method on each producer. It is named after what it holds, beside MoqMediaTrackProducer and MoqMediaTrackStreamProducer, rather than MoqMediaRendition, which would read next to the consumer-side MoqRendition.
  • The stream importer drops and resumes itself. A frame producer's caller drops frames and calls set_enabled(true) on its keyframe, but a byte-stream caller cannot find that keyframe without parsing the stream. TrackStream already splits it, so it gates its own output. This needs no extra public discontinuity on the stream producer.
  • The catalog enabled flips back at the resuming keyframe, not at set_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").
  • Before the first access unit, disabling skips the discontinuity marker, as feat(obs): disable simulcast rungs the grant cannot fund #7029 does for frame rungs. Nothing was published, so there is no timeline to break.
  • The splitter is never reset by a disable. A reset would discard a partial NAL that the next chunk continues (the review finding below). The gate drops completed access units instead. The resume therefore lands on the first keyframe the splitter completes after 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.
  • Kotlin and Dart alias only the enum type (Producer). Kotlin cannot reach nested classes through a typealias. In Dart, Track / Stream variant aliases would collide with dart:async's Stream, so their docs name the generated variants.

Decisions

The maintainer settled the open decision on 2026-11-03:

  1. Should MoqVideoProducerOptions.lower (the built-in encoder's rung) also accept a pre-encoded producer? ✅ Leave it as MoqVideoProducer; the alternative was to accept MoqMediaProducer with a new ceiling API for pre-encoded rungs (such as the hint's bitrate or an explicit max_bps). The quest Plan listed it beside MoqVideoInit.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 (via quest gate) on 3f2ce48fde: 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 the Media.Producer wrapper relies on CI. cargo clippy -p moq-mux --all-targets -D warnings is clean.
  • Merges of origin/main:
    • b6607a9171 and 9582c27834 resolved MIGRATE.md conflicts by keeping both entries: the first against main's iroh removal, the second against moq-dev/moq#7697's Delay entries in all seven guides. check dart passes on the committed generated bindings.
    • 07aad55d54 resolved moq-dev/moq#7735 in rs/moq-mux/src/import/track.rs: TrackStream::initialize routes main's split.initialize(data) through this PR's gate. All 501 moq-mux import:: and codec:: tests pass, fix(mux): preserve init headers across a pre-frame reset #7735's headers_survive_reset included.
    • 3f2ce48fde merged 43 commits of main, including the catalog delay, TS timing, and other moq-mux changes. Only the seven MIGRATE.md guides 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, or media.rs code since the last merge. Its container/producer.rs change is buffered-span jitter accounting and leaves discontinuity, grouping, and enabled alone. After the merge, 505 moq-mux import:: and codec:: tests and 10 moq-ffi rung, track_stream, and video_publish tests pass.
  • just test interop --all passed on 4fe91ca639. Nothing since then touches the wire or session code: the later changes are rs/moq-mux import gating, doc wording, and merges.
  • quest check --base origin/main: ok.

Review

  • Reviewer: Codex (quest review), gpt-6.1-sol, medium effort, session 01a1a30e (started fresh after main changed the base's instructions).
  • Round 1 on 07aad55d54: one finding. Fixed in 0910a5f79e: 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's dropped, and AV1 inserts the owed header into the resumed keyframe. The test asserts the payload, and fails without the insertion.
  • Round 2 on 0910a5f79e6a62842591cf34a53e97b13528d938: no findings.
  • Head 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...0910a5f79e against git diff origin/main...3f2ce48fde), every added and removed line is identical, and only the MIGRATE.md and quest/m1/README.md context moved.
  • Earlier sessions:
    • 01a1a26c: round 1 found that dropped keyframes lost their config, fixed in fe803f3592. Rounds 2 to 4 found nothing; round 4 on 9582c27834 was the design checkpoint, with nothing left to fix.
    • 01a1a084: round 1 found that the splitter reset broke mid-NAL chunking, fixed in eec09447b7. Round 2 found nothing.
  • pullfrog's doc wording thread: fixed in ac1bc978d8, replied to, and resolved.

Follow-ups

None.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 5 commits November 2, 2026 23:46
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>
@kixelated kixelated changed the title Byte-stream renditions form a ladder feat(ffi)!: byte-stream producers form a simulcast ladder Nov 3, 2026
kixelated and others added 2 commits November 3, 2026 09:13
# Conflicts:
#	dart/moq/MIGRATE.md
#	rs/moq-ffi/MIGRATE.md
…rter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 3, 2026 17:41
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 45e93c63-3829-413d-82b3-83a18a4c9c00
Commits

Reviewing files that changed from the base of the PR and between 9a29bc1 and 3f2ce48.

Files selected for processing: 28
  • cpp/moq/MIGRATE.md
  • cpp/obs/src/moq-output.cpp
  • dart/moq/MIGRATE.md
  • dart/moq/lib/media.dart
  • dart/moq_ffi/lib/src/moq.dart
  • doc/lib/cpp/index.md
  • doc/lib/dart/index.md
  • doc/lib/go/index.md
  • doc/lib/kt/index.md
  • doc/lib/py/index.md
  • doc/lib/swift/index.md
  • go/wrapper/MIGRATE.md
  • go/wrapper/media/media.go
  • kt/moq/MIGRATE.md
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/media/Media.kt
  • py/moq-rs/MIGRATE.md
  • py/moq-rs/moq/media.py
  • quest/m1/README.md
  • quest/m1/stream-ladder.md
  • rs/moq-ffi/MIGRATE.md
  • rs/moq-ffi/src/media.rs
  • rs/moq-ffi/src/test.rs
  • rs/moq-mux/src/codec/av1/import.rs
  • rs/moq-mux/src/codec/h264/import.rs
  • rs/moq-mux/src/codec/h265/import.rs
  • rs/moq-mux/src/import/track.rs
  • swift/MIGRATE.md
  • swift/Sources/Moq/Media.swift
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one recurring wording nit inline.

Reviewed changes

  • MoqMediaProducer enum (rs/moq-ffi/src/media.rs): MoqVideoInit.lower becomes Option<MoqMediaProducer> (Track / Stream). video_priority reads the lower track's priority, so either producer kind sits one rung below; MediaStreamProducer records the resolved priority and MoqMediaTrackStreamProducer::set_enabled is added.
  • TrackStream gate (rs/moq-mux/src/import/track.rs): a new Gate (Open/Closed/Resuming) plus a published flag drops access units while disabled, emits a discontinuity once 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 run configure.
  • Codec importers (h264/h265/av1): the inline keyframe-config block is extracted into configure; write_frames calls it. Behavior-preserving.
  • Bindings and docs: Python _lower, Swift Media.Producer, Go sealed media.Producer + SetEnabled, Kotlin/Dart Producer aliases, C++/OBS kTrack, all six doc/lib/*/index.md, and every MIGRATE.md updated.
  • Tests: track_stream_disables_and_resumes_at_a_keyframe and track_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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread doc/lib/py/index.md Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 written now reads drops what is written to it in all six doc/lib/{cpp,dart,go,kt,py,swift}/index.md pages (ac1bc978d).
  • Fixed the test doc comment: the same wording in rs/moq-ffi/src/test.rs's stream_rung_is_funded_after_its_lower_and_resumes_at_a_keyframe comment.
  • 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.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

kixelated and others added 3 commits November 3, 2026 11:52
# 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>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 arms owed_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.
  • dropped replaces configure on the gate's dropped frames (h264/h265/av1 importers): each importer exposes pub(crate) fn dropped, keeping configure private while the publish! macro still accounts for dropped access units. H.264/H.265 rely on the splitter re-injecting retained parameter sets; AV1 does not, hence owed_seq.
  • Merged origin/main (#7735): TrackStream::initialize routes the splitter's new initialize(data) through the gate, and find_sequence_header now returns the last (applied) header.
  • Test tightened: track_stream_configures_from_a_dropped_keyframe resumes through finish() 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.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 3, 2026
@moq-bot

moq-bot Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

quest land watch: the merge queue removed this pull request at 0910a5f because it conflicts with main (merge_conflict).

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)

@moq-bot

moq-bot Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

codex / Codex land fixer note:

The PR adds migration entries at the same insertion points as base. I reproduced the seven conflicts and verified that retaining both entries resolves them without changing behavior; git diff --check passed. The trial merge was aborted and the PR branch remains at its pinned head.

Validation could not be completed: swift test --package-path swift returned /bin/bash: line 1: swift: command not found. The Rust build command cargo build --locked -p moq-mux -p moq-ffi also reported failed to run custom build command for moq-vaapi v0.1.0 and Unable to find libclang. The build was then interrupted. No passing build or test result is claimed.

Failure log, lines 3–25:

3: Auto-merging cpp/moq/MIGRATE.md
4: CONFLICT (content): Merge conflict in cpp/moq/MIGRATE.md
5: Auto-merging dart/moq/MIGRATE.md
6: CONFLICT (content): Merge conflict in dart/moq/MIGRATE.md
7: Auto-merging dart/moq_ffi/lib/src/moq.dart
8: Auto-merging doc/lib/dart/index.md
9: Auto-merging doc/lib/go/index.md
10: Auto-merging doc/lib/kt/index.md
11: Auto-merging doc/lib/py/index.md
12: Auto-merging doc/lib/swift/index.md
13: Auto-merging go/wrapper/MIGRATE.md
14: CONFLICT (content): Merge conflict in go/wrapper/MIGRATE.md
15: Auto-merging kt/moq/MIGRATE.md
16: CONFLICT (content): Merge conflict in kt/moq/MIGRATE.md
17: Auto-merging py/moq-rs/MIGRATE.md
18: CONFLICT (content): Merge conflict in py/moq-rs/MIGRATE.md
19: Auto-merging quest/m1/README.md
20: Auto-merging rs/moq-ffi/MIGRATE.md
21: CONFLICT (content): Merge conflict in rs/moq-ffi/MIGRATE.md
22: Auto-merging rs/moq-ffi/src/test.rs
23: Auto-merging swift/MIGRATE.md
24: CONFLICT (content): Merge conflict in swift/MIGRATE.md
25: Automatic merge failed; fix conflicts and then commit the result.

(written by codex / Codex)

@moq-bot

moq-bot Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

codex / Codex land fixer: Seven additive MIGRATE.md conflicts; validation blocked: &#96;swift test --package-path swift&#96; reports &#96;swift: command not found&#96;; &#96;cargo build --locked -p moq-mux -p moq-ffi&#96; reports &#96;Unable to find libc. Recommendation: Merge base retaining both migration entries in all seven files, then build and test all affected packages in an environment with Swift and libclang.

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
@kixelated
kixelated added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit d46b69a Nov 4, 2026
10 of 11 checks passed
@kixelated
kixelated deleted the quest/m1/stream-ladder branch November 4, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant