arcly_stream/packager/
playlist.rs1use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
7pub struct Segment {
8 pub seq: u64,
10 pub duration: f64,
12 pub uri: String,
14 pub discontinuity: bool,
16}
17
18#[derive(Debug, Clone)]
24pub struct HlsPlaylist {
25 target_duration: u64,
26 window: usize,
27 low_latency: bool,
28 part_target: f64,
29 media_sequence: u64,
30 discontinuity_sequence: u64,
31 segments: VecDeque<Segment>,
32 map_uri: Option<String>,
33 finished: bool,
34}
35
36impl HlsPlaylist {
37 pub fn new(target_duration: u64, window: usize) -> Self {
39 Self {
40 target_duration,
41 window: window.max(1),
42 low_latency: false,
43 part_target: 0.0,
44 media_sequence: 0,
45 discontinuity_sequence: 0,
46 segments: VecDeque::new(),
47 map_uri: None,
48 finished: false,
49 }
50 }
51
52 pub fn low_latency(mut self, part_target: f64) -> Self {
54 self.low_latency = true;
55 self.part_target = part_target;
56 self
57 }
58
59 pub fn set_map(&mut self, uri: impl Into<String>) {
62 self.map_uri = Some(uri.into());
63 }
64
65 pub fn push(&mut self, seg: Segment) {
67 if self.segments.is_empty() {
68 self.media_sequence = seg.seq;
69 }
70 self.segments.push_back(seg);
71 while self.segments.len() > self.window {
72 if let Some(old) = self.segments.pop_front() {
73 if old.discontinuity {
74 self.discontinuity_sequence += 1;
75 }
76 self.media_sequence = self.segments.front().map(|s| s.seq).unwrap_or(old.seq + 1);
77 }
78 }
79 }
80
81 pub fn finish(&mut self) {
83 self.finished = true;
84 }
85
86 pub fn segments(&self) -> &VecDeque<Segment> {
88 &self.segments
89 }
90
91 pub fn render(&self) -> String {
93 let mut s = String::with_capacity(256 + self.segments.len() * 64);
94 s.push_str("#EXTM3U\n");
95 s.push_str("#EXT-X-VERSION:");
96 s.push_str(if self.low_latency { "9\n" } else { "3\n" });
97 s.push_str(&format!("#EXT-X-TARGETDURATION:{}\n", self.target_duration));
98 s.push_str(&format!("#EXT-X-MEDIA-SEQUENCE:{}\n", self.media_sequence));
99 if self.discontinuity_sequence > 0 {
100 s.push_str(&format!(
101 "#EXT-X-DISCONTINUITY-SEQUENCE:{}\n",
102 self.discontinuity_sequence
103 ));
104 }
105 if self.low_latency {
106 s.push_str(&format!(
107 "#EXT-X-PART-INF:PART-TARGET={:.3}\n",
108 self.part_target
109 ));
110 s.push_str("#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n");
111 }
112 if let Some(uri) = &self.map_uri {
113 s.push_str(&format!("#EXT-X-MAP:URI=\"{uri}\"\n"));
114 }
115 for seg in &self.segments {
116 if seg.discontinuity {
117 s.push_str("#EXT-X-DISCONTINUITY\n");
118 }
119 s.push_str(&format!("#EXTINF:{:.3},\n", seg.duration));
120 s.push_str(&seg.uri);
121 s.push('\n');
122 }
123 if self.finished {
124 s.push_str("#EXT-X-ENDLIST\n");
125 }
126 s
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn seg(seq: u64, dur: f64) -> Segment {
135 Segment {
136 seq,
137 duration: dur,
138 uri: format!("seg{seq}.m4s"),
139 discontinuity: false,
140 }
141 }
142
143 #[test]
144 fn renders_live_playlist_with_sliding_window() {
145 let mut pl = HlsPlaylist::new(6, 3);
146 for i in 0..5 {
147 pl.push(seg(i, 5.0));
148 }
149 assert_eq!(pl.segments().len(), 3);
151 let out = pl.render();
152 assert!(out.starts_with("#EXTM3U\n"));
153 assert!(out.contains("#EXT-X-TARGETDURATION:6\n"));
154 assert!(out.contains("#EXT-X-MEDIA-SEQUENCE:2\n"));
155 assert!(out.contains("seg4.m4s"));
156 assert!(!out.contains("seg1.m4s")); assert!(!out.contains("#EXT-X-ENDLIST"));
158 }
159
160 #[test]
161 fn finish_appends_endlist() {
162 let mut pl = HlsPlaylist::new(6, 5);
163 pl.push(seg(0, 4.0));
164 pl.finish();
165 assert!(pl.render().trim_end().ends_with("#EXT-X-ENDLIST"));
166 }
167
168 #[test]
169 fn low_latency_emits_part_tags() {
170 let pl = HlsPlaylist::new(4, 5).low_latency(0.33);
171 let out = pl.render();
172 assert!(out.contains("#EXT-X-VERSION:9"));
173 assert!(out.contains("PART-TARGET=0.330"));
174 assert!(out.contains("CAN-BLOCK-RELOAD=YES"));
175 }
176}