Skip to content

feat(bindings): fetch and decode a retained media group - #2827

Open
kixelated wants to merge 5 commits into
mainfrom
claude/ffi-fetch-media-group
Open

feat(bindings): fetch and decode a retained media group#2827
kixelated wants to merge 5 commits into
mainfrom
claude/ffi-fetch-media-group

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Summary

This is the fetch_media_group half of #2764, split out so it can land on main now. The commits are Qizot's, rebased and stripped of the timeline surface.

Fetch one retained group by sequence and decode it through the rendition's container, without opening a live subscription. It sits between the two things we already have: fetch_group hands back raw payloads, subscribe_media decodes but only live. Neither lets a binding client pull a single past group as media frames, which is what a seek, a thumbnail, or a segment-at-a-time exporter needs.

Why the timeline part is not here

The rest of #2764 exposes hang::catalog::Timeline and its group index through the FFI. That model is per media track on main (VideoConfig.timeline / AudioConfig.timeline, one record per group), and dev has already replaced it:

main dev
Section location per rendition catalog root, one per broadcast
Record unit one per group {group, pts} one per segment {segment, pts, duration, tracks}
Section fields track, timescale, wall + duration_max
Track name <rendition>.timeline.z timeline.z (DEFAULT_NAME)

Shipping the main shape through uniffi means MoqVideo.timeline / MoqAudio.timeline have to be removed later and MoqTimelineEntry reshaped, which is a semver break in published dev.moq:moq and Swift packages rather than an additive change. Better to wrap the segment model once it settles. Nothing here depends on it.

Public API changes

All additive, hence main.

  • moq-mux adds container::GroupConsumer.
  • moq-ffi adds MoqMediaGroupConsumer and MoqBroadcastConsumer.fetch_media_group.
  • Swift adds MediaGroupConsumer and BroadcastConsumer.fetchMediaGroup.
  • Kotlin adds the MediaGroupConsumer alias and a frames() Flow.

The refactor

Qizot's version decoded the group inline in moq-ffi: a pending queue, a Container::read loop, and a hand-rolled copy of the first-frame keyframe invariant that container::Consumer already implements. That put a decoding primitive in the bindings layer where no Rust caller can reach it, and stated the invariant in two places.

The last commit moves it to moq_mux::container::GroupConsumer, the group-scoped counterpart to container::Consumer: it reads exactly the group it is handed, with no subscription, group ordering, or latency skipping, and mirrors read/poll_read. moq-hls wants this same primitive to serve a segment from a FETCH. moq-ffi now wraps it, and the keyframe invariant lives in one place.

Test plan

  • cargo nextest run -p moq-mux (two new tests: a legacy group read to completion, and a CMAF fragment whose batch is handed back one frame at a time)
  • cargo nextest run -p moq-ffi (56 passing, including the three fetch-media-group tests carried over from feat(bindings): expose retained media group fetching #2764)
  • just check / just test

Note that just check does not compile moq-ffi or libmoq, so the moq-ffi tests above were run directly.

Cross-package sync

doc/lib/{kt,swift} and both wrapper READMEs are updated. Skipped rows, per the table in CLAUDE.md:

  • rs/libmoq and doc/lib/c: no C ABI is added.
  • py/ and go/wrapper: both need hand-written wrappers on top of the generated bindings, and feat(bindings): expose retained media group fetching #2764 did not add them either. This leaves Swift/Kotlin able to fetch media groups while Python and Go cannot, which is a parity gap worth a follow-up rather than a reason to hold this.

Original PR: #2764. Commits 1-4 are authored by @Qizot.

(Written by Opus 5)

Qizot and others added 5 commits August 12, 2026 23:10
Add `MoqBroadcastConsumer.fetch_media_group`: fetch one retained group by
sequence and decode its track container into media frames, without opening
a live subscription or applying latency-based group skipping.

This sits between the existing `fetch_group` (raw byte payloads) and
`subscribe_media` (live, container-decoded), reusing `MoqFetchGroupOptions`
for delivery priority.

The container is parsed before the fetch is issued so invalid CMAF init data
does not leave a dynamic group request waiting for a consumer that can never
read it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wrap `fetch_media_group` as `BroadcastConsumer.fetchMediaGroup` and add
`MediaGroupConsumer`, an `AsyncSequence` over the finite group's decoded
frames that cancels the native read when iteration ends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alias `MediaGroupConsumer` and add a `frames()` Flow over a fetched group,
matching the cancellation handling of the existing consumer flows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`fetch_media_group` decoded its group inline in moq-ffi: a pending queue, a
`Container::read` loop, and a hand-rolled copy of the first-frame keyframe
invariant that `container::Consumer` already implements. That put a decoding
primitive in the bindings layer, where no Rust caller can reach it, and left
the invariant stated in two places.

Move it into `moq_mux::container::GroupConsumer`: the group-scoped counterpart
to `container::Consumer`, reading exactly the group it is handed with no
subscription, group ordering, or latency skipping. `read`/`poll_read` mirror
the track-level consumer, so moq-hls can use it to serve a segment from a
FETCH without going through a live subscription.

moq-ffi now wraps it, and the keyframe invariant lives only in moq-mux.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@kixelated, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a16afc6-c55c-47b0-9b81-7c4fd62a8fec

📥 Commits

Reviewing files that changed from the base of the PR and between f91e3bb and 8468bca.

📒 Files selected for processing (13)
  • doc/lib/kt/moq.md
  • doc/lib/swift/moq.md
  • kt/README.md
  • kt/gradle.properties
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt
  • kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt
  • kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
  • rs/moq-ffi/src/consumer.rs
  • rs/moq-ffi/src/test.rs
  • rs/moq-mux/src/container/group.rs
  • rs/moq-mux/src/container/mod.rs
  • swift/Sources/Moq/Broadcast.swift
  • swift/Sources/Moq/Media.swift

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

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8468bcaef0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// Unlike [`Self::subscribe_media`], this does not create a live subscription or apply
/// latency-based group skipping. The returned consumer reads exactly the requested group
/// until [`MoqMediaGroupConsumer::next`] returns `None`.
pub async fn fetch_media_group(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add fetched-media support to the remaining bindings

When this FFI API ships, the ergonomic Python and Go packages still cannot use it: py/moq-rs/moq/subscribe.py and go/wrapper/moq/subscribe.go expose raw fetch_group and live subscribe_media, but have no fetched-media consumer or method, and rs/libmoq plus the Python, Go, and C docs are also unchanged. Add the corresponding wrappers, exports, tests, and documentation so the public binding surfaces remain synchronized. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L170-L174

Useful? React with 👍 / 👎.

}

/// Fetch one complete group and decode its track container into media frames.
public func fetchMediaGroup(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bump the Swift wrapper version before publishing

The new Swift API will not reach package users because this commit leaves swift/VERSION at 0.4.3, which was already assigned by commit 05c7bf3. The checked release-swift-lib.yml treats an existing mirror tag as the release gate and skips publication, so bump the wrapper version alongside these source changes. (Written by GPT-5.6 Sol)

Useful? React with 👍 / 👎.

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.

2 participants