use std::fmt::Write;
use super::store::Snapshot;
const VERSION: u32 = 9;
pub fn render_media(snapshot: &Snapshot) -> String {
let part_hold_back = snapshot.part_target * 3.0;
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-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK={part_hold_back:.3}"
);
let _ = writeln!(out, "#EXT-X-PART-INF:PART-TARGET={:.3}", snapshot.part_target);
let _ = writeln!(out, "#EXT-X-MEDIA-SEQUENCE:{}", snapshot.media_sequence);
if snapshot.discontinuity_sequence > 0 {
let _ = writeln!(out, "#EXT-X-DISCONTINUITY-SEQUENCE:{}", snapshot.discontinuity_sequence);
}
let _ = writeln!(out, "#EXT-X-MAP:URI=\"init.mp4\"");
for segment in &snapshot.segments {
if segment.discontinuity {
let _ = writeln!(out, "#EXT-X-DISCONTINUITY");
}
for (index, part) in segment.parts.iter().enumerate() {
let independent = if part.independent { ",INDEPENDENT=YES" } else { "" };
let _ = writeln!(
out,
"#EXT-X-PART:DURATION={:.5},URI=\"part/{}/{}.m4s\"{}",
part.duration, segment.sequence, index, independent
);
}
if segment.complete {
let _ = writeln!(out, "#EXTINF:{:.5},", segment.duration);
let _ = writeln!(out, "seg/{}.m4s", segment.sequence);
}
}
if snapshot.finished {
let _ = writeln!(out, "#EXT-X-ENDLIST");
} else {
let (sequence, index) = match snapshot.segments.last() {
Some(last) if !last.complete => (last.sequence, last.parts.len()),
_ => (snapshot.next_sequence, 0),
};
let _ = writeln!(out, "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"part/{sequence}/{index}.m4s\"");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::export::store::{PartMeta, SegmentMeta};
fn part(duration: f64, independent: bool) -> PartMeta {
PartMeta { duration, independent }
}
#[test]
fn renders_ll_hls_tags() {
let snapshot = Snapshot {
init_ready: true,
part_target: 0.5,
target_duration: 1,
media_sequence: 10,
discontinuity_sequence: 0,
next_sequence: 12,
segments: vec![
SegmentMeta {
sequence: 10,
parts: vec![part(0.5, true), part(0.5, false)],
duration: 1.0,
complete: true,
discontinuity: false,
},
SegmentMeta {
sequence: 11,
parts: vec![part(0.5, true)],
duration: 0.5,
complete: false,
discontinuity: false,
},
],
finished: false,
};
let out = render_media(&snapshot);
assert!(out.starts_with("#EXTM3U\n#EXT-X-VERSION:9\n"));
assert!(!out.contains("#EXT-X-DISCONTINUITY"));
assert!(out.contains("#EXT-X-TARGETDURATION:1\n"));
assert!(out.contains("PART-HOLD-BACK=1.500"));
assert!(out.contains("CAN-BLOCK-RELOAD=YES"));
assert!(out.contains("#EXT-X-PART-INF:PART-TARGET=0.500\n"));
assert!(out.contains("#EXT-X-MEDIA-SEQUENCE:10\n"));
assert!(out.contains("#EXT-X-MAP:URI=\"init.mp4\"\n"));
assert!(out.contains("#EXT-X-PART:DURATION=0.50000,URI=\"part/10/0.m4s\",INDEPENDENT=YES\n"));
assert!(out.contains("#EXT-X-PART:DURATION=0.50000,URI=\"part/10/1.m4s\"\n"));
assert!(!out.contains("part/10/1.m4s\",INDEPENDENT"));
assert!(out.contains("#EXTINF:1.00000,\nseg/10.m4s\n"));
assert!(out.contains("#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"part/11/1.m4s\"\n"));
assert!(!out.contains("#EXT-X-ENDLIST"));
}
#[test]
fn finished_playlist_has_endlist_and_no_preload() {
let snapshot = Snapshot {
init_ready: true,
part_target: 1.0,
target_duration: 1,
media_sequence: 0,
discontinuity_sequence: 0,
next_sequence: 1,
segments: vec![SegmentMeta {
sequence: 0,
parts: vec![part(1.0, true)],
duration: 1.0,
complete: true,
discontinuity: false,
}],
finished: true,
};
let out = render_media(&snapshot);
assert!(out.contains("#EXT-X-ENDLIST\n"));
assert!(!out.contains("#EXT-X-PRELOAD-HINT"));
}
#[test]
fn discontinuity_precedes_resumed_segment() {
let snapshot = Snapshot {
init_ready: true,
part_target: 1.0,
target_duration: 1,
media_sequence: 0,
discontinuity_sequence: 0,
next_sequence: 2,
segments: vec![
SegmentMeta {
sequence: 0,
parts: vec![part(1.0, true)],
duration: 1.0,
complete: true,
discontinuity: false,
},
SegmentMeta {
sequence: 1,
parts: vec![part(1.0, true)],
duration: 1.0,
complete: true,
discontinuity: true,
},
],
finished: false,
};
let out = render_media(&snapshot);
let disc = out.find("#EXT-X-DISCONTINUITY").expect("discontinuity tag");
let seg0 = out.find("part/0/0.m4s").expect("seg 0 part");
let seg1 = out.find("part/1/0.m4s").expect("seg 1 part");
assert!(
seg0 < disc && disc < seg1,
"discontinuity must sit between seg 0 and seg 1"
);
}
#[test]
fn emits_discontinuity_sequence_when_nonzero() {
let snapshot = Snapshot {
init_ready: true,
part_target: 1.0,
target_duration: 6,
media_sequence: 8,
discontinuity_sequence: 2,
next_sequence: 9,
segments: vec![SegmentMeta {
sequence: 8,
parts: vec![part(1.0, true)],
duration: 1.0,
complete: true,
discontinuity: false,
}],
finished: false,
};
let out = render_media(&snapshot);
assert!(out.contains("#EXT-X-TARGETDURATION:6\n"));
assert!(out.contains("#EXT-X-DISCONTINUITY-SEQUENCE:2\n"));
}
}