Skip to content

feat(bindings): expose retained media group fetching - #2764

Open
Qizot wants to merge 5 commits into
moq-dev:mainfrom
Qizot:feat/fetch-media
Open

feat(bindings): expose retained media group fetching#2764
Qizot wants to merge 5 commits into
moq-dev:mainfrom
Qizot:feat/fetch-media

Conversation

@Qizot

@Qizot Qizot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Let binding clients fetch one retained media group and decode it without opening a live subscription.
  • Expose each rendition's catalog-advertised timeline so clients can map timestamps to group sequences before fetching.
  • Add idiomatic Swift AsyncSequence and Kotlin Flow helpers for finite media groups and timelines.
  • Enable native Android logging through the existing android-logcat feature.

Public API changes

All changes are additive and target main.

  • moq-ffi adds MoqTimeline, MoqTimelineEntry, MoqTimelineConsumer, and MoqMediaGroupConsumer; adds MoqBroadcastConsumer.fetch_media_group and subscribe_timeline; and adds optional timeline metadata to MoqVideo and MoqAudio.
  • Swift adds Timeline, TimelineEntry, TimelineConsumer, and MediaGroupConsumer; and adds BroadcastConsumer.fetchMediaGroup and subscribeTimeline.
  • Kotlin adds Timeline, TimelineEntry, TimelineConsumer, and MediaGroupConsumer aliases; and adds frames() and entries() Flow helpers.

Test plan

  • cargo test -p moq-ffi
  • cargo test -p moq-mux import::track::tests
  • cargo fmt --all -- --check
  • just kt check
  • just swift check
  • git diff --check

The full Kotlin check passed before the final base refresh. Post-rebase UniFFI Kotlin generation passed, while the local shell no longer exposed the repository's required Gradle 8 executable; CI will rerun the full Kotlin matrix on the final commit.

Cross-package sync

The Kotlin and Swift wrapper READMEs and library docs are updated. libmoq, Python, Go, and their docs are unchanged because this change does not add a C ABI or hand-written Python/Go wrapper surface.

Post-Deploy Monitoring & Validation

No additional operational monitoring required. This is an additive library API change covered by FFI and language-wrapper tests.

(Written by GPT-5)

Qizot added 5 commits August 12, 2026 09:58
Expose generic media and timeline consumers. Keep group selection and playback policy at the application layer.
Build Android moq-ffi artifacts with the existing android-logcat feature so native logs are available through logcat.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The FFI now represents catalog timelines and timeline entries. It supports timeline subscriptions and finite media-group fetching with container-aware frame decoding, buffering, sequencing, and cancellation. Kotlin adds typealiases and Flow extensions. Swift adds timeline and media-group async sequences. Integration tests cover Legacy and CMAF decoding, invalid containers, timestamps, keyframe metadata, completion, and timeline mappings. Documentation and dependency references now use version 0.4.3.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: exposing retained media group fetching in bindings.
Description check ✅ Passed The description accurately explains the binding APIs, timeline support, platform changes, tests, and documentation updates.
Docstring Coverage ✅ Passed Docstring coverage is 93.18% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@doc/lib/kt/moq.md`:
- Around line 236-238: Update the fetchMediaGroup call in the timeline entry
example to pass the required fourth FetchGroupOptions argument, using null or
FetchGroupOptions() as appropriate, while preserving the existing group and
frame collection flow.

In `@kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt`:
- Around line 118-135: Move the media completion call in the smoke test so
`media.finish()` runs immediately after `writeFrame` and before
`it.frames().toList()` collects the finite flow. Keep `broadcast.finish()`
before timeline-entry collection and preserve the existing frame assertions.

In `@rs/moq-ffi/src/consumer.rs`:
- Around line 158-161: Document the exported FFI surface by adding rustdoc
comments to the public MoqTimelineConsumer struct and its
MoqTimelineConsumer::cancel method, describing their purpose and cancellation
behavior. Ensure both exported Rust items are covered without changing their
implementation.
- Around line 1-5: Add a module-level `//!` documentation comment at the
beginning of the Rust module root before the imports in consumer.rs, describing
the module’s purpose and following the repository’s documentation conventions.

In `@rs/moq-ffi/src/media.rs`:
- Around line 30-32: Document every newly exported field involved in the
timeline bindings: add concise Rust doc comments to the fields of MoqTimeline,
the MoqVideo::timeline and MoqAudio::timeline fields, and the MoqTimelineEntry
fields. Keep the descriptions short and accurate for the generated binding API.

In `@rs/moq-ffi/src/test.rs`:
- Around line 497-512: Update the async tests around
fetch_media_group_decodes_multiple_cmaf_samples and
fetch_media_group_rejects_invalid_container_before_fetching: call
tokio::time::pause() at the start of the decoding test, and wrap the
invalid-container fetch_media_group request in a short tokio::time::timeout
before asserting the expected MoqError::Codec result.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f65c0e7-f2a8-4c0c-947e-f20559d4d8ca

📥 Commits

Reviewing files that changed from the base of the PR and between 9040cfe and e9ef8d3.

📒 Files selected for processing (15)
  • 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/build.sh
  • rs/moq-ffi/src/audio.rs
  • rs/moq-ffi/src/consumer.rs
  • rs/moq-ffi/src/media.rs
  • rs/moq-ffi/src/test.rs
  • swift/Sources/Moq/Aliases.swift
  • swift/Sources/Moq/Broadcast.swift
  • swift/Sources/Moq/Media.swift

Comment thread doc/lib/kt/moq.md
Comment on lines +236 to +238
consumer.subscribeTimeline(timeline).entries().collect { entry ->
consumer.fetchMediaGroup(name, entry.group, audio.container).use { group ->
group.frames().collect { frame ->

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f -e kt . kt/moq | xargs -r rg -n -C 8 \
  '\bfetchMediaGroup\s*\(|FetchGroupOptions'

Repository: moq-dev/moq

Length of output: 6063


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Kotlin declarations and call sites ---'
rg -n -C 12 'fetchMediaGroup|fun fetchGroup|FetchGroupOptions' kt --glob '*.kt'

printf '%s\n' '--- Generated/API declaration sources ---'
rg -n -C 10 'fetch_media_group|fetchMediaGroup|FetchGroupOptions|MoqFetchGroupOptions' . \
  --glob '!target/**' --glob '!build/**' --glob '!node_modules/**'

Repository: moq-dev/moq

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Rust UniFFI fetch declarations ---'
rg -n -A 45 -B 12 \
  'fetch_media_group|fetchMediaGroup|fetch_group|MoqFetchGroupOptions' \
  rs/moq-ffi/src/consumer.rs

printf '%s\n' '--- Kotlin documentation context ---'
sed -n '210,255p' doc/lib/kt/moq.md

printf '%s\n' '--- Kotlin build and binding-generation configuration ---'
rg -n -C 8 \
  'uniffi|bindgen|moqffi|moq-ffi|generated' \
  kt --glob '*.gradle' --glob '*.gradle.kts' --glob '*.toml' --glob '*.properties' --glob '*.md'

Repository: moq-dev/moq

Length of output: 32804


🌐 Web query:

moq-dev/moq Kotlin fetchMediaGroup MoqFetchGroupOptions generated Kotlin signature

💡 Result:

The project moq-dev/moq provides ergonomic Kotlin bindings for Media over QUIC [1]. While the library utilizes UniFFI to generate bindings from a Rust core [2][3], the specific API signature for fetching groups—often discussed in the context of the MoQ transport protocol [4]—is structured around Fetch control messages [5][6]. Based on current project structures and available documentation, MoqFetchGroupOptions is not a standard, high-level signature found in the public dev.moq:moq Kotlin library. Instead, the library manages fetches via native-to-Kotlin FFI bindings that mirror the underlying moq-transport message structure [2][1]. In the broader moq ecosystem (including implementations like moqtail that share design patterns with moq-dev), fetching is typically handled by passing an object containing parameters such as priority, group order, and track location [5][7][8]. In the moq-dev/moq Kotlin bindings, you should look to use the Kotlin idiomatic APIs (typically involving Coroutines and Flow) provided by the dev.moq:moq artifact [2][1]. Because the Kotlin bindings are auto-generated from the Rust core [2][3], the specific class or function signatures are subject to the current state of the moq-ffi layer [9]. To identify the exact generated signature for your version, you can inspect the generated Kotlin source files usually located in build/generated/source/uniffi after a clean build of your project [3].

Citations:


🌐 Web query:

UniFFI Kotlin generated default argument Option parameter uniffi default None

💡 Result:

In UniFFI, you can specify None as a default value for Option<T> parameters when using proc-macros, which translates to null in the generated Kotlin code [1][2][3]. For function or method arguments, you can define the default in the #[uniffi::export] attribute using the default argument [2][3]. For example: #[uniffi::export(default(max_splits = None))] pub fn split(max_splits: Option) -> Vec { //... } This will generate a Kotlin function signature with a nullable parameter defaulted to null: fun split(maxSplits: UInt? = null): List {... } When using UDL files, optional arguments can be defined similarly. While null is the equivalent in Kotlin bindings, the UniFFI documentation specifically notes that when using proc-macros, you use the None literal to signify the default value for an Option<T> type [1][2]. For record fields, you can also assign defaults in a similar manner, ensuring the generated Kotlin data class correctly initializes the field to null if None is provided as the default [4][3].

Citations:


Pass the fetch options argument.

fetchMediaGroup requires a fourth nullable FetchGroupOptions parameter with no default. Add null or FetchGroupOptions() so the example compiles.

(Written by )

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doc/lib/kt/moq.md` around lines 236 - 238, Update the fetchMediaGroup call in
the timeline entry example to pass the required fourth FetchGroupOptions
argument, using null or FetchGroupOptions() as appropriate, while preserving the
existing group and frame collection flow.

Comment on lines +118 to +135
media.writeFrame(Frame(payload = "opus frame".encodeToByteArray(), timestampUs = 5_000_000uL))

val fetched: MediaGroupConsumer = consumer.fetchMediaGroup(
name,
0uL,
audio.container,
FetchGroupOptions(priority = 3u),
)
fetched.use {
assertEquals(0uL, it.sequence())
val frames = it.frames().toList()
assertEquals(1, frames.size)
val frame = frames.single()
assertEquals("opus frame", frame.payload.decodeToString())
assertEquals(5_000_000uL, frame.timestampUs)
}

media.finish()

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Finish the media group before collecting the finite Flow.

it.frames().toList() returns only after next() returns null. The group remains open until media.finish() at Line 135, but that line is unreachable while collection waits.

Call media.finish() after writeFrame and before collecting frames(). Keep broadcast.finish() before collecting timeline entries.

(Written by CodeRabbit)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt` around lines 118 -
135, Move the media completion call in the smoke test so `media.finish()` runs
immediately after `writeFrame` and before `it.frames().toList()` collects the
finite flow. Keep `broadcast.finish()` before timeline-entry collection and
preserve the existing frame assertions.

Comment on lines +1 to +5
use std::collections::VecDeque;
use std::sync::Arc;

use bytes::Buf;
use moq_mux::container::Container as _;

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a module-level documentation comment.

Add a //! comment before the imports. rs/moq-ffi/src/consumer.rs is a Rust module root.

As per coding guidelines, **/*.rs: “add a module-level //! doc comment to every Rust module root.”

(Written by CodeRabbit)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-ffi/src/consumer.rs` around lines 1 - 5, Add a module-level `//!`
documentation comment at the beginning of the Rust module root before the
imports in consumer.rs, describing the module’s purpose and following the
repository’s documentation conventions.

Source: Coding guidelines

Comment on lines +158 to +161
#[derive(uniffi::Object)]
pub struct MoqTimelineConsumer {
task: Task<Timeline>,
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the exported timeline consumer surface.

Add rustdoc for MoqTimelineConsumer and MoqTimelineConsumer::cancel. Both are public FFI items.

As per coding guidelines, **/*.rs: “Document every exported Rust item.”

(Written by CodeRabbit)

Also applies to: 338-340

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-ffi/src/consumer.rs` around lines 158 - 161, Document the exported FFI
surface by adding rustdoc comments to the public MoqTimelineConsumer struct and
its MoqTimelineConsumer::cancel method, describing their purpose and
cancellation behavior. Ensure both exported Rust items are covered without
changing their implementation.

Source: Coding guidelines

Comment thread rs/moq-ffi/src/media.rs
Comment on lines +30 to +32
pub track: String,
pub timescale: u32,
pub wall: Option<u64>,

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the newly exported record fields.

Add short field documentation for MoqTimeline, MoqVideo::timeline, MoqAudio::timeline, and MoqTimelineEntry. These fields are exposed through generated binding APIs.

As per coding guidelines, **/*.rs: “Document every exported Rust item.”

(Written by CodeRabbit)

Also applies to: 116-116, 127-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-ffi/src/media.rs` around lines 30 - 32, Document every newly exported
field involved in the timeline bindings: add concise Rust doc comments to the
fields of MoqTimeline, the MoqVideo::timeline and MoqAudio::timeline fields, and
the MoqTimelineEntry fields. Keep the descriptions short and accurate for the
generated binding API.

Source: Coding guidelines

Comment thread rs/moq-ffi/src/test.rs
Comment on lines +497 to +512
#[tokio::test]
async fn fetch_media_group_rejects_invalid_container_before_fetching() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let _track = broadcast.create_track("media", None).unwrap();
let consumer = MoqBroadcastConsumer::new(broadcast.consume());

let result = consumer
.fetch_media_group(
"media".into(),
0,
crate::media::MoqContainer::Cmaf { init: Vec::new() },
None,
)
.await;

assert!(matches!(result, Err(MoqError::Codec(_))));

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make the async failure checks deterministic.

Call tokio::time::pause() at the start of fetch_media_group_decodes_multiple_cmaf_samples.

Wrap the invalid-container request in a short tokio::time::timeout. If container validation regresses, fetch_group can wait for the dynamic group and stall this test instead of producing a direct assertion failure.

As per coding guidelines, rs/**/src/**/*.rs: “Async tests that depend on time must call tokio::time::pause() first for deterministic timer behavior.”

(Written by CodeRabbit)

Also applies to: 515-577

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rs/moq-ffi/src/test.rs` around lines 497 - 512, Update the async tests around
fetch_media_group_decodes_multiple_cmaf_samples and
fetch_media_group_rejects_invalid_container_before_fetching: call
tokio::time::pause() at the start of the decoding test, and wrap the
invalid-container fetch_media_group request in a short tokio::time::timeout
before asserting the expected MoqError::Codec result.

Source: Coding guidelines

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