Skip to content

Commit 81ac702

Browse files
kixelatedcodex
andauthored
fix(moq-hls): account for audio groups in master variants (moq-dev#2264)
Co-authored-by: Codex <codex@openai.com>
1 parent 11f4c37 commit 81ac702

1 file changed

Lines changed: 119 additions & 27 deletions

File tree

rs/moq-hls/src/export/master.rs

Lines changed: 119 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! URIs are relative to the master playlist (`/<broadcast>/master.m3u8`), so a
44
//! rendition's `<name>/media.m3u8` resolves under the broadcast directory.
55
6+
use std::collections::BTreeMap;
67
use std::fmt::Write;
78

89
const VERSION: u32 = 9;
@@ -32,41 +33,86 @@ pub struct AudioVariant {
3233
pub codec: String,
3334
}
3435

35-
/// Render the multivariant playlist. The first audio rendition is marked default.
36+
struct AudioGroup<'a> {
37+
id: String,
38+
bandwidth: u64,
39+
codec: &'a str,
40+
variants: Vec<&'a AudioVariant>,
41+
}
42+
43+
fn group_audio(audio: &[AudioVariant]) -> Vec<AudioGroup<'_>> {
44+
let mut codecs = BTreeMap::<&str, Vec<&AudioVariant>>::new();
45+
for variant in audio {
46+
codecs.entry(&variant.codec).or_default().push(variant);
47+
}
48+
49+
let multiple = codecs.len() > 1;
50+
codecs
51+
.into_iter()
52+
.enumerate()
53+
.map(|(index, (codec, variants))| AudioGroup {
54+
id: if multiple {
55+
format!("{AUDIO_GROUP}-{index}")
56+
} else {
57+
AUDIO_GROUP.to_string()
58+
},
59+
bandwidth: variants
60+
.iter()
61+
.map(|variant| variant.bandwidth)
62+
.max()
63+
.unwrap_or_default(),
64+
codec,
65+
variants,
66+
})
67+
.collect()
68+
}
69+
70+
fn render_video(out: &mut String, variant: &VideoVariant, audio: Option<&AudioGroup<'_>>) {
71+
let bandwidth = variant
72+
.bandwidth
73+
.saturating_add(audio.map_or(0, |group| group.bandwidth));
74+
let codecs = audio.map_or_else(
75+
|| variant.codec.clone(),
76+
|group| format!("{},{}", variant.codec, group.codec),
77+
);
78+
let mut line = format!("#EXT-X-STREAM-INF:BANDWIDTH={bandwidth}");
79+
if let (Some(w), Some(h)) = (variant.width, variant.height) {
80+
let _ = write!(line, ",RESOLUTION={w}x{h}");
81+
}
82+
let _ = write!(line, ",CODECS=\"{codecs}\"");
83+
if let Some(group) = audio {
84+
let _ = write!(line, ",AUDIO=\"{}\"", group.id);
85+
}
86+
let _ = writeln!(out, "{line}");
87+
let _ = writeln!(out, "{}/media.m3u8", variant.name);
88+
}
89+
90+
/// Render the multivariant playlist. The first rendition in each audio codec group is default.
3691
pub fn render_master(video: &[VideoVariant], audio: &[AudioVariant]) -> String {
3792
let mut out = String::new();
3893
let _ = writeln!(out, "#EXTM3U");
3994
let _ = writeln!(out, "#EXT-X-VERSION:{VERSION}");
4095

41-
let has_audio = !audio.is_empty();
42-
for (index, variant) in audio.iter().enumerate() {
43-
let default = if index == 0 { "YES" } else { "NO" };
44-
let _ = writeln!(
45-
out,
46-
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"{AUDIO_GROUP}\",NAME=\"{}\",DEFAULT={default},AUTOSELECT=YES,URI=\"{}/media.m3u8\"",
47-
variant.name, variant.name
48-
);
96+
let audio_groups = group_audio(audio);
97+
for group in &audio_groups {
98+
for (index, variant) in group.variants.iter().enumerate() {
99+
let default = if index == 0 { "YES" } else { "NO" };
100+
let _ = writeln!(
101+
out,
102+
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"{}\",NAME=\"{}\",DEFAULT={default},AUTOSELECT=YES,URI=\"{}/media.m3u8\"",
103+
group.id, variant.name, variant.name
104+
);
105+
}
49106
}
50107

51-
// One audio codec is enough for the combined CODECS attribute.
52-
let audio_codec = audio.first().map(|a| a.codec.as_str());
53-
54108
for variant in video {
55-
let codecs = match audio_codec {
56-
Some(audio) => format!("{},{}", variant.codec, audio),
57-
None => variant.codec.clone(),
58-
};
59-
60-
let mut line = format!("#EXT-X-STREAM-INF:BANDWIDTH={}", variant.bandwidth);
61-
if let (Some(w), Some(h)) = (variant.width, variant.height) {
62-
let _ = write!(line, ",RESOLUTION={w}x{h}");
109+
if audio_groups.is_empty() {
110+
render_video(&mut out, variant, None);
111+
} else {
112+
for group in &audio_groups {
113+
render_video(&mut out, variant, Some(group));
114+
}
63115
}
64-
let _ = write!(line, ",CODECS=\"{codecs}\"");
65-
if has_audio {
66-
let _ = write!(line, ",AUDIO=\"{AUDIO_GROUP}\"");
67-
}
68-
let _ = writeln!(out, "{line}");
69-
let _ = writeln!(out, "{}/media.m3u8", variant.name);
70116
}
71117

72118
// Audio-only broadcast: still expose a playable variant per audio rendition.
@@ -109,11 +155,57 @@ mod tests {
109155
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud\",NAME=\"audio\",DEFAULT=YES,AUTOSELECT=YES,URI=\"audio/media.m3u8\"\n"
110156
));
111157
assert!(out.contains(
112-
"#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720,CODECS=\"avc1.42c01f,mp4a.40.2\",AUDIO=\"aud\"\n"
158+
"#EXT-X-STREAM-INF:BANDWIDTH=2628000,RESOLUTION=1280x720,CODECS=\"avc1.42c01f,mp4a.40.2\",AUDIO=\"aud\"\n"
113159
));
114160
assert!(out.contains("\nvideo/media.m3u8\n"));
115161
}
116162

163+
#[test]
164+
fn separates_audio_codecs_into_accurate_variants() {
165+
let video = vec![VideoVariant {
166+
name: "video".into(),
167+
bandwidth: 2_500_000,
168+
width: Some(1280),
169+
height: Some(720),
170+
codec: "avc1.42c01f".into(),
171+
}];
172+
let audio = vec![
173+
AudioVariant {
174+
name: "aac-low".into(),
175+
bandwidth: 96_000,
176+
codec: "mp4a.40.2".into(),
177+
},
178+
AudioVariant {
179+
name: "aac-high".into(),
180+
bandwidth: 128_000,
181+
codec: "mp4a.40.2".into(),
182+
},
183+
AudioVariant {
184+
name: "opus".into(),
185+
bandwidth: 160_000,
186+
codec: "opus".into(),
187+
},
188+
];
189+
190+
let out = render_master(&video, &audio);
191+
assert!(out.contains(
192+
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud-0\",NAME=\"aac-low\",DEFAULT=YES,AUTOSELECT=YES,URI=\"aac-low/media.m3u8\"\n"
193+
));
194+
assert!(out.contains(
195+
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud-0\",NAME=\"aac-high\",DEFAULT=NO,AUTOSELECT=YES,URI=\"aac-high/media.m3u8\"\n"
196+
));
197+
assert!(out.contains(
198+
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud-1\",NAME=\"opus\",DEFAULT=YES,AUTOSELECT=YES,URI=\"opus/media.m3u8\"\n"
199+
));
200+
assert!(out.contains(
201+
"#EXT-X-STREAM-INF:BANDWIDTH=2628000,RESOLUTION=1280x720,CODECS=\"avc1.42c01f,mp4a.40.2\",AUDIO=\"aud-0\"\n"
202+
));
203+
assert!(out.contains(
204+
"#EXT-X-STREAM-INF:BANDWIDTH=2660000,RESOLUTION=1280x720,CODECS=\"avc1.42c01f,opus\",AUDIO=\"aud-1\"\n"
205+
));
206+
assert_eq!(out.matches("\nvideo/media.m3u8\n").count(), 2);
207+
}
208+
117209
#[test]
118210
fn audio_only_is_playable() {
119211
let audio = vec![AudioVariant {

0 commit comments

Comments
 (0)