-
-
Notifications
You must be signed in to change notification settings - Fork 233
feat(bindings): expose media timelines #2764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -289,6 +289,7 @@ mod tests { | |
| channel_count: 2, | ||
| bitrate: None, | ||
| container: MoqContainer::Legacy, | ||
| timeline: None, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 As per coding guidelines, (Written by CodeRabbit) Also applies to: 338-340 🤖 Prompt for AI AgentsSource: 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. | ||
|
|
@@ -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. | ||
|
|
@@ -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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 As per coding guidelines, (Written by CodeRabbit) Also applies to: 116-116, 127-134 🤖 Prompt for AI AgentsSource: 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 { | ||
|
|
@@ -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)] | ||
|
|
@@ -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. | ||
|
|
@@ -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), | ||
| }, | ||
| )) | ||
| }) | ||
|
|
@@ -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), | ||
| }, | ||
| )) | ||
| }) | ||
|
|
||
There was a problem hiding this comment.
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:
Repository: moq-dev/moq
Length of output: 6063
🏁 Script executed:
Repository: moq-dev/moq
Length of output: 50367
🏁 Script executed:
Repository: moq-dev/moq
Length of output: 32804
🌐 Web query:
moq-dev/moq Kotlin fetchMediaGroup MoqFetchGroupOptions generated Kotlin signature💡 Result:
The project
moq-dev/moqprovides 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 aroundFetchcontrol messages [5][6]. Based on current project structures and available documentation,MoqFetchGroupOptionsis not a standard, high-level signature found in the publicdev.moq:moqKotlin library. Instead, the library manages fetches via native-to-Kotlin FFI bindings that mirror the underlyingmoq-transportmessage structure [2][1]. In the broadermoqecosystem (including implementations likemoqtailthat share design patterns withmoq-dev), fetching is typically handled by passing an object containing parameters such as priority, group order, and track location [5][7][8]. In themoq-dev/moqKotlin bindings, you should look to use the Kotlin idiomatic APIs (typically involving Coroutines and Flow) provided by thedev.moq:moqartifact [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 themoq-ffilayer [9]. To identify the exact generated signature for your version, you can inspect the generated Kotlin source files usually located inbuild/generated/source/uniffiafter 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
Noneas a default value forOption<T>parameters when using proc-macros, which translates tonullin the generated Kotlin code [1][2][3]. For function or method arguments, you can define the default in the#[uniffi::export]attribute using thedefaultargument [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 tonull: fun split(maxSplits: UInt? = null): List {... } When using UDL files, optional arguments can be defined similarly. Whilenullis the equivalent in Kotlin bindings, the UniFFI documentation specifically notes that when using proc-macros, you use theNoneliteral to signify the default value for anOption<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 tonullifNoneis provided as the default [4][3].Citations:
Pass the fetch options argument.
fetchMediaGrouprequires a fourth nullableFetchGroupOptionsparameter with no default. AddnullorFetchGroupOptions()so the example compiles.(Written by )
🤖 Prompt for AI Agents