use std::fmt::Write;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
const VERSION: u32 = 6;
const VERSION_GAP: u32 = 8;
pub(crate) struct Snapshot {
pub target_duration: u64,
pub media_sequence: u64,
pub segments: Vec<Segment>,
pub finished: bool,
pub program_date_time: Option<SystemTime>,
pub generation: Option<Arc<str>>,
}
pub(crate) struct Segment {
pub segment: u64,
pub duration: Duration,
pub gap: bool,
pub discontinuity: bool,
}
pub(crate) fn render_media(snapshot: &Snapshot, init: &str, query: Option<&str>) -> String {
let suffix = query.map(|q| format!("?{q}")).unwrap_or_default();
let generation = snapshot
.generation
.as_deref()
.map(|g| format!("{g}."))
.unwrap_or_default();
let version = if snapshot.segments.iter().any(|s| s.gap) {
VERSION_GAP
} else {
VERSION
};
let mut out = String::new();
let _ = writeln!(out, "#EXTM3U");
let _ = writeln!(out, "#EXT-X-VERSION:{version}");
let _ = writeln!(out, "#EXT-X-TARGETDURATION:{}", snapshot.target_duration);
let _ = writeln!(out, "#EXT-X-MEDIA-SEQUENCE:{}", snapshot.media_sequence);
let _ = writeln!(out, "#EXT-X-MAP:URI=\"init.{init}.mp4{suffix}\"");
for (index, segment) in snapshot.segments.iter().enumerate() {
if index == 0
&& let Some(pdt) = snapshot.program_date_time
{
let _ = writeln!(
out,
"#EXT-X-PROGRAM-DATE-TIME:{}",
humantime::format_rfc3339_millis(pdt)
);
}
if segment.discontinuity {
let _ = writeln!(out, "#EXT-X-DISCONTINUITY");
}
if segment.gap {
let _ = writeln!(out, "#EXT-X-GAP");
}
let _ = writeln!(out, "#EXTINF:{:.5},", segment.duration.as_secs_f64());
let _ = writeln!(out, "seg/{generation}{}.m4s{suffix}", segment.segment);
}
if snapshot.finished {
let _ = writeln!(out, "#EXT-X-ENDLIST");
}
out
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn renders_live_playlist() {
let snapshot = Snapshot {
target_duration: 2,
media_sequence: 10,
segments: vec![
Segment {
segment: 10,
duration: Duration::from_secs(2),
gap: false,
discontinuity: false,
},
Segment {
segment: 11,
duration: Duration::from_millis(1960),
gap: false,
discontinuity: false,
},
],
finished: false,
program_date_time: Some(SystemTime::UNIX_EPOCH + Duration::from_millis(1_751_846_400_123)),
generation: None,
};
let out = render_media(&snapshot, "0123abcd", None);
assert!(out.starts_with("#EXTM3U\n#EXT-X-VERSION:6\n"));
assert!(out.contains("#EXT-X-TARGETDURATION:2\n"));
assert!(out.contains("#EXT-X-MEDIA-SEQUENCE:10\n"));
assert!(out.contains("#EXT-X-MAP:URI=\"init.0123abcd.mp4\"\n"));
assert!(out.contains("#EXT-X-PROGRAM-DATE-TIME:2025-07-07T00:00:00.123Z\n"));
assert!(out.contains("#EXTINF:2.00000,\nseg/10.m4s\n"));
assert!(out.contains("#EXTINF:1.96000,\nseg/11.m4s\n"));
assert!(!out.contains("#EXT-X-ENDLIST"));
let signed = render_media(&snapshot, "0123abcd", Some("jwt=abc.def"));
assert!(signed.contains("#EXT-X-MAP:URI=\"init.0123abcd.mp4?jwt=abc.def\"\n"));
assert!(signed.contains("\nseg/10.m4s?jwt=abc.def\n"));
assert!(signed.contains("\nseg/11.m4s?jwt=abc.def\n"));
}
#[test]
fn generation_rides_every_segment_url() {
let snapshot = Snapshot {
target_duration: 2,
media_sequence: 0,
segments: vec![Segment {
segment: 0,
duration: Duration::from_secs(2),
gap: false,
discontinuity: false,
}],
finished: false,
program_date_time: None,
generation: Some("run-7".into()),
};
let out = render_media(&snapshot, "0123abcd", Some("jwt=x"));
assert!(out.contains("#EXT-X-MAP:URI=\"init.0123abcd.mp4?jwt=x\"\n"));
assert!(out.contains("\nseg/run-7.0.m4s?jwt=x\n"));
}
#[test]
fn finished_playlist_has_endlist() {
let snapshot = Snapshot {
target_duration: 4,
media_sequence: 0,
segments: vec![Segment {
segment: 0,
duration: Duration::from_secs(4),
gap: false,
discontinuity: false,
}],
finished: true,
program_date_time: None,
generation: None,
};
let out = render_media(&snapshot, "0123abcd", None);
assert!(out.contains("#EXT-X-ENDLIST\n"));
assert!(!out.contains("PROGRAM-DATE-TIME"));
}
}