Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions doc/lib/kt/moq.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,24 +228,28 @@ Call `request.abort(code)` when the requested group cannot be produced. Fetch is

### Fetching media groups

`fetchGroup` hands back raw payloads. `fetchMediaGroup` decodes the same group through the rendition's advertised container, so you get timestamped frames without opening a live subscription:
Catalog audio and video renditions include an optional timeline that maps presentation timestamps to retained group sequences. Subscribe to that index, then fetch and decode a specific group with the rendition's advertised container:

```kotlin
val (name, audio) = consumer.catalog().audio.entries.first()

consumer.fetchMediaGroup(
name,
42uL,
audio.container,
FetchGroupOptions(priority = 10u),
).use { group ->
group.frames().collect { frame ->
println("${frame.timestampUs}: ${frame.payload.size} bytes")
val catalog = consumer.catalog()
val (name, audio) = catalog.audio.entries.first()
val timeline = requireNotNull(audio.timeline)

consumer.subscribeTimeline(timeline).entries().collect { entry ->
consumer.fetchMediaGroup(
name,
entry.group,
audio.container,
FetchGroupOptions(priority = 10u),
).use { group ->
group.frames().collect { frame ->
Comment on lines +238 to +245

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.

println("${frame.timestampUs}: ${frame.payload.size} bytes")
}
}
}
```

`frames()` is a cancellation-aware `Flow`. A fetched media group is finite: it completes after the group's last decoded frame, unlike the live `subscribeMedia` stream. Latency-based group skipping does not apply, so you always get every frame in the group.
`entries()` and `frames()` are cancellation-aware `Flow` helpers. A fetched media group is finite: it completes after the group's last decoded frame, unlike the live `subscribeMedia` stream. Latency-based group skipping does not apply, so you always get every frame in the group.

### On-demand raw tracks

Expand Down
22 changes: 13 additions & 9 deletions doc/lib/swift/moq.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,23 +174,27 @@ Call `request.abort(errorCode:)` when the requested group cannot be produced. Fe

### Fetching media groups

`fetchGroup` hands back raw payloads. `fetchMediaGroup` decodes the same group through the rendition's advertised container, so you get timestamped frames without opening a live subscription:
Catalog audio and video renditions include an optional timeline that maps presentation timestamps to retained group sequences. Subscribe to that index, then fetch and decode a specific group with the rendition's advertised container:

```swift
let catalog = try await consumer.subscribeCatalog().next()!
let (name, audio) = catalog.audio.first!

let group = try await consumer.fetchMediaGroup(
name: name,
sequence: 42,
container: audio.container
)
for try await frame in group {
print(frame.timestampUs, frame.payload.count)
if let timeline = audio.timeline {
for try await entry in try await consumer.subscribeTimeline(timeline) {
let group = try await consumer.fetchMediaGroup(
name: name,
sequence: entry.group,
container: audio.container
)
for try await frame in group {
print(frame.timestampUs, frame.payload.count)
}
}
}
```

`MediaGroupConsumer` is an `AsyncSequence`, and cancels the native read when iteration ends. A fetched media group is finite: it completes after the group's last decoded frame, unlike the live `subscribeMedia` stream. Latency-based group skipping does not apply, so you always get every frame in the group.
Both timeline and fetched media consumers are `AsyncSequence` values and cancel native reads when iteration ends. A fetched media group is finite: it completes after the group's last decoded frame, unlike the live `subscribeMedia` stream. Latency-based group skipping does not apply, so you always get every frame in the group.

### On-demand raw tracks

Expand Down
2 changes: 1 addition & 1 deletion kt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The `dev.moq` package is intentionally thin: Kotlin has extension functions, so
- **`Moq.connect(...)`**: a connection facade (`Moq.kt`), so you never hand-wire a `MoqClient`.
- **Typealiases** (`Aliases.kt`): re-export the `Moq*`-prefixed FFI types under clean `dev.moq` names (`OriginProducer`, `BroadcastConsumer`, `Catalog`, `Frame`, ...), so you import `dev.moq.*` only. A couple of sealed types (`Container`, `MoqException`) are not aliased because Kotlin can't resolve their subtypes through a typealias; use `uniffi.moq.*` for those.
- **Flow extensions** (`Flows.kt`): `updates()`, `groups()`, `frames()`, `announcements()`, `catalog()` turn the pull-based consumers into coroutine `Flow`s with cancellation wired through.
- **Fetched media**: `fetchMediaGroup(...).frames()` streams the decoded frames of one retained group, then completes.
- **Fetched media and timelines**: `fetchMediaGroup(...).frames()` streams decoded frames from one retained group, while `subscribeTimeline(...).entries()` streams its timestamp-to-group index.
- **`logLevel(...)`**: configures native Rust tracing without importing the raw bindings package.
- **Raw datagrams**: `TrackProducer.appendDatagram(Frame(payload, timestampUs))` sends one best-effort frame and returns its sequence; `TrackConsumer.recvDatagram()` and `datagrams()` receive them. Payloads are capped at 1200 bytes, require a datagram-capable transport plus lite-05 or newer moq-lite, and have no stream fallback.
- **`MoqException.isShutdown`** (`Errors.kt`): true for the graceful `Cancelled`/`Closed` cases.
Expand Down
6 changes: 6 additions & 0 deletions kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Aliases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ typealias MediaStreamProducer = uniffi.moq.MoqMediaStreamProducer
typealias MediaConsumer = uniffi.moq.MoqMediaConsumer
/** A finite fetched media group: yields container-decoded frames until the group ends. */
typealias MediaGroupConsumer = uniffi.moq.MoqMediaGroupConsumer
/** A media timeline subscription: yields timestamp-to-group index entries. */
typealias TimelineConsumer = uniffi.moq.MoqTimelineConsumer
/** The write side of a raw-audio track; PCM written here is encoded inside the FFI boundary. */
typealias AudioProducer = uniffi.moq.MoqAudioProducer
/** The read side of a raw-audio track: yields decoded PCM frames. */
Expand Down Expand Up @@ -111,6 +113,10 @@ typealias Route = uniffi.moq.MoqRoute
typealias Subscription = uniffi.moq.MoqSubscription
/** Options for fetching one past group by sequence. */
typealias FetchGroupOptions = uniffi.moq.MoqFetchGroupOptions
/** A media track's companion timestamp-to-group index. */
typealias Timeline = uniffi.moq.MoqTimeline
/** One timestamp-to-group mapping read from a media timeline track. */
typealias TimelineEntry = uniffi.moq.MoqTimelineEntry
/** Delivery settings for a raw track: priority, ordering, latency budget, and timescale. */
typealias TrackInfo = uniffi.moq.MoqTrackInfo
/** One audio frame: PCM payload bytes plus a presentation timestamp. */
Expand Down
12 changes: 12 additions & 0 deletions kt/moq/src/jvmAndAndroidMain/kotlin/dev/moq/Flows.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import uniffi.moq.MoqMediaGroupConsumer
import uniffi.moq.MoqOriginConsumer
import uniffi.moq.MoqOriginDynamic
import uniffi.moq.MoqRoute
import uniffi.moq.MoqTimelineConsumer
import uniffi.moq.MoqTimelineEntry
import uniffi.moq.MoqTrackConsumer
import uniffi.moq.MoqTrackDynamic
import uniffi.moq.MoqTrackRequest
Expand Down Expand Up @@ -81,6 +83,16 @@ fun MoqMediaGroupConsumer.frames(): Flow<MoqMediaFrame> = flow {
if (cause is CancellationException) cancel()
}

/** Stream of indexed group boundaries from a media timeline track. */
fun MoqTimelineConsumer.entries(): Flow<MoqTimelineEntry> = flow {
while (true) {
currentCoroutineContext().ensureActive()
emit(next() ?: break)
}
}.onCompletion { cause ->
if (cause is CancellationException) cancel()
}

/**
* Stream of decoded audio frames in the layout declared by the
* `MoqAudioDecoderConfig` the consumer was created with.
Expand Down
15 changes: 12 additions & 3 deletions kt/moq/src/jvmAndAndroidTest/kotlin/dev/moq/SmokeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,16 @@ class SmokeTest {
}
}

/** A fetched media group streams its decoded frames and then completes. */
@Test
fun `media group helper streams fetched frames`() = runTest {
fun `media group and timeline helpers stream fetched data`() = runTest {
BroadcastProducer().use { broadcast ->
val media = broadcast.publishMedia(
Init(format = "opus", data = opusHead(), video = null),
)
val consumer = broadcast.consume()
val (name, audio) = consumer.catalog().audio.entries.single()
val timeline = checkNotNull(audio.timeline)
val timelineConsumer: TimelineConsumer = consumer.subscribeTimeline(timeline)

media.writeFrame(Frame(payload = "opus frame".encodeToByteArray(), timestampUs = 5_000_000uL))

Expand All @@ -124,7 +125,6 @@ class SmokeTest {
audio.container,
FetchGroupOptions(priority = 3u),
)

// Close the group so the fetched stream terminates instead of waiting for more.
media.finish()

Expand All @@ -136,6 +136,15 @@ class SmokeTest {
assertEquals("opus frame", frame.payload.decodeToString())
assertEquals(5_000_000uL, frame.timestampUs)
}

broadcast.finish()
timelineConsumer.use {
val entries = it.entries().toList()
assertEquals(1, entries.size)
val entry = entries.single()
assertEquals(0uL, entry.group)
assertEquals(5_000_000uL, entry.timestampUs)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions rs/moq-ffi/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ mod tests {
channel_count: 2,
bitrate: None,
container: MoqContainer::Legacy,
timeline: None,
}
}

Expand Down
42 changes: 42 additions & 0 deletions rs/moq-ffi/src/consumer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,27 @@ pub struct MoqCatalogConsumer {
task: Task<Catalog>,
}

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

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


struct Timeline {
inner: moq_mux::timeline::Consumer,
}

impl Timeline {
async fn next(&mut self) -> Result<Option<MoqTimelineEntry>, MoqError> {
let Some(entry) = self.inner.next().await? else {
return Ok(None);
};
Ok(Some(MoqTimelineEntry {
group: entry.group,
timestamp_us: timestamp_us(entry.pts)?,
}))
}
}

struct Catalog {
// Consume with the untyped `Extra` extension so application sections survive into
// `MoqCatalog.sections` instead of being dropped.
Expand Down Expand Up @@ -218,6 +239,15 @@ impl MoqBroadcastConsumer {
}))
}

/// Subscribe to a media track's timestamp-to-group timeline index.
pub async fn subscribe_timeline(&self, timeline: MoqTimeline) -> Result<Arc<MoqTimelineConsumer>, MoqError> {
let timeline: hang::catalog::Timeline = timeline.into();
let consumer = moq_mux::timeline::Consumer::subscribe(&self.inner, &timeline).await?;
Ok(Arc::new(MoqTimelineConsumer {
task: Task::new(Timeline { inner: consumer }),
}))
}

/// Subscribe to a track by name, the same pattern as moq-boy's command/status tracks.
///
/// Frames are returned as plain byte payloads with no codec or container parsing.
Expand Down Expand Up @@ -296,6 +326,18 @@ impl MoqBroadcastConsumer {
}
}

#[uniffi::export]
impl MoqTimelineConsumer {
/// Return the next indexed group boundary, or `None` when the timeline ends.
pub async fn next(&self) -> Result<Option<MoqTimelineEntry>, MoqError> {
self.task.run(|mut state| async move { state.next().await }).await
}

pub fn cancel(&self) {
self.task.cancel();
}
}

fn map_fetch_error(err: moq_net::Error) -> MoqError {
match err {
moq_net::Error::NotFound => MoqError::NotFound,
Expand Down
38 changes: 38 additions & 0 deletions rs/moq-ffi/src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ pub struct MoqVideoProperties {
pub flip: Option<bool>,
}

/// A media track's companion timestamp-to-group index.
#[derive(Clone, uniffi::Record)]
pub struct MoqTimeline {
pub track: String,
pub timescale: u32,
pub wall: Option<u64>,
Comment on lines +30 to +32

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

}

impl From<hang::catalog::Timeline> for MoqTimeline {
fn from(timeline: hang::catalog::Timeline) -> Self {
Self {
track: timeline.track,
timescale: timeline.timescale,
wall: timeline.wall,
}
}
}

impl From<MoqTimeline> for hang::catalog::Timeline {
fn from(timeline: MoqTimeline) -> Self {
let mut out = Self::new(timeline.track);
out.timescale = timeline.timescale;
out.wall = timeline.wall;
out
}
}

/// How a track's frames are packaged, as advertised in the catalog.
#[derive(Clone, uniffi::Enum)]
pub enum MoqContainer {
Expand Down Expand Up @@ -89,6 +116,7 @@ pub struct MoqVideo {
pub stalled: bool,
pub framerate: Option<f64>,
pub container: MoqContainer,
pub timeline: Option<MoqTimeline>,
}

#[derive(Clone, uniffi::Record)]
Expand All @@ -99,6 +127,14 @@ pub struct MoqAudio {
pub channel_count: u32,
pub bitrate: Option<u64>,
pub container: MoqContainer,
pub timeline: Option<MoqTimeline>,
}

/// One timestamp-to-group mapping read from a media timeline track.
#[derive(Clone, uniffi::Record)]
pub struct MoqTimelineEntry {
pub group: u64,
pub timestamp_us: u64,
}

/// A payload and the time it should be presented.
Expand Down Expand Up @@ -223,6 +259,7 @@ pub(crate) fn convert_catalog(catalog: &moq_mux::catalog::hang::Catalog<moq_mux:
stalled: config.stalled.unwrap_or(false),
framerate: config.framerate,
container: MoqContainer::from_catalog(&config.container)?,
timeline: config.timeline.clone().map(Into::into),
},
))
})
Expand All @@ -242,6 +279,7 @@ pub(crate) fn convert_catalog(catalog: &moq_mux::catalog::hang::Catalog<moq_mux:
channel_count: config.channel_count,
bitrate: config.bitrate,
container: MoqContainer::from_catalog(&config.container)?,
timeline: config.timeline.clone().map(Into::into),
},
))
})
Expand Down
33 changes: 33 additions & 0 deletions rs/moq-ffi/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,39 @@ async fn fetch_media_group_decodes_multiple_cmaf_samples() {
);
}

#[tokio::test]
async fn subscribes_to_timeline_group_mappings() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let mut timeline = moq_mux::timeline::Producer::new(&mut broadcast, "video").unwrap();
let section = timeline.section();
let track = broadcast.create_track("video", None).unwrap();
let consumer = MoqBroadcastConsumer::new(broadcast.consume());
let mut media = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy)
.with_recorder(timeline.recorder());

for timestamp_us in [5_000_000, 7_000_000] {
media
.write(moq_mux::container::Frame {
timestamp: moq_net::Timestamp::from_micros(timestamp_us).unwrap(),
payload: bytes::Bytes::from_static(b"video"),
keyframe: true,
duration: None,
})
.unwrap();
}
media.finish().unwrap();
timeline.finish().unwrap();

let timeline = consumer.subscribe_timeline(section.into()).await.unwrap();
let first = timeline.next().await.unwrap().expect("first timeline entry");
assert_eq!(first.group, 0);
assert_eq!(first.timestamp_us, 5_000_000);
let second = timeline.next().await.unwrap().expect("second timeline entry");
assert_eq!(second.group, 1);
assert_eq!(second.timestamp_us, 7_000_000);
assert!(timeline.next().await.unwrap().is_none());
}

#[tokio::test]
async fn dynamic_track_serves_fetch_miss_and_priority() {
let broadcast = MoqBroadcastProducer::new().unwrap();
Expand Down
4 changes: 4 additions & 0 deletions swift/Sources/Moq/Aliases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public typealias Route = MoqFFI.MoqRoute
public typealias Subscription = MoqFFI.MoqSubscription
/// Options for fetching one complete group by sequence.
public typealias FetchGroupOptions = MoqFFI.MoqFetchGroupOptions
/// A media track's companion timestamp-to-group index.
public typealias Timeline = MoqFFI.MoqTimeline
/// One timestamp-to-group mapping read from a media timeline track.
public typealias TimelineEntry = MoqFFI.MoqTimelineEntry
/// Publisher-side track properties: priority, group ordering, latency budget,
/// and timescale.
public typealias TrackInfo = MoqFFI.MoqTrackInfo
Expand Down
5 changes: 5 additions & 0 deletions swift/Sources/Moq/Broadcast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public final class BroadcastConsumer: Sendable {
)
}

/// Subscribe to a media track's timestamp-to-group timeline index.
public func subscribeTimeline(_ timeline: Timeline) async throws -> TimelineConsumer {
TimelineConsumer(try await ffi.subscribeTimeline(timeline: timeline))
}

/// Subscribe to a media track, delivering frames in decode order. `container`
/// comes from the catalog. `subscription` tunes delivery priority, group ordering
/// priority, group range, and the latency budget; omit for defaults. Raise
Expand Down
Loading
Loading