From eac6e3cf2e3eaca24e6e4aa121f2e20449ff13e5 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Jul 2024 13:42:14 +0000 Subject: [PATCH 1/8] committosupport-commoncatalogformat-for-moqtransport --- moq-pub/src/main.rs | 8 ++++--- moq-pub/src/media.rs | 57 ++++++++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/moq-pub/src/main.rs b/moq-pub/src/main.rs index 6748a2d6..c3ebcae6 100644 --- a/moq-pub/src/main.rs +++ b/moq-pub/src/main.rs @@ -50,6 +50,7 @@ async fn main() -> anyhow::Result<()> { tracing::subscriber::set_global_default(tracer).unwrap(); let cli = Cli::parse(); + let mut nmspc = cli.name.clone(); let (writer, _, reader) = serve::Tracks::new(cli.name).produce(); let media = Media::new(writer)?; @@ -70,19 +71,20 @@ async fn main() -> anyhow::Result<()> { tokio::select! { res = session.run() => res.context("session error")?, - res = run_media(media) => res.context("media error")?, + res = run_media(media,&mut nmspc) => res.context("media error")?, res = publisher.announce(reader) => res.context("publisher error")?, } Ok(()) } -async fn run_media(mut media: Media) -> anyhow::Result<()> { +async fn run_media(mut media: Media,namespace: & String) -> anyhow::Result<()> { let mut input = tokio::io::stdin(); let mut buf = BytesMut::new(); + let mut name = namespace.clone(); loop { input.read_buf(&mut buf).await.context("failed to read from stdin")?; - media.parse(&mut buf).context("failed to parse media")?; + media.parse(&mut buf,&mut name).context("failed to parse media")?; } } diff --git a/moq-pub/src/media.rs b/moq-pub/src/media.rs index ba384d2f..f59b728d 100644 --- a/moq-pub/src/media.rs +++ b/moq-pub/src/media.rs @@ -45,12 +45,13 @@ impl Media { // Parse the input buffer, reading any full atoms we can find. // Keep appending more data and calling parse. - pub fn parse(&mut self, buf: &mut B) -> anyhow::Result<()> { - while self.parse_atom(buf)? {} + pub fn parse(&mut self, buf: &mut B,namespace: & String) -> anyhow::Result<()> { + let mut name = namespace.clone(); + while self.parse_atom(buf,&mut name)? {} Ok(()) } - fn parse_atom(&mut self, buf: &mut B) -> anyhow::Result { + fn parse_atom(&mut self, buf: &mut B,namespace: &mut String) -> anyhow::Result { let atom = match next_atom(buf)? { Some(atom) => atom, None => return Ok(false), @@ -76,7 +77,8 @@ impl Media { // Parse the moov box so we can detect the timescales for each track. let moov = mp4::MoovBox::read_box(&mut reader, header.size)?; - self.setup(&moov, atom)?; + let mut namespc = namespace.clone(); + self.setup(&moov, atom,&mut namespc)?; self.moov = Some(moov); } mp4::BoxType::MoofBox => { @@ -127,7 +129,7 @@ impl Media { Ok(true) } - fn setup(&mut self, moov: &mp4::MoovBox, raw: Bytes) -> anyhow::Result<()> { + fn setup(&mut self, moov: &mp4::MoovBox, raw: Bytes,namespace: &mut String) -> anyhow::Result<()> { // Create a track for each track in the moov for trak in &moov.traks { let id = trak.tkhd.track_id; @@ -154,11 +156,12 @@ impl Media { // Produce the catalog for trak in &moov.traks { let mut track = json!({ - "container": "mp4", "init_track": "0.mp4", "data_track": format!("{}.m4s", trak.tkhd.track_id), }); + let mut selection_params = json!({}); + let stsd = &trak.mdia.minf.stbl.stsd; if let Some(avc1) = &stsd.avc1 { // avc1[.PPCCLL] @@ -176,10 +179,11 @@ impl Media { let codec = rfc6381_codec::Codec::avc1(profile, constraints, level); let codec_str = codec.to_string(); - track["kind"] = json!("video"); - track["codec"] = json!(codec_str); - track["width"] = json!(width); - track["height"] = json!(height); + track["name"] = json!(format!("video_{}p", height)); + selection_params["codec"] = json!(codec_str); + selection_params["width"] = json!(width); + selection_params["height"] = json!(height); + track["selectionParams"] = json!(selection_params); } else if let Some(_hev1) = &stsd.hev1 { // TODO https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L106 anyhow::bail!("HEVC not yet supported") @@ -192,25 +196,30 @@ impl Media { .dec_config; let codec_str = format!("mp4a.{:02x}.{}", desc.object_type_indication, desc.dec_specific.profile); - track["kind"] = json!("audio"); - track["codec"] = json!(codec_str); - track["channel_count"] = json!(mp4a.channelcount); - track["sample_rate"] = json!(mp4a.samplerate.value()); - track["sample_size"] = json!(mp4a.samplesize); + track["name"] = json!("audio"); + selection_params["codec"] = json!(codec_str); + selection_params["channel_count"] = json!(mp4a.channelcount); + selection_params["sample_rate"] = json!(mp4a.samplerate.value()); + selection_params["sample_size"] = json!(mp4a.samplesize); + let bitrate = max(desc.max_bitrate, desc.avg_bitrate); if bitrate > 0 { - track["bit_rate"] = json!(bitrate); + selection_params["bit_rate"] = json!(bitrate); } + track["selectionParams"] = json!(selection_params); } else if let Some(vp09) = &stsd.vp09 { // https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L238 let vpcc = &vp09.vpcc; let codec_str = format!("vp09.0.{:02x}.{:02x}.{:02x}", vpcc.profile, vpcc.level, vpcc.bit_depth); - track["kind"] = json!("video"); - track["codec"] = json!(codec_str); - track["width"] = json!(vp09.width); // no idea if this needs to be multiplied - track["height"] = json!(vp09.height); // no idea if this needs to be multiplied + track["name"] = json!(format!("video_{}p", vp09.height)); + selection_params["codec"] = json!(codec_str); + selection_params["width"] = json!(vp09.width);// no idea if this needs to be multiplied + selection_params["height"] = json!(vp09.height); // no idea if this needs to be multiplied + track["selectionParams"] = json!(selection_params); + + // TODO Test if this actually works; I'm just guessing based on mp4box.js anyhow::bail!("VP9 not yet supported") @@ -222,7 +231,15 @@ impl Media { tracks.push(track); } + let nm = ["quic.video/watch/",namespace].join(""); let catalog = json!({ + "version": 1, + "sequence": 0, + "streamingFormat": 1, + "streamingFormatVersion": "0.2", + "namespace": nm, + "packaging": "cmaf", + "renderGroup":1, "tracks": tracks }); From a5813197e0d26cf46fba198d84098f1c416de699 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 11 Jul 2024 14:19:11 +0000 Subject: [PATCH 2/8] committosupport-commoncatalogformat-addlukereviewcomments --- Cargo.lock | 21 ++-- moq-pub/Cargo.toml | 1 + moq-pub/src/media.rs | 246 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 222 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 058503b4..d22abd6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -927,6 +927,7 @@ dependencies = [ "moq-transport", "mp4", "rfc6381-codec", + "serde", "serde_json", "tokio", "tracing", @@ -1260,9 +1261,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1317,9 +1318,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1639,18 +1640,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -1769,9 +1770,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.32" +version = "2.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" dependencies = [ "proc-macro2", "quote", diff --git a/moq-pub/Cargo.toml b/moq-pub/Cargo.toml index 0fc0cd22..2a18850f 100644 --- a/moq-pub/Cargo.toml +++ b/moq-pub/Cargo.toml @@ -33,3 +33,4 @@ serde_json = "1" rfc6381-codec = "0.1" tracing = "0.1" tracing-subscriber = "0.3" +serde = "1.0.204" diff --git a/moq-pub/src/media.rs b/moq-pub/src/media.rs index f59b728d..44847aa2 100644 --- a/moq-pub/src/media.rs +++ b/moq-pub/src/media.rs @@ -2,12 +2,172 @@ use anyhow::{self, Context}; use bytes::{Buf, Bytes}; use moq_transport::serve::{GroupWriter, GroupsWriter, TrackWriter, TracksWriter}; use mp4::{self, ReadBox, TrackType}; -use serde_json::json; +use serde::{Deserialize, Serialize}; use std::cmp::max; use std::collections::HashMap; use std::io::Cursor; use std::time; +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct CommonTrackFields { + pub namespace: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub packaging: Option, + #[serde(rename = "renderGroup",skip_serializing_if = "Option::is_none")] + pub render_group: Option, + #[serde(rename = "altGroup",skip_serializing_if = "Option::is_none")] + pub alt_group: Option, + +} + +impl CommonTrackFields { + /// Serialize function to conditionally include fields based on their commonality amoung tracks + pub fn serialize_with_common_fields( + &self, + tracks: &mut [Tracks], + ) -> Option { + // Checking if packaging,render_group and alt_group are common across tracks + let mut common_packaging = self.packaging.clone(); + let mut common_render_group = self.render_group; + let mut common_alt_group = self.alt_group; + + for track in &mut *tracks { + if let Some(ref track_packaging) = track.packaging { + if common_packaging.is_none() { + common_packaging = Some(track_packaging.clone()); + } else if common_packaging != Some(track_packaging.clone()) { + common_packaging = None; + } + } + + if let Some(track_render_group) = track.render_group { + if common_render_group.is_none() { + common_render_group = Some(track_render_group); + } else if common_render_group != Some(track_render_group) { + common_render_group = None; + } + } + + if let Some(track_alt_group) = track.alt_group { + if common_alt_group.is_none() { + common_alt_group = Some(track_alt_group); + } else if common_alt_group != Some(track_alt_group) { + common_alt_group = None; + } + } + + } + + if common_render_group.is_some() { + for track in tracks.iter_mut() { + track.render_group = None; + } + } + if common_packaging.is_some() { + for track in tracks.iter_mut() { + track.packaging = None; + } + } + if common_alt_group.is_some() { + for track in tracks.iter_mut() { + track.alt_group = None; + } + } + + + // Serialize only if all tracks have the same values for packaging, render_group and alt_group + if common_packaging.is_some() || common_render_group.is_some() { + Some(CommonTrackFields { + namespace: self.namespace.clone(), + packaging: common_packaging, + render_group: common_render_group, + alt_group: common_alt_group, + }) + } else { + None + } + } +} + + +#[derive(Serialize, Deserialize, Debug)] +pub struct Catalog { + pub version: u16, + #[serde(rename = "streamingFormat")] + pub streaming_format: u16, + #[serde(rename = "streamingFormatVersion")] + pub streaming_format_version: String, + #[serde(rename = "supportsDeltaUpdates")] + pub streaming_delta_updates: bool, + #[serde(rename = "commonTrackFields")] + pub common_track_fields: Option, + pub tracks: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Tracks { + pub name: String, + #[serde(rename = "initTrack")] + pub init_track: Option, + #[serde(rename = "initData")] + pub data_track: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub packaging: Option, + #[serde(rename = "renderGroup",skip_serializing_if = "Option::is_none")] + pub render_group: Option, + #[serde(rename = "altGroup",skip_serializing_if = "Option::is_none")] + pub alt_group: Option, + #[serde(rename = "selectionParams")] + pub selection_params: SelectionParam, + #[serde(rename = "temporalId",skip_serializing_if = "Option::is_none")] + pub temporal_id: Option, + #[serde(rename = "spatialId",skip_serializing_if = "Option::is_none")] + pub spatial_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub depends: Option>, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub enum TrackPackaging { + #[serde(rename = "cmaf")] + Cmaf, + #[serde(rename = "loc")] + Loc, +} + +impl Default for TrackPackaging { + fn default() -> Self { + TrackPackaging::Cmaf + } +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct SelectionParam { + pub codec: Option, + #[serde(rename = "mimeType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub framerate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bitrate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub samplerate: Option, + #[serde(rename = "channelConfig",skip_serializing_if = "Option::is_none")] + pub channel_config: Option, + #[serde(rename = "displayWidth",skip_serializing_if = "Option::is_none")] + pub display_width: Option, + #[serde(rename = "displayHeight",skip_serializing_if = "Option::is_none")] + pub display_height: Option, + #[serde(rename = "lang",skip_serializing_if = "Option::is_none")] + pub language: Option, +} + + pub struct Media { // Tracks based on their track ID. tracks: HashMap, @@ -155,14 +315,19 @@ impl Media { // Produce the catalog for trak in &moov.traks { - let mut track = json!({ - "init_track": "0.mp4", - "data_track": format!("{}.m4s", trak.tkhd.track_id), - }); - let mut selection_params = json!({}); + let mut selection_params = SelectionParam::default(); + let mut track = Tracks::default(); let stsd = &trak.mdia.minf.stbl.stsd; + + + if trak.mdia.hdlr.handler_type.to_string() == "vide" || trak.mdia.hdlr.handler_type.to_string() == "soun" { + track.packaging = Some(TrackPackaging::Cmaf); + }else{ + track.packaging = Some(TrackPackaging::Loc); + } + if let Some(avc1) = &stsd.avc1 { // avc1[.PPCCLL] // @@ -179,11 +344,14 @@ impl Media { let codec = rfc6381_codec::Codec::avc1(profile, constraints, level); let codec_str = codec.to_string(); - track["name"] = json!(format!("video_{}p", height)); - selection_params["codec"] = json!(codec_str); - selection_params["width"] = json!(width); - selection_params["height"] = json!(height); - track["selectionParams"] = json!(selection_params); + track.name = format!("video_{}p", height); + selection_params.codec = Some(codec_str); + selection_params.width = Some(width.into()); + selection_params.height = Some(height.into()); + track.render_group = Some(1); + track.alt_group = Some(1); + track.selection_params = selection_params + } else if let Some(_hev1) = &stsd.hev1 { // TODO https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L106 anyhow::bail!("HEVC not yet supported") @@ -196,29 +364,30 @@ impl Media { .dec_config; let codec_str = format!("mp4a.{:02x}.{}", desc.object_type_indication, desc.dec_specific.profile); - track["name"] = json!("audio"); - selection_params["codec"] = json!(codec_str); - selection_params["channel_count"] = json!(mp4a.channelcount); - selection_params["sample_rate"] = json!(mp4a.samplerate.value()); - selection_params["sample_size"] = json!(mp4a.samplesize); - + track.name = "audio".to_string(); + selection_params.codec = Some(codec_str); + selection_params.channel_config = Some(mp4a.channelcount); + selection_params.samplerate = Some(mp4a.samplerate.value().into()); let bitrate = max(desc.max_bitrate, desc.avg_bitrate); if bitrate > 0 { - selection_params["bit_rate"] = json!(bitrate); + selection_params.bitrate = Some(bitrate); } - track["selectionParams"] = json!(selection_params); + track.render_group = Some(1); + track.selection_params = selection_params + } else if let Some(vp09) = &stsd.vp09 { // https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L238 let vpcc = &vp09.vpcc; let codec_str = format!("vp09.0.{:02x}.{:02x}.{:02x}", vpcc.profile, vpcc.level, vpcc.bit_depth); - track["name"] = json!(format!("video_{}p", vp09.height)); - selection_params["codec"] = json!(codec_str); - selection_params["width"] = json!(vp09.width);// no idea if this needs to be multiplied - selection_params["height"] = json!(vp09.height); // no idea if this needs to be multiplied - track["selectionParams"] = json!(selection_params); - + track.name = format!("video_{}p", vp09.height); + selection_params.codec = Some(codec_str); + selection_params.width = Some(vp09.width.into()); + selection_params.height = Some(vp09.height.into()); + track.render_group = Some(1); + track.alt_group = Some(1); + track.selection_params = selection_params; // TODO Test if this actually works; I'm just guessing based on mp4box.js @@ -228,22 +397,27 @@ impl Media { anyhow::bail!("unknown codec for track: {}", trak.tkhd.track_id); } + track.init_track = Some("0.mp4".to_string()); + track.data_track = Some(format!("{}.m4s", trak.tkhd.track_id)); + tracks.push(track); + } - let nm = ["quic.video/watch/",namespace].join(""); - let catalog = json!({ - "version": 1, - "sequence": 0, - "streamingFormat": 1, - "streamingFormatVersion": "0.2", - "namespace": nm, - "packaging": "cmaf", - "renderGroup":1, - "tracks": tracks - }); + let mut commontrackfields = CommonTrackFields::default(); + commontrackfields.namespace = Some(["quic.video/watch/",namespace].join("")); + + let catalog = Catalog { + version: 1, + streaming_format: 1, + streaming_format_version: "0.2".to_string(), + streaming_delta_updates: true, + common_track_fields: commontrackfields.serialize_with_common_fields(&mut tracks), + tracks, + }; let catalog_str = serde_json::to_string_pretty(&catalog)?; + log::info!("catalog: {}", catalog_str); // Create a single fragment for the segment. From 935688f8ed54f6910d447570bbb9322c426f0579 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 15 Jul 2024 06:52:27 +0000 Subject: [PATCH 3/8] committosupport-commoncatalogformat-cargoclippyandfmtcheckfix --- moq-pub/src/main.rs | 10 +- moq-pub/src/media.rs | 295 ++++++++++++++++++++----------------------- 2 files changed, 144 insertions(+), 161 deletions(-) diff --git a/moq-pub/src/main.rs b/moq-pub/src/main.rs index c3ebcae6..607fd1d0 100644 --- a/moq-pub/src/main.rs +++ b/moq-pub/src/main.rs @@ -50,7 +50,7 @@ async fn main() -> anyhow::Result<()> { tracing::subscriber::set_global_default(tracer).unwrap(); let cli = Cli::parse(); - let mut nmspc = cli.name.clone(); + let nmspc = cli.name.clone(); let (writer, _, reader) = serve::Tracks::new(cli.name).produce(); let media = Media::new(writer)?; @@ -71,20 +71,20 @@ async fn main() -> anyhow::Result<()> { tokio::select! { res = session.run() => res.context("session error")?, - res = run_media(media,&mut nmspc) => res.context("media error")?, + res = run_media(media,&nmspc) => res.context("media error")?, res = publisher.announce(reader) => res.context("publisher error")?, } Ok(()) } -async fn run_media(mut media: Media,namespace: & String) -> anyhow::Result<()> { +async fn run_media(mut media: Media, namespace: &str) -> anyhow::Result<()> { let mut input = tokio::io::stdin(); let mut buf = BytesMut::new(); - let mut name = namespace.clone(); + let name = namespace.to_owned(); loop { input.read_buf(&mut buf).await.context("failed to read from stdin")?; - media.parse(&mut buf,&mut name).context("failed to parse media")?; + media.parse(&mut buf, &name).context("failed to parse media")?; } } diff --git a/moq-pub/src/media.rs b/moq-pub/src/media.rs index 44847aa2..d590a1e7 100644 --- a/moq-pub/src/media.rs +++ b/moq-pub/src/media.rs @@ -10,164 +10,151 @@ use std::time; #[derive(Serialize, Deserialize, Debug, Default)] pub struct CommonTrackFields { - pub namespace: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub packaging: Option, - #[serde(rename = "renderGroup",skip_serializing_if = "Option::is_none")] - pub render_group: Option, - #[serde(rename = "altGroup",skip_serializing_if = "Option::is_none")] - pub alt_group: Option, - + pub namespace: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub packaging: Option, + #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")] + pub render_group: Option, + #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")] + pub alt_group: Option, } impl CommonTrackFields { - /// Serialize function to conditionally include fields based on their commonality amoung tracks - pub fn serialize_with_common_fields( - &self, - tracks: &mut [Tracks], - ) -> Option { - // Checking if packaging,render_group and alt_group are common across tracks - let mut common_packaging = self.packaging.clone(); - let mut common_render_group = self.render_group; - let mut common_alt_group = self.alt_group; - - for track in &mut *tracks { - if let Some(ref track_packaging) = track.packaging { - if common_packaging.is_none() { - common_packaging = Some(track_packaging.clone()); - } else if common_packaging != Some(track_packaging.clone()) { - common_packaging = None; - } - } - - if let Some(track_render_group) = track.render_group { - if common_render_group.is_none() { - common_render_group = Some(track_render_group); - } else if common_render_group != Some(track_render_group) { - common_render_group = None; - } - } - - if let Some(track_alt_group) = track.alt_group { - if common_alt_group.is_none() { - common_alt_group = Some(track_alt_group); - } else if common_alt_group != Some(track_alt_group) { - common_alt_group = None; - } - } - - } - - if common_render_group.is_some() { - for track in tracks.iter_mut() { - track.render_group = None; - } - } - if common_packaging.is_some() { - for track in tracks.iter_mut() { - track.packaging = None; - } + /// Serialize function to conditionally include fields based on their commonality amoung tracks + pub fn serialize_with_common_fields(&self, tracks: &mut [Tracks]) -> Option { + // Checking if packaging,render_group and alt_group are common across tracks + let mut common_packaging = self.packaging.clone(); + let mut common_render_group = self.render_group; + let mut common_alt_group = self.alt_group; + + for track in &mut *tracks { + if let Some(ref track_packaging) = track.packaging { + if common_packaging.is_none() { + common_packaging = Some(track_packaging.clone()); + } else if common_packaging != Some(track_packaging.clone()) { + common_packaging = None; + } + } + + if let Some(track_render_group) = track.render_group { + if common_render_group.is_none() { + common_render_group = Some(track_render_group); + } else if common_render_group != Some(track_render_group) { + common_render_group = None; + } + } + + if let Some(track_alt_group) = track.alt_group { + if common_alt_group.is_none() { + common_alt_group = Some(track_alt_group); + } else if common_alt_group != Some(track_alt_group) { + common_alt_group = None; + } + } + } + + if common_render_group.is_some() { + for track in tracks.iter_mut() { + track.render_group = None; + } + } + if common_packaging.is_some() { + for track in tracks.iter_mut() { + track.packaging = None; + } + } + if common_alt_group.is_some() { + for track in tracks.iter_mut() { + track.alt_group = None; + } + } + + // Serialize only if all tracks have the same values for packaging, render_group and alt_group + if common_packaging.is_some() || common_render_group.is_some() { + Some(CommonTrackFields { + namespace: self.namespace.clone(), + packaging: common_packaging, + render_group: common_render_group, + alt_group: common_alt_group, + }) + } else { + None + } } - if common_alt_group.is_some() { - for track in tracks.iter_mut() { - track.alt_group = None; - } - } - - - // Serialize only if all tracks have the same values for packaging, render_group and alt_group - if common_packaging.is_some() || common_render_group.is_some() { - Some(CommonTrackFields { - namespace: self.namespace.clone(), - packaging: common_packaging, - render_group: common_render_group, - alt_group: common_alt_group, - }) - } else { - None - } - } } - #[derive(Serialize, Deserialize, Debug)] pub struct Catalog { - pub version: u16, - #[serde(rename = "streamingFormat")] - pub streaming_format: u16, - #[serde(rename = "streamingFormatVersion")] - pub streaming_format_version: String, - #[serde(rename = "supportsDeltaUpdates")] - pub streaming_delta_updates: bool, - #[serde(rename = "commonTrackFields")] - pub common_track_fields: Option, - pub tracks: Vec, + pub version: u16, + #[serde(rename = "streamingFormat")] + pub streaming_format: u16, + #[serde(rename = "streamingFormatVersion")] + pub streaming_format_version: String, + #[serde(rename = "supportsDeltaUpdates")] + pub streaming_delta_updates: bool, + #[serde(rename = "commonTrackFields")] + pub common_track_fields: Option, + pub tracks: Vec, } #[derive(Serialize, Deserialize, Debug, Default)] pub struct Tracks { - pub name: String, - #[serde(rename = "initTrack")] - pub init_track: Option, - #[serde(rename = "initData")] - pub data_track: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub packaging: Option, - #[serde(rename = "renderGroup",skip_serializing_if = "Option::is_none")] - pub render_group: Option, - #[serde(rename = "altGroup",skip_serializing_if = "Option::is_none")] - pub alt_group: Option, - #[serde(rename = "selectionParams")] - pub selection_params: SelectionParam, - #[serde(rename = "temporalId",skip_serializing_if = "Option::is_none")] - pub temporal_id: Option, - #[serde(rename = "spatialId",skip_serializing_if = "Option::is_none")] - pub spatial_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub depends: Option>, + pub name: String, + #[serde(rename = "initTrack")] + pub init_track: Option, + #[serde(rename = "initData")] + pub data_track: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub packaging: Option, + #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")] + pub render_group: Option, + #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")] + pub alt_group: Option, + #[serde(rename = "selectionParams")] + pub selection_params: SelectionParam, + #[serde(rename = "temporalId", skip_serializing_if = "Option::is_none")] + pub temporal_id: Option, + #[serde(rename = "spatialId", skip_serializing_if = "Option::is_none")] + pub spatial_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub depends: Option>, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] pub enum TrackPackaging { - #[serde(rename = "cmaf")] - Cmaf, - #[serde(rename = "loc")] - Loc, -} - -impl Default for TrackPackaging { - fn default() -> Self { - TrackPackaging::Cmaf - } + #[serde(rename = "cmaf")] + #[default] + Cmaf, + #[serde(rename = "loc")] + Loc, } #[derive(Serialize, Deserialize, Debug, Default)] pub struct SelectionParam { - pub codec: Option, - #[serde(rename = "mimeType")] - #[serde(skip_serializing_if = "Option::is_none")] - pub mime_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub framerate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub bitrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub width: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub height: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub samplerate: Option, - #[serde(rename = "channelConfig",skip_serializing_if = "Option::is_none")] - pub channel_config: Option, - #[serde(rename = "displayWidth",skip_serializing_if = "Option::is_none")] - pub display_width: Option, - #[serde(rename = "displayHeight",skip_serializing_if = "Option::is_none")] - pub display_height: Option, - #[serde(rename = "lang",skip_serializing_if = "Option::is_none")] - pub language: Option, + pub codec: Option, + #[serde(rename = "mimeType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub framerate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bitrate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub samplerate: Option, + #[serde(rename = "channelConfig", skip_serializing_if = "Option::is_none")] + pub channel_config: Option, + #[serde(rename = "displayWidth", skip_serializing_if = "Option::is_none")] + pub display_width: Option, + #[serde(rename = "displayHeight", skip_serializing_if = "Option::is_none")] + pub display_height: Option, + #[serde(rename = "lang", skip_serializing_if = "Option::is_none")] + pub language: Option, } - pub struct Media { // Tracks based on their track ID. tracks: HashMap, @@ -205,13 +192,13 @@ impl Media { // Parse the input buffer, reading any full atoms we can find. // Keep appending more data and calling parse. - pub fn parse(&mut self, buf: &mut B,namespace: & String) -> anyhow::Result<()> { - let mut name = namespace.clone(); - while self.parse_atom(buf,&mut name)? {} + pub fn parse(&mut self, buf: &mut B, namespace: &str) -> anyhow::Result<()> { + let name = namespace.to_owned(); + while self.parse_atom(buf, &name)? {} Ok(()) } - fn parse_atom(&mut self, buf: &mut B,namespace: &mut String) -> anyhow::Result { + fn parse_atom(&mut self, buf: &mut B, namespace: &str) -> anyhow::Result { let atom = match next_atom(buf)? { Some(atom) => atom, None => return Ok(false), @@ -237,8 +224,8 @@ impl Media { // Parse the moov box so we can detect the timescales for each track. let moov = mp4::MoovBox::read_box(&mut reader, header.size)?; - let mut namespc = namespace.clone(); - self.setup(&moov, atom,&mut namespc)?; + let namespc = namespace.to_owned(); + self.setup(&moov, atom, &namespc)?; self.moov = Some(moov); } mp4::BoxType::MoofBox => { @@ -289,7 +276,7 @@ impl Media { Ok(true) } - fn setup(&mut self, moov: &mp4::MoovBox, raw: Bytes,namespace: &mut String) -> anyhow::Result<()> { + fn setup(&mut self, moov: &mp4::MoovBox, raw: Bytes, namespace: &str) -> anyhow::Result<()> { // Create a track for each track in the moov for trak in &moov.traks { let id = trak.tkhd.track_id; @@ -315,16 +302,14 @@ impl Media { // Produce the catalog for trak in &moov.traks { - let mut selection_params = SelectionParam::default(); let mut track = Tracks::default(); let stsd = &trak.mdia.minf.stbl.stsd; - if trak.mdia.hdlr.handler_type.to_string() == "vide" || trak.mdia.hdlr.handler_type.to_string() == "soun" { track.packaging = Some(TrackPackaging::Cmaf); - }else{ + } else { track.packaging = Some(TrackPackaging::Loc); } @@ -350,8 +335,7 @@ impl Media { selection_params.height = Some(height.into()); track.render_group = Some(1); track.alt_group = Some(1); - track.selection_params = selection_params - + track.selection_params = selection_params } else if let Some(_hev1) = &stsd.hev1 { // TODO https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L106 anyhow::bail!("HEVC not yet supported") @@ -374,8 +358,7 @@ impl Media { selection_params.bitrate = Some(bitrate); } track.render_group = Some(1); - track.selection_params = selection_params - + track.selection_params = selection_params } else if let Some(vp09) = &stsd.vp09 { // https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L238 let vpcc = &vp09.vpcc; @@ -389,7 +372,6 @@ impl Media { track.alt_group = Some(1); track.selection_params = selection_params; - // TODO Test if this actually works; I'm just guessing based on mp4box.js anyhow::bail!("VP9 not yet supported") } else { @@ -401,11 +383,12 @@ impl Media { track.data_track = Some(format!("{}.m4s", trak.tkhd.track_id)); tracks.push(track); - } - let mut commontrackfields = CommonTrackFields::default(); - commontrackfields.namespace = Some(["quic.video/watch/",namespace].join("")); + let commontrackfields = CommonTrackFields { + namespace: Some(["quic.video/watch/", namespace].join("")), + ..Default::default() + }; let catalog = Catalog { version: 1, @@ -417,7 +400,7 @@ impl Media { }; let catalog_str = serde_json::to_string_pretty(&catalog)?; - + log::info!("catalog: {}", catalog_str); // Create a single fragment for the segment. From 8e825dd074e944979f682f17c30139bb54c9cca0 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 15 Jul 2024 07:12:40 +0000 Subject: [PATCH 4/8] committosupport-commoncatalogformat-cargoclippyandfmtcheckfix2 --- moq-pub/src/main.rs | 3 +-- moq-pub/src/media.rs | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/moq-pub/src/main.rs b/moq-pub/src/main.rs index 607fd1d0..a6687ebd 100644 --- a/moq-pub/src/main.rs +++ b/moq-pub/src/main.rs @@ -81,10 +81,9 @@ async fn main() -> anyhow::Result<()> { async fn run_media(mut media: Media, namespace: &str) -> anyhow::Result<()> { let mut input = tokio::io::stdin(); let mut buf = BytesMut::new(); - let name = namespace.to_owned(); loop { input.read_buf(&mut buf).await.context("failed to read from stdin")?; - media.parse(&mut buf, &name).context("failed to parse media")?; + media.parse(&mut buf, namespace).context("failed to parse media")?; } } diff --git a/moq-pub/src/media.rs b/moq-pub/src/media.rs index d590a1e7..bd1dd67c 100644 --- a/moq-pub/src/media.rs +++ b/moq-pub/src/media.rs @@ -193,8 +193,7 @@ impl Media { // Parse the input buffer, reading any full atoms we can find. // Keep appending more data and calling parse. pub fn parse(&mut self, buf: &mut B, namespace: &str) -> anyhow::Result<()> { - let name = namespace.to_owned(); - while self.parse_atom(buf, &name)? {} + while self.parse_atom(buf, namespace)? {} Ok(()) } @@ -224,8 +223,7 @@ impl Media { // Parse the moov box so we can detect the timescales for each track. let moov = mp4::MoovBox::read_box(&mut reader, header.size)?; - let namespc = namespace.to_owned(); - self.setup(&moov, atom, &namespc)?; + self.setup(&moov, atom, namespace)?; self.moov = Some(moov); } mp4::BoxType::MoofBox => { From d24b22fbe884161a1c86660afafc8d670a205fc5 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 15 Jul 2024 07:48:11 +0000 Subject: [PATCH 5/8] committosupport-commoncatalogformat-obikenobi-semvercheck-warningfix --- moq-pub/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moq-pub/Cargo.toml b/moq-pub/Cargo.toml index 2a18850f..defc6721 100644 --- a/moq-pub/Cargo.toml +++ b/moq-pub/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Mike English", "Luke Curley"] repository = "https://github.com/kixelated/moq-rs" license = "MIT OR Apache-2.0" -version = "0.5.1" +version = "1.0.0" edition = "2021" keywords = ["quic", "http3", "webtransport", "media", "live"] From 746171bcfffc656973e6fb7638962452f95b5c91 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 19 Jul 2024 16:29:42 +0000 Subject: [PATCH 6/8] committosupport-commoncatalogformat-moqjscommit7baad29-refacfix-channelconfigtostring --- moq-pub/src/media.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/moq-pub/src/media.rs b/moq-pub/src/media.rs index bd1dd67c..e2212383 100644 --- a/moq-pub/src/media.rs +++ b/moq-pub/src/media.rs @@ -10,6 +10,7 @@ use std::time; #[derive(Serialize, Deserialize, Debug, Default)] pub struct CommonTrackFields { + #[serde(skip_serializing_if = "Option::is_none")] pub namespace: Option, #[serde(skip_serializing_if = "Option::is_none")] pub packaging: Option, @@ -99,6 +100,8 @@ pub struct Catalog { #[derive(Serialize, Deserialize, Debug, Default)] pub struct Tracks { + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace: Option, pub name: String, #[serde(rename = "initTrack")] pub init_track: Option, @@ -146,7 +149,7 @@ pub struct SelectionParam { #[serde(skip_serializing_if = "Option::is_none")] pub samplerate: Option, #[serde(rename = "channelConfig", skip_serializing_if = "Option::is_none")] - pub channel_config: Option, + pub channel_config: Option, #[serde(rename = "displayWidth", skip_serializing_if = "Option::is_none")] pub display_width: Option, #[serde(rename = "displayHeight", skip_serializing_if = "Option::is_none")] @@ -327,6 +330,7 @@ impl Media { let codec = rfc6381_codec::Codec::avc1(profile, constraints, level); let codec_str = codec.to_string(); + track.namespace = Some(namespace.to_string()); track.name = format!("video_{}p", height); selection_params.codec = Some(codec_str); selection_params.width = Some(width.into()); @@ -346,9 +350,10 @@ impl Media { .dec_config; let codec_str = format!("mp4a.{:02x}.{}", desc.object_type_indication, desc.dec_specific.profile); + track.namespace = Some(namespace.to_string()); track.name = "audio".to_string(); selection_params.codec = Some(codec_str); - selection_params.channel_config = Some(mp4a.channelcount); + selection_params.channel_config = Some(mp4a.channelcount.to_string()); selection_params.samplerate = Some(mp4a.samplerate.value().into()); let bitrate = max(desc.max_bitrate, desc.avg_bitrate); @@ -362,6 +367,7 @@ impl Media { let vpcc = &vp09.vpcc; let codec_str = format!("vp09.0.{:02x}.{:02x}.{:02x}", vpcc.profile, vpcc.level, vpcc.bit_depth); + track.namespace = Some(namespace.to_string()); track.name = format!("video_{}p", vp09.height); selection_params.codec = Some(codec_str); selection_params.width = Some(vp09.width.into()); From 73bad569a6711e507800129a9a8ea4654fbec80c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 21 Jul 2024 09:25:09 -0700 Subject: [PATCH 7/8] Inline PR changes. Basically just moved the catalog stuff to a separate crate. moq-sub will use it eventually. --- Cargo.lock | 11 ++- Cargo.toml | 1 + moq-catalog/Cargo.toml | 17 ++++ moq-catalog/src/lib.rs | 190 ++++++++++++++++++++++++++++++++++++++ moq-pub/Cargo.toml | 4 +- moq-pub/src/main.rs | 7 +- moq-pub/src/media.rs | 202 ++++------------------------------------- 7 files changed, 240 insertions(+), 192 deletions(-) create mode 100644 moq-catalog/Cargo.toml create mode 100644 moq-catalog/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d22abd6a..4f3381f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,6 +856,13 @@ dependencies = [ "url", ] +[[package]] +name = "moq-catalog" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "moq-clock" version = "0.4.1" @@ -916,18 +923,18 @@ dependencies = [ [[package]] name = "moq-pub" -version = "0.5.1" +version = "0.6.0" dependencies = [ "anyhow", "bytes", "clap", "env_logger", "log", + "moq-catalog", "moq-native", "moq-transport", "mp4", "rfc6381-codec", - "serde", "serde_json", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index d3526cc5..0c5fce8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "moq-clock", "moq-dir", "moq-native", + "moq-catalog", ] resolver = "2" diff --git a/moq-catalog/Cargo.toml b/moq-catalog/Cargo.toml new file mode 100644 index 00000000..88dfe9c0 --- /dev/null +++ b/moq-catalog/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "moq-catalog" +description = "Media over QUIC" +authors = ["Luke Curley"] +repository = "https://github.com/kixelated/moq-rs" +license = "MIT OR Apache-2.0" + +version = "0.1.0" +edition = "2021" + +keywords = ["quic", "http3", "webtransport", "media", "live"] +categories = ["multimedia", "network-programming", "web-programming"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = "1" diff --git a/moq-catalog/src/lib.rs b/moq-catalog/src/lib.rs new file mode 100644 index 00000000..c234adc0 --- /dev/null +++ b/moq-catalog/src/lib.rs @@ -0,0 +1,190 @@ +//! This module contains the structs and functions for the MoQ catalog format +/// The catalog format is a JSON file that describes the tracks available in a broadcast. +/// +/// The current version of the catalog format is draft-01. +/// https://www.ietf.org/archive/id/draft-ietf-moq-catalogformat-01.html +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Root { + pub version: u16, + + #[serde(rename = "streamingFormat")] + pub streaming_format: u16, + + #[serde(rename = "streamingFormatVersion")] + pub streaming_format_version: String, + + #[serde(rename = "supportsDeltaUpdates")] + pub streaming_delta_updates: bool, + + #[serde(rename = "commonTrackFields")] + pub common_track_fields: CommonTrackFields, + + pub tracks: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Track { + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + pub name: String, + + #[serde(rename = "initTrack")] + pub init_track: Option, + + #[serde(rename = "initData")] + pub data_track: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub packaging: Option, + + #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")] + pub render_group: Option, + + #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")] + pub alt_group: Option, + + #[serde(rename = "selectionParams")] + pub selection_params: SelectionParam, + + #[serde(rename = "temporalId", skip_serializing_if = "Option::is_none")] + pub temporal_id: Option, + + #[serde(rename = "spatialId", skip_serializing_if = "Option::is_none")] + pub spatial_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub depends: Option>, +} + +impl Track { + #[allow(dead_code)] // TODO use + fn with_common(&mut self, common: &CommonTrackFields) { + if self.namespace.is_none() { + self.namespace.clone_from(&common.namespace); + } + if self.packaging.is_none() { + self.packaging.clone_from(&common.packaging); + } + if self.render_group.is_none() { + self.render_group = common.render_group; + } + if self.alt_group.is_none() { + self.alt_group = common.alt_group; + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] +pub enum TrackPackaging { + #[serde(rename = "cmaf")] + #[default] + Cmaf, + + #[serde(rename = "loc")] + Loc, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct SelectionParam { + pub codec: Option, + + #[serde(rename = "mimeType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub framerate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub bitrate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub samplerate: Option, + + #[serde(rename = "channelConfig", skip_serializing_if = "Option::is_none")] + pub channel_config: Option, + + #[serde(rename = "displayWidth", skip_serializing_if = "Option::is_none")] + pub display_width: Option, + + #[serde(rename = "displayHeight", skip_serializing_if = "Option::is_none")] + pub display_height: Option, + + #[serde(rename = "lang", skip_serializing_if = "Option::is_none")] + pub language: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct CommonTrackFields { + #[serde(skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub packaging: Option, + + #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")] + pub render_group: Option, + + #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")] + pub alt_group: Option, +} + +impl CommonTrackFields { + /// Serialize function to conditionally include fields based on their commonality amoung tracks + pub fn from_tracks(tracks: &mut [Track]) -> Self { + if tracks.is_empty() { + return Default::default(); + } + + // Use the first track as the basis + let mut common = Self { + namespace: tracks[0].namespace.clone(), + packaging: tracks[0].packaging.clone(), + render_group: tracks[0].render_group, + alt_group: tracks[0].alt_group, + }; + + // Loop over the other tracks to check if they have the same values + for track in &mut tracks[1..] { + if track.namespace != common.namespace { + common.namespace = None; + } + if track.packaging != common.packaging { + common.packaging = None; + } + if track.render_group != common.render_group { + common.render_group = None + } + if track.alt_group != common.alt_group { + common.alt_group = None; + } + } + + // Loop again to remove the common fields from the tracks + for track in tracks { + if common.namespace.is_some() { + track.namespace = None; + } + if track.packaging.is_some() { + track.packaging = None; + } + if track.render_group.is_some() { + track.render_group = None; + } + if track.alt_group.is_some() { + track.alt_group = None; + } + } + + common + } +} diff --git a/moq-pub/Cargo.toml b/moq-pub/Cargo.toml index defc6721..06063b06 100644 --- a/moq-pub/Cargo.toml +++ b/moq-pub/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Mike English", "Luke Curley"] repository = "https://github.com/kixelated/moq-rs" license = "MIT OR Apache-2.0" -version = "1.0.0" +version = "0.6.0" edition = "2021" keywords = ["quic", "http3", "webtransport", "media", "live"] @@ -16,6 +16,7 @@ categories = ["multimedia", "network-programming", "web-programming"] [dependencies] moq-native = { path = "../moq-native", version = "0.2" } moq-transport = { path = "../moq-transport", version = "0.5" } +moq-catalog = { path = "../moq-catalog", version = "0.1" } url = "2" bytes = "1" @@ -33,4 +34,3 @@ serde_json = "1" rfc6381-codec = "0.1" tracing = "0.1" tracing-subscriber = "0.3" -serde = "1.0.204" diff --git a/moq-pub/src/main.rs b/moq-pub/src/main.rs index a6687ebd..6748a2d6 100644 --- a/moq-pub/src/main.rs +++ b/moq-pub/src/main.rs @@ -50,7 +50,6 @@ async fn main() -> anyhow::Result<()> { tracing::subscriber::set_global_default(tracer).unwrap(); let cli = Cli::parse(); - let nmspc = cli.name.clone(); let (writer, _, reader) = serve::Tracks::new(cli.name).produce(); let media = Media::new(writer)?; @@ -71,19 +70,19 @@ async fn main() -> anyhow::Result<()> { tokio::select! { res = session.run() => res.context("session error")?, - res = run_media(media,&nmspc) => res.context("media error")?, + res = run_media(media) => res.context("media error")?, res = publisher.announce(reader) => res.context("publisher error")?, } Ok(()) } -async fn run_media(mut media: Media, namespace: &str) -> anyhow::Result<()> { +async fn run_media(mut media: Media) -> anyhow::Result<()> { let mut input = tokio::io::stdin(); let mut buf = BytesMut::new(); loop { input.read_buf(&mut buf).await.context("failed to read from stdin")?; - media.parse(&mut buf, namespace).context("failed to parse media")?; + media.parse(&mut buf).context("failed to parse media")?; } } diff --git a/moq-pub/src/media.rs b/moq-pub/src/media.rs index e2212383..7f8c8794 100644 --- a/moq-pub/src/media.rs +++ b/moq-pub/src/media.rs @@ -2,162 +2,11 @@ use anyhow::{self, Context}; use bytes::{Buf, Bytes}; use moq_transport::serve::{GroupWriter, GroupsWriter, TrackWriter, TracksWriter}; use mp4::{self, ReadBox, TrackType}; -use serde::{Deserialize, Serialize}; use std::cmp::max; use std::collections::HashMap; use std::io::Cursor; use std::time; -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct CommonTrackFields { - #[serde(skip_serializing_if = "Option::is_none")] - pub namespace: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub packaging: Option, - #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")] - pub render_group: Option, - #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")] - pub alt_group: Option, -} - -impl CommonTrackFields { - /// Serialize function to conditionally include fields based on their commonality amoung tracks - pub fn serialize_with_common_fields(&self, tracks: &mut [Tracks]) -> Option { - // Checking if packaging,render_group and alt_group are common across tracks - let mut common_packaging = self.packaging.clone(); - let mut common_render_group = self.render_group; - let mut common_alt_group = self.alt_group; - - for track in &mut *tracks { - if let Some(ref track_packaging) = track.packaging { - if common_packaging.is_none() { - common_packaging = Some(track_packaging.clone()); - } else if common_packaging != Some(track_packaging.clone()) { - common_packaging = None; - } - } - - if let Some(track_render_group) = track.render_group { - if common_render_group.is_none() { - common_render_group = Some(track_render_group); - } else if common_render_group != Some(track_render_group) { - common_render_group = None; - } - } - - if let Some(track_alt_group) = track.alt_group { - if common_alt_group.is_none() { - common_alt_group = Some(track_alt_group); - } else if common_alt_group != Some(track_alt_group) { - common_alt_group = None; - } - } - } - - if common_render_group.is_some() { - for track in tracks.iter_mut() { - track.render_group = None; - } - } - if common_packaging.is_some() { - for track in tracks.iter_mut() { - track.packaging = None; - } - } - if common_alt_group.is_some() { - for track in tracks.iter_mut() { - track.alt_group = None; - } - } - - // Serialize only if all tracks have the same values for packaging, render_group and alt_group - if common_packaging.is_some() || common_render_group.is_some() { - Some(CommonTrackFields { - namespace: self.namespace.clone(), - packaging: common_packaging, - render_group: common_render_group, - alt_group: common_alt_group, - }) - } else { - None - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Catalog { - pub version: u16, - #[serde(rename = "streamingFormat")] - pub streaming_format: u16, - #[serde(rename = "streamingFormatVersion")] - pub streaming_format_version: String, - #[serde(rename = "supportsDeltaUpdates")] - pub streaming_delta_updates: bool, - #[serde(rename = "commonTrackFields")] - pub common_track_fields: Option, - pub tracks: Vec, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct Tracks { - #[serde(skip_serializing_if = "Option::is_none")] - pub namespace: Option, - pub name: String, - #[serde(rename = "initTrack")] - pub init_track: Option, - #[serde(rename = "initData")] - pub data_track: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub packaging: Option, - #[serde(rename = "renderGroup", skip_serializing_if = "Option::is_none")] - pub render_group: Option, - #[serde(rename = "altGroup", skip_serializing_if = "Option::is_none")] - pub alt_group: Option, - #[serde(rename = "selectionParams")] - pub selection_params: SelectionParam, - #[serde(rename = "temporalId", skip_serializing_if = "Option::is_none")] - pub temporal_id: Option, - #[serde(rename = "spatialId", skip_serializing_if = "Option::is_none")] - pub spatial_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub depends: Option>, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Default)] -pub enum TrackPackaging { - #[serde(rename = "cmaf")] - #[default] - Cmaf, - #[serde(rename = "loc")] - Loc, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct SelectionParam { - pub codec: Option, - #[serde(rename = "mimeType")] - #[serde(skip_serializing_if = "Option::is_none")] - pub mime_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub framerate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub bitrate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub width: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub height: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub samplerate: Option, - #[serde(rename = "channelConfig", skip_serializing_if = "Option::is_none")] - pub channel_config: Option, - #[serde(rename = "displayWidth", skip_serializing_if = "Option::is_none")] - pub display_width: Option, - #[serde(rename = "displayHeight", skip_serializing_if = "Option::is_none")] - pub display_height: Option, - #[serde(rename = "lang", skip_serializing_if = "Option::is_none")] - pub language: Option, -} - pub struct Media { // Tracks based on their track ID. tracks: HashMap, @@ -195,12 +44,12 @@ impl Media { // Parse the input buffer, reading any full atoms we can find. // Keep appending more data and calling parse. - pub fn parse(&mut self, buf: &mut B, namespace: &str) -> anyhow::Result<()> { - while self.parse_atom(buf, namespace)? {} + pub fn parse(&mut self, buf: &mut B) -> anyhow::Result<()> { + while self.parse_atom(buf)? {} Ok(()) } - fn parse_atom(&mut self, buf: &mut B, namespace: &str) -> anyhow::Result { + fn parse_atom(&mut self, buf: &mut B) -> anyhow::Result { let atom = match next_atom(buf)? { Some(atom) => atom, None => return Ok(false), @@ -226,7 +75,7 @@ impl Media { // Parse the moov box so we can detect the timescales for each track. let moov = mp4::MoovBox::read_box(&mut reader, header.size)?; - self.setup(&moov, atom, namespace)?; + self.setup(&moov, atom)?; self.moov = Some(moov); } mp4::BoxType::MoofBox => { @@ -277,7 +126,7 @@ impl Media { Ok(true) } - fn setup(&mut self, moov: &mp4::MoovBox, raw: Bytes, namespace: &str) -> anyhow::Result<()> { + fn setup(&mut self, moov: &mp4::MoovBox, raw: Bytes) -> anyhow::Result<()> { // Create a track for each track in the moov for trak in &moov.traks { let id = trak.tkhd.track_id; @@ -303,17 +152,19 @@ impl Media { // Produce the catalog for trak in &moov.traks { - let mut selection_params = SelectionParam::default(); - let mut track = Tracks::default(); + let mut selection_params = moq_catalog::SelectionParam::default(); + + let mut track = moq_catalog::Track { + init_track: Some("0.mp4".to_string()), + data_track: Some(format!("{}.m4s", trak.tkhd.track_id)), + namespace: Some(self.broadcast.namespace.clone()), + packaging: Some(moq_catalog::TrackPackaging::Cmaf), + render_group: Some(1), + ..Default::default() + }; let stsd = &trak.mdia.minf.stbl.stsd; - if trak.mdia.hdlr.handler_type.to_string() == "vide" || trak.mdia.hdlr.handler_type.to_string() == "soun" { - track.packaging = Some(TrackPackaging::Cmaf); - } else { - track.packaging = Some(TrackPackaging::Loc); - } - if let Some(avc1) = &stsd.avc1 { // avc1[.PPCCLL] // @@ -330,14 +181,10 @@ impl Media { let codec = rfc6381_codec::Codec::avc1(profile, constraints, level); let codec_str = codec.to_string(); - track.namespace = Some(namespace.to_string()); track.name = format!("video_{}p", height); selection_params.codec = Some(codec_str); selection_params.width = Some(width.into()); selection_params.height = Some(height.into()); - track.render_group = Some(1); - track.alt_group = Some(1); - track.selection_params = selection_params } else if let Some(_hev1) = &stsd.hev1 { // TODO https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L106 anyhow::bail!("HEVC not yet supported") @@ -350,7 +197,6 @@ impl Media { .dec_config; let codec_str = format!("mp4a.{:02x}.{}", desc.object_type_indication, desc.dec_specific.profile); - track.namespace = Some(namespace.to_string()); track.name = "audio".to_string(); selection_params.codec = Some(codec_str); selection_params.channel_config = Some(mp4a.channelcount.to_string()); @@ -360,21 +206,15 @@ impl Media { if bitrate > 0 { selection_params.bitrate = Some(bitrate); } - track.render_group = Some(1); - track.selection_params = selection_params } else if let Some(vp09) = &stsd.vp09 { // https://github.com/gpac/mp4box.js/blob/325741b592d910297bf609bc7c400fc76101077b/src/box-codecs.js#L238 let vpcc = &vp09.vpcc; let codec_str = format!("vp09.0.{:02x}.{:02x}.{:02x}", vpcc.profile, vpcc.level, vpcc.bit_depth); - track.namespace = Some(namespace.to_string()); track.name = format!("video_{}p", vp09.height); selection_params.codec = Some(codec_str); selection_params.width = Some(vp09.width.into()); selection_params.height = Some(vp09.height.into()); - track.render_group = Some(1); - track.alt_group = Some(1); - track.selection_params = selection_params; // TODO Test if this actually works; I'm just guessing based on mp4box.js anyhow::bail!("VP9 not yet supported") @@ -383,23 +223,17 @@ impl Media { anyhow::bail!("unknown codec for track: {}", trak.tkhd.track_id); } - track.init_track = Some("0.mp4".to_string()); - track.data_track = Some(format!("{}.m4s", trak.tkhd.track_id)); + track.selection_params = selection_params; tracks.push(track); } - let commontrackfields = CommonTrackFields { - namespace: Some(["quic.video/watch/", namespace].join("")), - ..Default::default() - }; - - let catalog = Catalog { + let catalog = moq_catalog::Root { version: 1, streaming_format: 1, streaming_format_version: "0.2".to_string(), streaming_delta_updates: true, - common_track_fields: commontrackfields.serialize_with_common_fields(&mut tracks), + common_track_fields: moq_catalog::CommonTrackFields::from_tracks(&mut tracks), tracks, }; From f05c00e2a9d8e218a8a5a2a086e2beaa47dd3f87 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 21 Jul 2024 09:34:29 -0700 Subject: [PATCH 8/8] Forgot the derive feature. --- moq-catalog/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moq-catalog/Cargo.toml b/moq-catalog/Cargo.toml index 88dfe9c0..dfb9d2be 100644 --- a/moq-catalog/Cargo.toml +++ b/moq-catalog/Cargo.toml @@ -14,4 +14,4 @@ categories = ["multimedia", "network-programming", "web-programming"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = "1" +serde = { version = "1", features = ["derive"] }