From d29a69487bf72991b3258d595826fb1162a87f6c Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Thu, 5 Feb 2026 15:45:07 -0500 Subject: [PATCH 1/5] first pass at mvp producer --- bun.lock | 3 ++- js/hang-demo/package.json | 1 + rs/moq-mux/src/import/hls.rs | 51 +++++++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 53c6563dc..9721fe176 100644 --- a/bun.lock +++ b/bun.lock @@ -66,6 +66,7 @@ "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.13", "highlight.js": "^11.11.1", + "solid-js": "^1.9.10", "tailwindcss": "^4.1.13", "typescript": "^5.9.2", "vite": "^6.3.6", @@ -118,7 +119,7 @@ }, "js/signals": { "name": "@moq/signals", - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "dequal": "^2.0.3", }, diff --git a/js/hang-demo/package.json b/js/hang-demo/package.json index da5a01232..91815a52c 100644 --- a/js/hang-demo/package.json +++ b/js/hang-demo/package.json @@ -19,6 +19,7 @@ "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.13", "highlight.js": "^11.11.1", + "solid-js": "^1.9.10", "tailwindcss": "^4.1.13", "typescript": "^5.9.2", "vite": "^6.3.6", diff --git a/rs/moq-mux/src/import/hls.rs b/rs/moq-mux/src/import/hls.rs index 4409d4a2f..19c8287fb 100644 --- a/rs/moq-mux/src/import/hls.rs +++ b/rs/moq-mux/src/import/hls.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::collections::hash_map::Entry; +use std::fmt::Write; use std::path::PathBuf; use std::time::Duration; @@ -76,6 +77,12 @@ struct StepOutcome { /// /// Provides `init()` to prime the ingest with initial segments, and `service()` /// to run the continuous ingest loop. +/// +/// In addition to importing media, this generates HLS playlists as MoQ tracks: +/// - `playlist.m3u8`: Main/master playlist referencing all renditions +/// - `video{n}.m3u8`: Media playlist for each video rendition +/// - `audio.m3u8`: Media playlist for the audio rendition (if present) +/// @TODO: multi audio pub struct Hls { /// Broadcast that all CMAF importers write into. broadcast: moq_lite::BroadcastProducer, @@ -83,6 +90,8 @@ pub struct Hls { /// The catalog being produced. catalog: hang::CatalogProducer, + master_playlist_track: Option, + /// fMP4 importers for each discovered video rendition. /// Each importer feeds a separate MoQ track but shares the same catalog. video_importers: Vec, @@ -108,14 +117,19 @@ enum TrackKind { struct TrackState { playlist: Url, + playlist_track: moq_lite::TrackProducer, + init_track: moq_lite::TrackProducer, next_sequence: Option, init_ready: bool, + //last_playlist_hash: Option, // do we need this to detect playlist changes? } impl TrackState { - fn new(playlist: Url) -> Self { + fn new(playlist: Url, playlist_track: moq_lite::TrackProducer, init_track: moq_lite::TrackProducer) -> Self { Self { playlist, + playlist_track, + init_track, next_sequence: None, init_ready: false, } @@ -140,6 +154,7 @@ impl Hls { Ok(Self { broadcast, catalog, + master_playlist_track: None, video_importers: Vec::new(), passthrough, audio_importer: None, @@ -293,9 +308,11 @@ impl Hls { anyhow::ensure!(!variants.is_empty(), "no usable variants found in master playlist"); // Create a video track state for every usable variant. - for variant in &variants { + for (index, variant) in variants.iter().enumerate() { let video_url = resolve_uri(&self.base_url, &variant.uri)?; - self.video.push(TrackState::new(video_url)); + let playlist_track = self.broadcast.create_track(moq_lite::Track::new(format!("video{}.m3u8", index))); + let init_track = self.broadcast.create_track(moq_lite::Track::new(format!("video{}.init.mp4", index))); + self.video.push(TrackState::new(video_url, playlist_track, init_track)); } // Choose an audio rendition based on the first variant with an audio group. @@ -303,7 +320,9 @@ impl Hls { if let Some(audio_tag) = select_audio(&master, group_id) { if let Some(uri) = &audio_tag.uri { let audio_url = resolve_uri(&self.base_url, uri)?; - self.audio = Some(TrackState::new(audio_url)); + let playlist_track = self.broadcast.create_track(moq_lite::Track::new("audio.m3u8")); + let init_track = self.broadcast.create_track(moq_lite::Track::new("audio.init.mp4")); + self.audio = Some(TrackState::new(audio_url, playlist_track, init_track)); } else { warn!(%group_id, "audio rendition missing URI"); } @@ -318,12 +337,18 @@ impl Hls { audio = audio_url.as_deref().unwrap_or("none"), "selected master playlist renditions" ); - + self.master_playlist_track = Some(self.broadcast.create_track(moq_lite::Track::new("playlist.m3u8"))); + if let Some(ref mut track) = self.master_playlist_track { + publish_master_playlist(track.clone(), master); + } return Ok(()); } // Fallback: treat the provided URL as a single media playlist. - self.video.push(TrackState::new(self.base_url.clone())); + self.video.push(TrackState::new(self.base_url.clone(), + self.broadcast.create_track(moq_lite::Track::new("video0.m3u8")), + self.broadcast.create_track(moq_lite::Track::new("video0.init.mp4")), + )); Ok(()) } @@ -621,6 +646,20 @@ fn resolve_uri(base: &Url, value: &str) -> std::result::Result Date: Tue, 10 Feb 2026 20:49:47 -0500 Subject: [PATCH 2/5] add ContentType header based on Fetch extension --- rs/moq-mux/src/import/hls.rs | 12 +++++------- rs/moq-relay/src/web.rs | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/rs/moq-mux/src/import/hls.rs b/rs/moq-mux/src/import/hls.rs index 19c8287fb..076207c67 100644 --- a/rs/moq-mux/src/import/hls.rs +++ b/rs/moq-mux/src/import/hls.rs @@ -647,16 +647,14 @@ fn resolve_uri(base: &Url, value: &str) -> std::result::Result = Vec::new(); + master.write_to(&mut v).unwrap(); - // Close the group (important!) + group.write_frame(v); group.close(); } diff --git a/rs/moq-relay/src/web.rs b/rs/moq-relay/src/web.rs index 81534f97f..b1f885f9e 100644 --- a/rs/moq-relay/src/web.rs +++ b/rs/moq-relay/src/web.rs @@ -362,6 +362,7 @@ async fn serve_fetch( group: None, frame: Some(frame), deadline, + content_type: None, }), Ok(None) => Err(StatusCode::NOT_FOUND), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), @@ -370,6 +371,7 @@ async fn serve_fetch( group: Some(group), frame: None, deadline, + content_type: content_type_for_track(&track.info.name), }), } }) @@ -382,10 +384,21 @@ async fn serve_fetch( } } +fn content_type_for_track(track_name: &str) -> Option<&'static str> { + if track_name.ends_with(".m3u8") { + Some("application/vnd.apple.mpegurl") + } else if track_name.ends_with(".init") || track_name.ends_with(".m4s") || track_name.ends_with(".mp4") { + Some("video/mp4") + } else { + None + } +} + struct ServeGroup { group: Option, frame: Option, deadline: tokio::time::Instant, + content_type: Option<&'static str>, } impl ServeGroup { @@ -415,7 +428,15 @@ impl ServeGroup { impl IntoResponse for ServeGroup { fn into_response(self) -> Response { - Response::new(Body::new(self)) + let content_type = self.content_type; + let mut response = Response::new(Body::new(self)); + if let Some(ct) = content_type { + response.headers_mut().insert( + axum::http::header::CONTENT_TYPE, + axum::http::HeaderValue::from_static(ct), + ); + } + response } } From e77397ef0a1840bb9b35f277008a06777e4099fd Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Tue, 17 Feb 2026 20:51:57 -0500 Subject: [PATCH 3/5] simplify hls import, proper playlist tracks --- rs/moq-mux/src/import/fmp4.rs | 4 +- rs/moq-mux/src/import/hls.rs | 220 ++++++++++++++-------------------- 2 files changed, 90 insertions(+), 134 deletions(-) diff --git a/rs/moq-mux/src/import/fmp4.rs b/rs/moq-mux/src/import/fmp4.rs index 0e612d9f2..04660b820 100644 --- a/rs/moq-mux/src/import/fmp4.rs +++ b/rs/moq-mux/src/import/fmp4.rs @@ -180,12 +180,12 @@ impl Fmp4 { let (kind, track) = match handler.as_ref() { b"vide" => { let config = self.init_video(trak)?; - let track = catalog.video.create_track("m4s", config.clone()); + let track = catalog.video.create_track(".m4s", config.clone()); (TrackKind::Video, track) } b"soun" => { let config = self.init_audio(trak)?; - let track = catalog.audio.create_track("m4s", config.clone()); + let track = catalog.audio.create_track(".m4s", config.clone()); (TrackKind::Audio, track) } b"sbtl" => anyhow::bail!("subtitle tracks are not supported"), diff --git a/rs/moq-mux/src/import/hls.rs b/rs/moq-mux/src/import/hls.rs index 076207c67..6d55ae76b 100644 --- a/rs/moq-mux/src/import/hls.rs +++ b/rs/moq-mux/src/import/hls.rs @@ -5,16 +5,13 @@ //! independent of any particular HTTP client; callers provide an implementation //! of [`Fetcher`] to perform the actual network I/O. -use std::collections::HashMap; -use std::collections::hash_map::Entry; -use std::fmt::Write; use std::path::PathBuf; use std::time::Duration; use anyhow::Context; use bytes::Bytes; use m3u8_rs::{ - AlternativeMedia, AlternativeMediaType, Map, MasterPlaylist, MediaPlaylist, MediaSegment, Resolution, VariantStream, + Map, MasterPlaylist, MediaPlaylist, MediaSegment, }; use reqwest::Client; use tracing::{debug, info, warn}; @@ -118,18 +115,19 @@ enum TrackKind { struct TrackState { playlist: Url, playlist_track: moq_lite::TrackProducer, - init_track: moq_lite::TrackProducer, + track_id: String, + init_track: Option, next_sequence: Option, init_ready: bool, - //last_playlist_hash: Option, // do we need this to detect playlist changes? } impl TrackState { - fn new(playlist: Url, playlist_track: moq_lite::TrackProducer, init_track: moq_lite::TrackProducer) -> Self { + fn new(playlist: Url, playlist_track: moq_lite::TrackProducer, track_id: String) -> Self { Self { playlist, playlist_track, - init_track, + track_id, + init_track: None, next_sequence: None, init_ready: false, } @@ -235,15 +233,17 @@ impl Hls { /// and returns how many segments were written along with the target /// duration to guide scheduling of the next step. async fn step(&mut self) -> anyhow::Result { + // Fetch master playlist, create tracks for each variant advertised. self.ensure_tracks().await?; let mut wrote = 0usize; let mut target_duration = None; + // @TODO: consolidate audio_tracks/video_tracks into media_tracks // Ingest a step from all active video variants. let video_tracks = std::mem::take(&mut self.video); for (index, mut track) in video_tracks.into_iter().enumerate() { - let playlist = self.fetch_media_playlist(track.playlist.clone()).await?; + let mut playlist = self.fetch_media_playlist(track.playlist.clone()).await?; // Use the first video's target duration as the base. if target_duration.is_none() { target_duration = Some(playlist.target_duration); @@ -252,12 +252,16 @@ impl Hls { .consume_segments(TrackKind::Video(index), &mut track, &playlist, None) .await?; wrote += count; + + self.rewrite_segment_locations(&mut playlist, &track.track_id); + + publish_playlist(track.playlist_track.clone(), Playlist::Media(playlist.clone())); self.video.push(track); } // Ingest from the shared audio track, if present. if let Some(mut track) = self.audio.take() { - let playlist = self.fetch_media_playlist(track.playlist.clone()).await?; + let mut playlist = self.fetch_media_playlist(track.playlist.clone()).await?; if target_duration.is_none() { target_duration = Some(playlist.target_duration); } @@ -265,6 +269,10 @@ impl Hls { .consume_segments(TrackKind::Audio, &mut track, &playlist, None) .await?; wrote += count; + + self.rewrite_segment_locations(&mut playlist, &track.track_id); + + publish_playlist(track.playlist_track.clone(), Playlist::Media(playlist.clone())); self.audio = Some(track); } @@ -303,43 +311,47 @@ impl Hls { } let body = self.fetch_bytes(self.base_url.clone()).await?; - if let Ok((_, master)) = m3u8_rs::parse_master_playlist(&body) { - let variants = select_variants(&master); - anyhow::ensure!(!variants.is_empty(), "no usable variants found in master playlist"); - + if let Ok((_, mut master)) = m3u8_rs::parse_master_playlist(&body) { // Create a video track state for every usable variant. - for (index, variant) in variants.iter().enumerate() { - let video_url = resolve_uri(&self.base_url, &variant.uri)?; - let playlist_track = self.broadcast.create_track(moq_lite::Track::new(format!("video{}.m3u8", index))); - let init_track = self.broadcast.create_track(moq_lite::Track::new(format!("video{}.init.mp4", index))); - self.video.push(TrackState::new(video_url, playlist_track, init_track)); + for (index, variant) in master.variants.iter_mut().enumerate() { + let media_url = resolve_uri(&self.base_url, &variant.uri)?; + let track_id = format!("{}{}", if variant.resolution.is_some() { "video" } else { "audio" }, index); + variant.uri = format!("{}.m3u8", track_id); + + let playlist_track = self.broadcast.create_track(moq_lite::Track::new(format!("{}.m3u8", track_id))); + + // @TODO: do we really need separate state for Audio and Video tracks? Can this just be a vec of Media Tracks? + // no reason this couldn't handle subtitles as well. + if track_id.contains("video") { + self.video.push(TrackState::new(media_url, playlist_track, track_id)); + } + // @NOTE: ignoring audio tracks because these really should only be advertised as #EXT-X-MEDIA variants + // I think this is just a bug in the output of our `just hls bbb` ingest, "real" hls playlists shouldn't do this } - // Choose an audio rendition based on the first variant with an audio group. - if let Some(group_id) = variants.iter().find_map(|v| v.audio.as_deref()) { - if let Some(audio_tag) = select_audio(&master, group_id) { - if let Some(uri) = &audio_tag.uri { - let audio_url = resolve_uri(&self.base_url, uri)?; - let playlist_track = self.broadcast.create_track(moq_lite::Track::new("audio.m3u8")); - let init_track = self.broadcast.create_track(moq_lite::Track::new("audio.init.mp4")); - self.audio = Some(TrackState::new(audio_url, playlist_track, init_track)); - } else { - warn!(%group_id, "audio rendition missing URI"); - } - } else { - warn!(%group_id, "audio group not found in master playlist"); + // Audio tracks all live under "alternatives", we'll also handle captions/text tracks here eventually + for (index, alternative) in master.alternatives.iter_mut().enumerate() { + if let Some(uri) = &alternative.uri { + let media_url = resolve_uri(&self.base_url, uri)?; + let track_id = format!("{}{}", &alternative.media_type, index); + alternative.uri = Some(format!("{}.m3u8", track_id)); + let playlist_track = self.broadcast.create_track(moq_lite::Track::new(format!("{}.m3u8", track_id))); + + // @TODO: push this into a generic media track array + self.audio = Some(TrackState::new(media_url, playlist_track, track_id)); } } let audio_url = self.audio.as_ref().map(|a| a.playlist.to_string()); info!( - video_variants = variants.len(), + video_variants = master.variants.len(), audio = audio_url.as_deref().unwrap_or("none"), "selected master playlist renditions" ); + self.master_playlist_track = Some(self.broadcast.create_track(moq_lite::Track::new("playlist.m3u8"))); if let Some(ref mut track) = self.master_playlist_track { - publish_master_playlist(track.clone(), master); + publish_playlist(track.clone(), Playlist::Master(master)) } return Ok(()); } @@ -347,7 +359,7 @@ impl Hls { // Fallback: treat the provided URL as a single media playlist. self.video.push(TrackState::new(self.base_url.clone(), self.broadcast.create_track(moq_lite::Track::new("video0.m3u8")), - self.broadcast.create_track(moq_lite::Track::new("video0.init.mp4")), + "video0".to_string() )); Ok(()) } @@ -430,7 +442,21 @@ impl Hls { let map = self.find_map(playlist).context("playlist missing EXT-X-MAP")?; let url = resolve_uri(&track.playlist, &map.uri)?; + + if track.init_track.is_none() { + let init_track_name = format!("{}.init.mp4", track.track_id); + track.init_track = Some(self.broadcast.create_track(moq_lite::Track::new(init_track_name))); + } + let mut bytes = self.fetch_bytes(url).await?; + + // Publish init segment to its track + let init_track = track.init_track.as_mut().expect("init_track was just created"); + let mut group = init_track.append_group(); + dbg!("group sequence:", group.info.sequence); + group.write_frame(bytes.clone()); + group.close(); + let importer = match kind { TrackKind::Video(index) => self.ensure_video_importer_for(index), TrackKind::Audio => self.ensure_audio_importer(), @@ -494,6 +520,7 @@ impl Hls { } async fn fetch_bytes(&self, url: Url) -> anyhow::Result { + dbg!(&url); if url.scheme() == "file" { let path = url.to_file_path().ok().context("invalid file URL")?; let bytes = tokio::fs::read(&path).await.context("failed to read file")?; @@ -532,6 +559,17 @@ impl Hls { .get_or_insert_with(|| Fmp4::new(self.broadcast.clone(), self.catalog.clone(), Fmp4Config { passthrough })) } + fn rewrite_segment_locations(&mut self, playlist: &mut MediaPlaylist, track_id: &str) { + let msn = playlist.media_sequence; + // Find and modify the first segment with a map + for (index, segment) in playlist.segments.iter_mut().enumerate() { + if let Some(ref mut map) = segment.map { + map.uri = format!("{}.init.mp4?group=0", track_id); // @TODO: do I need ?group=0 for this? + } + segment.uri = format!("{}.m4s?group={}", track_id, msn + index as u64); + } + } + #[cfg(test)] fn has_video_importer(&self) -> bool { !self.video_importers.is_empty() @@ -543,100 +581,6 @@ impl Hls { } } -fn select_audio<'a>(master: &'a MasterPlaylist, group_id: &str) -> Option<&'a AlternativeMedia> { - let mut first = None; - let mut default = None; - - for alternative in master - .alternatives - .iter() - .filter(|alt| alt.media_type == AlternativeMediaType::Audio && alt.group_id == group_id) - { - if first.is_none() { - first = Some(alternative); - } - if alternative.default { - default = Some(alternative); - break; - } - } - - default.or(first) -} - -fn select_variants(master: &MasterPlaylist) -> Vec<&VariantStream> { - // Helper to extract the first video codec token from the CODECS attribute. - fn first_video_codec(variant: &VariantStream) -> Option<&str> { - let codecs = variant.codecs.as_deref()?; - codecs.split(',').map(|s| s.trim()).find(|s| !s.is_empty()) - } - - // Map codec strings into a coarse "family" so we can prefer H.264 over others. - fn codec_family(codec: &str) -> Option<&'static str> { - if codec.starts_with("avc1.") || codec.starts_with("avc3.") { - Some("h264") - } else { - None - } - } - - // Consider only non-i-frame variants with a URI and a known codec family. - let candidates: Vec<(&VariantStream, &str, &str)> = master - .variants - .iter() - .filter(|variant| !variant.is_i_frame && !variant.uri.is_empty()) - .filter_map(|variant| { - let codec = first_video_codec(variant)?; - let family = codec_family(codec)?; - Some((variant, codec, family)) - }) - .collect(); - - if candidates.is_empty() { - return Vec::new(); - } - - // Prefer families in this order, falling back to the first available. - const FAMILY_PREFERENCE: &[&str] = &["h264"]; - - let families_present: Vec<&str> = candidates.iter().map(|(_, _, fam)| *fam).collect(); - - let target_family = FAMILY_PREFERENCE - .iter() - .find(|fav| families_present.iter().any(|fam| fam == *fav)) - .copied() - .unwrap_or(families_present[0]); - - // Keep only variants in the chosen family. - let family_variants: Vec<&VariantStream> = candidates - .into_iter() - .filter(|(_, _, fam)| *fam == target_family) - .map(|(variant, _, _)| variant) - .collect(); - - // Deduplicate by resolution, keeping the lowest-bandwidth variant for each size. - let mut by_resolution: HashMap, &VariantStream> = HashMap::new(); - - for variant in family_variants { - let key = variant.resolution; - let bandwidth = variant.average_bandwidth.unwrap_or(variant.bandwidth); - - match by_resolution.entry(key) { - Entry::Vacant(entry) => { - entry.insert(variant); - } - Entry::Occupied(mut entry) => { - let existing = entry.get(); - let existing_bw = existing.average_bandwidth.unwrap_or(existing.bandwidth); - if bandwidth < existing_bw { - entry.insert(variant); - } - } - } - } - - by_resolution.values().cloned().collect() -} fn resolve_uri(base: &Url, value: &str) -> std::result::Result { if let Ok(url) = Url::parse(value) { @@ -646,13 +590,25 @@ fn resolve_uri(base: &Url, value: &str) -> std::result::Result = Vec::new(); - master.write_to(&mut v).unwrap(); + + match playlist { + Playlist::Master(master) => master.write_to(&mut v).unwrap(), + Playlist::Media(media) => media.write_to(&mut v).unwrap(), + } group.write_frame(v); group.close(); From ba5433fddfaf36dc79fb872723c935d0a3b7c546 Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Tue, 17 Feb 2026 21:14:09 -0500 Subject: [PATCH 4/5] remove debug logs --- rs/moq-mux/src/import/hls.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rs/moq-mux/src/import/hls.rs b/rs/moq-mux/src/import/hls.rs index 6d55ae76b..1576515c0 100644 --- a/rs/moq-mux/src/import/hls.rs +++ b/rs/moq-mux/src/import/hls.rs @@ -453,7 +453,6 @@ impl Hls { // Publish init segment to its track let init_track = track.init_track.as_mut().expect("init_track was just created"); let mut group = init_track.append_group(); - dbg!("group sequence:", group.info.sequence); group.write_frame(bytes.clone()); group.close(); @@ -520,7 +519,6 @@ impl Hls { } async fn fetch_bytes(&self, url: Url) -> anyhow::Result { - dbg!(&url); if url.scheme() == "file" { let path = url.to_file_path().ok().context("invalid file URL")?; let bytes = tokio::fs::read(&path).await.context("failed to read file")?; @@ -597,12 +595,8 @@ enum Playlist { } fn publish_playlist(mut playlist_track: moq_lite::TrackProducer, playlist: Playlist) { - // The Multi-Variant Playlist never changes so we can just serialize it, publish a single group and move on. - // No need to rewrite pathing as variant playlists are all relative to base url. let mut group = playlist_track.append_group(); - dbg!(&playlist); // Debug print to see the playlist structure - let mut v: Vec = Vec::new(); match playlist { From d2ff0812a8c9a91e3828bc64a02e5421e84a352e Mon Sep 17 00:00:00 2001 From: Henry McIntyre Date: Wed, 18 Feb 2026 15:02:07 -0500 Subject: [PATCH 5/5] fix track naming discrepencies --- rs/moq-mux/src/import/fmp4.rs | 4 ++-- rs/moq-mux/src/import/hls.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rs/moq-mux/src/import/fmp4.rs b/rs/moq-mux/src/import/fmp4.rs index 04660b820..0e612d9f2 100644 --- a/rs/moq-mux/src/import/fmp4.rs +++ b/rs/moq-mux/src/import/fmp4.rs @@ -180,12 +180,12 @@ impl Fmp4 { let (kind, track) = match handler.as_ref() { b"vide" => { let config = self.init_video(trak)?; - let track = catalog.video.create_track(".m4s", config.clone()); + let track = catalog.video.create_track("m4s", config.clone()); (TrackKind::Video, track) } b"soun" => { let config = self.init_audio(trak)?; - let track = catalog.audio.create_track(".m4s", config.clone()); + let track = catalog.audio.create_track("m4s", config.clone()); (TrackKind::Audio, track) } b"sbtl" => anyhow::bail!("subtitle tracks are not supported"), diff --git a/rs/moq-mux/src/import/hls.rs b/rs/moq-mux/src/import/hls.rs index 1576515c0..048806448 100644 --- a/rs/moq-mux/src/import/hls.rs +++ b/rs/moq-mux/src/import/hls.rs @@ -333,7 +333,7 @@ impl Hls { for (index, alternative) in master.alternatives.iter_mut().enumerate() { if let Some(uri) = &alternative.uri { let media_url = resolve_uri(&self.base_url, uri)?; - let track_id = format!("{}{}", &alternative.media_type, index); + let track_id = format!("{}{}", &alternative.media_type.to_string().to_lowercase(), index); alternative.uri = Some(format!("{}.m3u8", track_id)); let playlist_track = self.broadcast.create_track(moq_lite::Track::new(format!("{}.m3u8", track_id)));