use std::collections::BTreeMap;
use std::fmt::Write;
use std::time::{Duration, SystemTime};
use super::Kind;
pub(crate) struct Representation {
pub name: String,
pub kind: Kind,
pub bandwidth: u64,
pub codec: String,
pub width: Option<u32>,
pub height: Option<u32>,
pub framerate: Option<f64>,
pub sample_rate: Option<u32>,
pub channel_count: Option<u32>,
pub timescale: u32,
pub segments: Vec<(u64, u64)>,
pub ended: bool,
}
pub(crate) struct Manifest {
pub availability_start: Option<SystemTime>,
pub publish: SystemTime,
pub window: Duration,
pub finished: bool,
pub video: Vec<Representation>,
pub audio: Vec<Representation>,
}
fn escape(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for c in value.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
fn xs_duration(duration: Duration) -> String {
format!("PT{:.3}S", duration.as_secs_f64())
}
fn frame_rate(rate: f64) -> Option<String> {
if !rate.is_finite() || rate <= 0.0 {
return None;
}
if rate.fract() == 0.0 {
Some(format!("{}", rate as u64))
} else {
Some(format!("{}/1000", (rate * 1000.0).round() as u64))
}
}
fn max_segment_duration<'a>(representations: impl Iterator<Item = &'a Representation>) -> u64 {
representations
.flat_map(|rep| {
let timescale = rep.timescale.max(1) as u64;
rep.segments.iter().map(move |(_, d)| d.div_ceil(timescale))
})
.max()
.unwrap_or(0)
.max(1)
}
fn render_representation(out: &mut String, rep: &Representation, suffix: &str) {
let kind = rep.kind.as_str();
let name = escape(&rep.name);
let mut attrs = format!(
"id=\"{kind}/{name}\" bandwidth=\"{}\" codecs=\"{}\"",
rep.bandwidth,
escape(&rep.codec)
);
if let (Some(width), Some(height)) = (rep.width, rep.height) {
let _ = write!(attrs, " width=\"{width}\" height=\"{height}\"");
}
if let Some(rate) = rep.framerate.and_then(frame_rate) {
let _ = write!(attrs, " frameRate=\"{rate}\"");
}
if let Some(sample_rate) = rep.sample_rate {
let _ = write!(attrs, " audioSamplingRate=\"{sample_rate}\"");
}
let _ = writeln!(out, " <Representation {attrs}>");
if let Some(channels) = rep.channel_count {
let _ = writeln!(
out,
" <AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"{channels}\"/>"
);
}
let _ = writeln!(
out,
" <SegmentTemplate timescale=\"{}\" initialization=\"{kind}/{name}/init.mp4{suffix}\" media=\"{kind}/{name}/seg/t$Time$.m4s{suffix}\">",
rep.timescale.max(1)
);
let _ = writeln!(out, " <SegmentTimeline>");
for (t, d) in &rep.segments {
let _ = writeln!(out, " <S t=\"{t}\" d=\"{d}\"/>");
}
let _ = writeln!(out, " </SegmentTimeline>");
let _ = writeln!(out, " </SegmentTemplate>");
let _ = writeln!(out, " </Representation>");
}
fn render_adaptation_sets(out: &mut String, kind: Kind, representations: &[Representation], suffix: &str) {
let mut codecs = BTreeMap::<&str, Vec<&Representation>>::new();
for rep in representations {
if rep.segments.is_empty() {
continue;
}
codecs.entry(&rep.codec).or_default().push(rep);
}
let (content, mime) = match kind {
Kind::Video => ("video", "video/mp4"),
Kind::Audio => ("audio", "audio/mp4"),
};
for representations in codecs.values() {
let _ = writeln!(
out,
" <AdaptationSet contentType=\"{content}\" mimeType=\"{mime}\" segmentAlignment=\"true\" startWithSAP=\"1\">"
);
for rep in representations {
render_representation(out, rep, suffix);
}
let _ = writeln!(out, " </AdaptationSet>");
}
}
pub(crate) fn render_manifest(manifest: &Manifest, query: Option<&str>) -> String {
let suffix = query.map(|q| format!("?{}", escape(q))).unwrap_or_default();
let representations = || manifest.video.iter().chain(&manifest.audio);
let target = max_segment_duration(representations());
let mut out = String::new();
let _ = writeln!(out, "<?xml version=\"1.0\" encoding=\"utf-8\"?>");
let _ = write!(
out,
"<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011\""
);
if manifest.finished {
let duration = representations()
.filter_map(|rep| {
let (t, d) = rep.segments.last()?;
let timescale = rep.timescale.max(1) as u64;
Some(Duration::from_nanos(
((t + d) as u128 * 1_000_000_000 / timescale as u128) as u64,
))
})
.max()
.unwrap_or_default();
let _ = write!(
out,
" type=\"static\" mediaPresentationDuration=\"{}\"",
xs_duration(duration)
);
} else {
let availability = manifest.availability_start.unwrap_or(SystemTime::UNIX_EPOCH);
let update = Duration::from_secs(target);
let delay = Duration::from_secs(3 * target).min(manifest.window.max(update));
let _ = write!(
out,
" type=\"dynamic\" availabilityStartTime=\"{}\" publishTime=\"{}\" minimumUpdatePeriod=\"{}\" timeShiftBufferDepth=\"{}\" suggestedPresentationDelay=\"{}\"",
humantime::format_rfc3339_millis(availability),
humantime::format_rfc3339_millis(manifest.publish),
xs_duration(update),
xs_duration(manifest.window),
xs_duration(delay),
);
}
let _ = writeln!(
out,
" minBufferTime=\"{}\" maxSegmentDuration=\"{}\">",
xs_duration(Duration::from_secs(target)),
xs_duration(Duration::from_secs(target)),
);
let _ = writeln!(out, " <Period id=\"0\" start=\"PT0.000S\">");
render_adaptation_sets(&mut out, Kind::Video, &manifest.video, &suffix);
render_adaptation_sets(&mut out, Kind::Audio, &manifest.audio, &suffix);
let _ = writeln!(out, " </Period>");
let _ = writeln!(out, "</MPD>");
out
}
#[cfg(test)]
mod tests {
use super::*;
fn video(segments: Vec<(u64, u64)>, ended: bool) -> Representation {
Representation {
name: "video0".into(),
kind: Kind::Video,
bandwidth: 2_500_000,
codec: "avc1.42c01f".into(),
width: Some(1280),
height: Some(720),
framerate: Some(30.0),
sample_rate: None,
channel_count: None,
timescale: 1000,
segments,
ended,
}
}
fn audio(segments: Vec<(u64, u64)>, ended: bool) -> Representation {
Representation {
name: "audio0".into(),
kind: Kind::Audio,
bandwidth: 128_000,
codec: "mp4a.40.2".into(),
width: None,
height: None,
framerate: None,
sample_rate: Some(48_000),
channel_count: Some(2),
timescale: 1000,
segments,
ended,
}
}
#[test]
fn renders_dynamic_manifest() {
let manifest = Manifest {
availability_start: Some(SystemTime::UNIX_EPOCH + Duration::from_millis(1_751_846_400_123)),
publish: SystemTime::UNIX_EPOCH + Duration::from_millis(1_751_846_410_000),
window: Duration::from_secs(16),
finished: false,
video: vec![video(vec![(0, 2_000), (2_000, 2_000)], false)],
audio: vec![audio(vec![(0, 2_000), (2_000, 2_000)], false)],
};
let out = render_manifest(&manifest, None);
assert!(out.starts_with("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"));
assert!(out.contains(" type=\"dynamic\""));
assert!(out.contains(" availabilityStartTime=\"2025-07-07T00:00:00.123Z\""));
assert!(out.contains(" publishTime=\"2025-07-07T00:00:10.000Z\""));
assert!(out.contains(" timeShiftBufferDepth=\"PT16.000S\""));
assert!(out.contains(" minimumUpdatePeriod=\"PT2.000S\""));
assert!(out.contains(" suggestedPresentationDelay=\"PT6.000S\""));
assert!(out.contains(" maxSegmentDuration=\"PT2.000S\""));
assert!(out.contains(
"<AdaptationSet contentType=\"video\" mimeType=\"video/mp4\" segmentAlignment=\"true\" startWithSAP=\"1\">"
));
assert!(out.contains(
"<Representation id=\"video/video0\" bandwidth=\"2500000\" codecs=\"avc1.42c01f\" width=\"1280\" height=\"720\" frameRate=\"30\">"
));
assert!(out.contains(
"<Representation id=\"audio/audio0\" bandwidth=\"128000\" codecs=\"mp4a.40.2\" audioSamplingRate=\"48000\">"
));
assert!(out.contains(
"<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"2\"/>"
));
assert!(out.contains(
"<SegmentTemplate timescale=\"1000\" initialization=\"video/video0/init.mp4\" media=\"video/video0/seg/t$Time$.m4s\">"
));
assert!(out.contains("<S t=\"0\" d=\"2000\"/>"));
assert!(out.contains("<S t=\"2000\" d=\"2000\"/>"));
assert!(!out.contains("mediaPresentationDuration"));
}
#[test]
fn renders_static_manifest_when_finished() {
let manifest = Manifest {
availability_start: None,
publish: SystemTime::UNIX_EPOCH,
window: Duration::from_secs(16),
finished: true,
video: vec![video(vec![(10_000, 2_000), (12_000, 2_500)], true)],
audio: Vec::new(),
};
let out = render_manifest(&manifest, None);
assert!(out.contains(" type=\"static\""));
assert!(out.contains(" mediaPresentationDuration=\"PT14.500S\""));
assert!(out.contains(" maxSegmentDuration=\"PT3.000S\""));
assert!(!out.contains("presentationTimeOffset"));
assert!(!out.contains("availabilityStartTime"));
assert!(!out.contains("minimumUpdatePeriod"));
assert!(!out.contains("timeShiftBufferDepth"));
}
#[test]
fn query_rides_child_urls_escaped() {
let manifest = Manifest {
availability_start: Some(SystemTime::UNIX_EPOCH),
publish: SystemTime::UNIX_EPOCH,
window: Duration::from_secs(16),
finished: false,
video: vec![video(vec![(0, 2_000)], false)],
audio: Vec::new(),
};
let out = render_manifest(&manifest, Some("jwt=abc.def&x=1"));
assert!(out.contains("initialization=\"video/video0/init.mp4?jwt=abc.def&x=1\""));
assert!(out.contains("media=\"video/video0/seg/t$Time$.m4s?jwt=abc.def&x=1\""));
}
#[test]
fn fractional_frame_rate_renders_as_fraction() {
assert_eq!(frame_rate(30.0).as_deref(), Some("30"));
assert_eq!(frame_rate(29.97).as_deref(), Some("29970/1000"));
assert_eq!(frame_rate(0.0), None);
}
#[test]
fn empty_and_gap_only_representations_are_omitted() {
let manifest = Manifest {
availability_start: Some(SystemTime::UNIX_EPOCH),
publish: SystemTime::UNIX_EPOCH,
window: Duration::from_secs(16),
finished: false,
video: vec![video(vec![(0, 2_000)], false)],
audio: vec![audio(Vec::new(), false)],
};
let out = render_manifest(&manifest, None);
assert!(out.contains("id=\"video/video0\""));
assert!(!out.contains("id=\"audio/audio0\""), "an empty timeline is unplayable");
}
}