1use std::collections::VecDeque;
16use std::fmt::Write as _;
17use std::sync::Arc;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20use super::engine::{SegmentEngine, BANDWIDTH_HINT};
21use super::{ChunkSink, Muxer, Packager};
22use crate::traits::StorageBackend;
23use crate::{MediaFrame, Result};
24use async_trait::async_trait;
25use bytes::{BufMut, Bytes, BytesMut};
26
27#[derive(Debug, Clone, Copy)]
29struct TimelineEntry {
30 start_ms: u64,
31 dur_ms: u64,
32}
33
34#[derive(Debug, Clone)]
39pub struct DashManifest {
40 window: usize,
41 timescale: u64,
42 low_latency: bool,
43 part_target: f64,
44 availability_start: u64, segments: VecDeque<(u64, TimelineEntry)>, codecs: Option<String>,
47 width: u16,
48 height: u16,
49 bandwidth: u32,
50 init_uri: String,
51 media_template: String,
52 finished: bool,
53}
54
55impl DashManifest {
56 pub fn new(window: usize) -> Self {
58 Self {
59 window: window.max(1),
60 timescale: 1000,
61 low_latency: false,
62 part_target: 0.0,
63 availability_start: SystemTime::now()
64 .duration_since(UNIX_EPOCH)
65 .map(|d| d.as_secs())
66 .unwrap_or(0),
67 segments: VecDeque::new(),
68 codecs: None,
69 width: 0,
70 height: 0,
71 bandwidth: 0,
72 init_uri: "init.m4s".into(),
73 media_template: "seg$Number$.m4s".into(),
74 finished: false,
75 }
76 }
77
78 pub fn low_latency(mut self, part_target: f64) -> Self {
82 self.low_latency = true;
83 self.part_target = part_target.max(0.05);
84 self
85 }
86
87 pub fn set_media_info(
90 &mut self,
91 codecs: Option<String>,
92 width: u16,
93 height: u16,
94 bandwidth: u32,
95 ) {
96 self.codecs = codecs;
97 self.width = width;
98 self.height = height;
99 self.bandwidth = bandwidth;
100 }
101
102 pub fn set_uris(&mut self, init_uri: impl Into<String>, media_template: impl Into<String>) {
104 self.init_uri = init_uri.into();
105 self.media_template = media_template.into();
106 }
107
108 pub fn push(&mut self, seq: u64, start_ms: u64, dur_ms: u64) {
110 self.segments
111 .push_back((seq, TimelineEntry { start_ms, dur_ms }));
112 while self.segments.len() > self.window {
113 self.segments.pop_front();
114 }
115 }
116
117 pub fn set_last_duration(&mut self, dur_ms: u64) {
122 if let Some((_, t)) = self.segments.back_mut() {
123 t.dur_ms = dur_ms;
124 }
125 }
126
127 pub fn finish(&mut self) {
130 self.finished = true;
131 }
132
133 fn start_number(&self) -> u64 {
136 self.segments.front().map(|(seq, _)| *seq).unwrap_or(0)
137 }
138
139 fn max_seg_secs(&self) -> f64 {
141 self.segments
142 .iter()
143 .map(|(_, t)| t.dur_ms)
144 .max()
145 .unwrap_or(0) as f64
146 / 1000.0
147 }
148
149 pub fn render(&self) -> String {
151 let mut s = String::with_capacity(512 + self.segments.len() * 48);
152 s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
153
154 let mpd_type = if self.finished { "static" } else { "dynamic" };
155 let profile = "urn:mpeg:dash:profile:isoff-live:2011";
156 s.push_str("<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" ");
157 let _ = write!(s, "type=\"{mpd_type}\" profiles=\"{profile}\" ");
160 s.push_str("minBufferTime=\"PT1.5S\" ");
161 if !self.finished {
162 let _ = write!(
163 s,
164 "availabilityStartTime=\"{}\" ",
165 unix_to_iso8601(self.availability_start)
166 );
167 s.push_str("minimumUpdatePeriod=\"PT1S\" ");
168 }
169 let _ = writeln!(s, "maxSegmentDuration=\"PT{:.3}S\">", self.max_seg_secs());
170
171 s.push_str(" <Period id=\"0\" start=\"PT0S\">\n");
172 s.push_str(" <AdaptationSet mimeType=\"video/mp4\" segmentAlignment=\"true\" startWithSAP=\"1\">\n");
173 let _ = write!(
174 s,
175 " <Representation id=\"v0\" bandwidth=\"{}\"",
176 self.bandwidth
177 );
178 if self.width > 0 && self.height > 0 {
179 let _ = write!(s, " width=\"{}\" height=\"{}\"", self.width, self.height);
180 }
181 if let Some(codecs) = &self.codecs {
182 let _ = write!(s, " codecs=\"{codecs}\"");
183 }
184 s.push_str(">\n");
185
186 let _ = write!(
187 s,
188 " <SegmentTemplate timescale=\"{}\" initialization=\"{}\" media=\"{}\" startNumber=\"{}\"",
189 self.timescale,
190 self.init_uri,
191 self.media_template,
192 self.start_number()
193 );
194 if self.low_latency {
195 let _ = write!(
196 s,
197 " availabilityTimeOffset=\"{:.3}\" availabilityTimeComplete=\"false\"",
198 (self.max_seg_secs() - self.part_target).max(0.0)
199 );
200 }
201 s.push_str(">\n");
202 s.push_str(" <SegmentTimeline>\n");
203 for (i, (_, t)) in self.segments.iter().enumerate() {
207 if i == 0 {
208 let _ = writeln!(
209 s,
210 " <S t=\"{}\" d=\"{}\"/>",
211 t.start_ms, t.dur_ms
212 );
213 } else {
214 let _ = writeln!(s, " <S d=\"{}\"/>", t.dur_ms);
215 }
216 }
217 s.push_str(" </SegmentTimeline>\n");
218 s.push_str(" </SegmentTemplate>\n");
219 s.push_str(" </Representation>\n");
220 s.push_str(" </AdaptationSet>\n");
221 s.push_str(" </Period>\n");
222 s.push_str("</MPD>\n");
223 s
224 }
225}
226
227fn unix_to_iso8601(secs: u64) -> String {
230 let days = (secs / 86_400) as i64;
231 let rem = secs % 86_400;
232 let (h, m, sec) = (rem / 3600, (rem % 3600) / 60, rem % 60);
233
234 let z = days + 719_468;
235 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
236 let doe = z - era * 146_097;
237 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
238 let y = yoe + era * 400;
239 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
240 let mp = (5 * doy + 2) / 153;
241 let d = doy - (153 * mp + 2) / 5 + 1;
242 let month = if mp < 10 { mp + 3 } else { mp - 9 };
243 let year = if month <= 2 { y + 1 } else { y };
244 format!("{year:04}-{month:02}-{d:02}T{h:02}:{m:02}:{sec:02}Z")
245}
246
247pub struct DashPackager<M: Muxer, S: StorageBackend> {
251 engine: SegmentEngine<M, S>,
252 manifest: DashManifest,
253 seg_start_ms: u64,
254 part_target_ms: i64,
257 part_anchor_dts: Option<i64>,
259 last_dts: i64,
261 seg_part_bytes: Vec<Bytes>,
264 seg_dur_ms: u64,
268 last_dur_ms: u64,
271 sink: Option<Arc<dyn ChunkSink>>,
274}
275
276impl<M: Muxer, S: StorageBackend> DashPackager<M, S> {
277 pub fn new(
280 muxer: M,
281 storage: S,
282 prefix: impl Into<String>,
283 target_duration: u64,
284 window: usize,
285 ) -> Self {
286 Self {
287 engine: SegmentEngine::new(muxer, storage, prefix, target_duration),
288 manifest: DashManifest::new(window),
289 seg_start_ms: 0,
290 part_target_ms: 0,
291 part_anchor_dts: None,
292 last_dts: 0,
293 seg_part_bytes: Vec::new(),
294 seg_dur_ms: 0,
295 last_dur_ms: target_duration.max(1) * 1000,
296 sink: None,
297 }
298 }
299
300 pub fn low_latency(mut self, part_target: f64) -> Self {
305 self.manifest = self.manifest.low_latency(part_target);
306 self.part_target_ms = (part_target.max(0.05) * 1000.0) as i64;
307 self
308 }
309
310 pub fn with_chunk_sink(mut self, sink: Arc<dyn ChunkSink>) -> Self {
313 self.sink = Some(sink);
314 self
315 }
316
317 pub fn manifest_key(&self) -> String {
319 format!("{}/manifest.mpd", self.engine.prefix)
320 }
321
322 async fn ensure_init(&mut self, frame: &MediaFrame) -> Result<()> {
323 if let Some(uri) = self.engine.ensure_init(frame).await? {
324 self.manifest.set_uris(
325 uri,
326 format!("seg$Number$.{}", self.engine.muxer.extension()),
327 );
328 self.manifest
330 .set_media_info(self.engine.muxer.codec_string(), 0, 0, BANDWIDTH_HINT);
331 }
332 Ok(())
333 }
334
335 async fn flush_part(&mut self) -> Result<()> {
340 let Some(bytes) = self.engine.muxer.take_partial()? else {
341 return Ok(());
342 };
343 if let Some(d) = self.engine.muxer.last_segment_duration_ms() {
344 self.seg_dur_ms += d;
345 }
346 if let Some(sink) = &self.sink {
347 sink.chunk(self.engine.seq, bytes.clone());
348 }
349 self.seg_part_bytes.push(bytes);
350 self.part_anchor_dts = Some(self.last_dts);
351 Ok(())
352 }
353
354 async fn cut(&mut self, duration: f64) -> Result<()> {
355 if self.part_target_ms > 0 {
356 self.flush_part().await?;
359 let mut full = BytesMut::new();
360 for b in &self.seg_part_bytes {
361 full.put_slice(b);
362 }
363 let full = if full.is_empty() {
366 self.engine.muxer.finish_segment()?
367 } else {
368 full.freeze()
369 };
370 if full.is_empty() {
371 return Ok(());
372 }
373 let uri = self.engine.segment_uri(self.engine.seq);
374 let key = self.engine.key(&uri);
375 self.engine.storage.put(&key, full).await?;
376 let dur_ms = if self.seg_dur_ms > 0 {
379 self.seg_dur_ms
380 } else {
381 (duration * 1000.0) as u64
382 };
383 self.manifest.set_last_duration(dur_ms);
386 self.seg_start_ms += dur_ms;
387 self.last_dur_ms = dur_ms;
388 self.write_manifest().await?;
389 if let Some(sink) = &self.sink {
390 sink.close(self.engine.seq);
391 }
392 self.seg_part_bytes.clear();
393 self.seg_dur_ms = 0;
394 self.engine.seq += 1;
395 return Ok(());
396 }
397
398 let bytes = self.engine.muxer.finish_segment()?;
399 if bytes.is_empty() {
400 return Ok(());
401 }
402 let uri = self.engine.segment_uri(self.engine.seq);
403 let key = self.engine.key(&uri);
404 self.engine.storage.put(&key, bytes).await?;
405 let dur_ms = self
410 .engine
411 .muxer
412 .last_segment_duration_ms()
413 .filter(|&d| d > 0)
414 .unwrap_or((duration * 1000.0) as u64);
415 self.manifest
416 .push(self.engine.seq, self.seg_start_ms, dur_ms);
417 self.seg_start_ms += dur_ms;
418 self.write_manifest().await?;
419 self.engine.seq += 1;
420 Ok(())
421 }
422
423 async fn write_manifest(&mut self) -> Result<()> {
424 let body = self.manifest.render();
425 self.engine
426 .storage
427 .put(
428 &self.engine.key("manifest.mpd"),
429 Bytes::from(body.into_bytes()),
430 )
431 .await
432 }
433}
434
435#[async_trait]
436impl<M: Muxer, S: StorageBackend> Packager for DashPackager<M, S> {
437 async fn push(&mut self, frame: &MediaFrame) -> Result<()> {
438 self.ensure_init(frame).await?;
439 let decision = self.engine.observe(frame);
440 if decision.skip {
441 return Ok(());
442 }
443 if let Some(duration) = decision.cut_previous {
444 self.cut(duration).await?;
445 }
446 if decision.open_new {
447 self.engine.muxer.start_segment()?;
448 self.part_anchor_dts = Some(frame.dts);
451 self.seg_part_bytes.clear();
452 self.seg_dur_ms = 0;
453 if let Some(sink) = &self.sink {
454 sink.open(self.engine.seq);
455 }
456 if self.part_target_ms > 0 {
462 self.manifest
463 .push(self.engine.seq, self.seg_start_ms, self.last_dur_ms);
464 self.write_manifest().await?;
465 }
466 }
467 self.engine.muxer.write(frame)?;
468 self.last_dts = frame.dts;
469
470 if self.part_target_ms > 0 {
473 if let Some(anchor) = self.part_anchor_dts {
474 if frame.dts - anchor >= self.part_target_ms {
475 self.flush_part().await?;
476 }
477 }
478 }
479 Ok(())
480 }
481
482 async fn finish(&mut self) -> Result<()> {
483 if let Some(duration) = self.engine.flush() {
484 self.cut(duration).await?;
485 }
486 self.manifest.finish();
487 self.write_manifest().await
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::packager::PassthroughMuxer;
495 use crate::testing::{video_frame, InMemoryStorage};
496
497 #[test]
498 fn iso8601_known_vector() {
499 assert_eq!(unix_to_iso8601(0), "1970-01-01T00:00:00Z");
501 assert_eq!(unix_to_iso8601(1_609_459_200), "2021-01-01T00:00:00Z");
502 assert_eq!(
503 unix_to_iso8601(1_609_459_200 + 3661),
504 "2021-01-01T01:01:01Z"
505 );
506 }
507
508 #[test]
509 fn manifest_renders_dynamic_mpd_with_timeline() {
510 let mut m = DashManifest::new(3);
511 m.set_media_info(Some("avc1.42001f".into()), 1280, 720, 2_000_000);
512 m.push(0, 0, 2000);
513 m.push(1, 2000, 1980);
514 let out = m.render();
515 assert!(out.contains("type=\"dynamic\""));
516 assert!(out.contains("availabilityStartTime="));
517 assert!(out.contains("<SegmentTimeline>"));
518 assert!(out.contains("<S t=\"0\" d=\"2000\"/>"));
519 assert!(out.contains("<S d=\"1980\"/>"));
520 assert!(out.contains("codecs=\"avc1.42001f\""));
521 assert!(out.contains("width=\"1280\" height=\"720\""));
522 }
523
524 #[test]
525 fn low_latency_emits_availability_time_offset() {
526 let mut m = DashManifest::new(3).low_latency(0.5);
527 m.push(0, 0, 2000);
528 let out = m.render();
529 assert!(out.contains("availabilityTimeOffset="));
530 assert!(out.contains("availabilityTimeComplete=\"false\""));
531 }
532
533 #[test]
534 fn finish_marks_static() {
535 let mut m = DashManifest::new(3);
536 m.push(0, 0, 2000);
537 m.finish();
538 let out = m.render();
539 assert!(out.contains("type=\"static\""));
540 assert!(!out.contains("minimumUpdatePeriod"));
541 }
542
543 #[tokio::test]
544 async fn ll_dash_streams_fragments_through_sink() {
545 use std::sync::Mutex;
546
547 #[derive(Default)]
548 struct CollectSink {
549 chunks: Mutex<Vec<(u64, Vec<u8>)>>,
550 closed: Mutex<Vec<u64>>,
551 }
552 impl ChunkSink for CollectSink {
553 fn open(&self, _seq: u64) {}
554 fn chunk(&self, seq: u64, bytes: Bytes) {
555 self.chunks.lock().unwrap().push((seq, bytes.to_vec()));
556 }
557 fn close(&self, seq: u64) {
558 self.closed.lock().unwrap().push(seq);
559 }
560 }
561
562 let store = InMemoryStorage::new();
563 let sink = Arc::new(CollectSink::default());
564 let mut dash = DashPackager::new(
565 PassthroughMuxer::new("m4s"),
566 store.clone(),
567 "live/dash",
568 2,
569 5,
570 )
571 .low_latency(0.5)
572 .with_chunk_sink(sink.clone());
573
574 dash.push(&video_frame(0, true)).await.unwrap();
577 for i in 1..8 {
578 dash.push(&video_frame(i * 250, false)).await.unwrap();
579 }
580 dash.push(&video_frame(2000, true)).await.unwrap(); dash.push(&video_frame(2250, false)).await.unwrap();
582 dash.finish().await.unwrap();
583
584 let (seg0_count, concat) = {
587 let chunks = sink.chunks.lock().unwrap();
588 let seg0: Vec<&Vec<u8>> = chunks
589 .iter()
590 .filter(|(s, _)| *s == 0)
591 .map(|(_, b)| b)
592 .collect();
593 let mut concat = Vec::new();
594 for b in &seg0 {
595 concat.extend_from_slice(b);
596 }
597 (seg0.len(), concat)
598 };
599 assert!(seg0_count >= 2, "segment 0 streamed in multiple fragments");
600 assert!(sink.closed.lock().unwrap().contains(&0), "segment 0 closed");
601
602 let full = store.get("live/dash/seg0.m4s").await.unwrap();
604 assert_eq!(
605 &concat[..],
606 &full[..],
607 "streamed fragments == full segment file"
608 );
609 }
610
611 #[tokio::test]
612 async fn packager_writes_segments_and_mpd() {
613 let store = InMemoryStorage::new();
614 let mut dash = DashPackager::new(
615 PassthroughMuxer::new("m4s"),
616 store.clone(),
617 "live/dash",
618 2,
619 5,
620 );
621
622 for i in 0..4 {
623 let pts = i * 1000;
624 dash.push(&video_frame(pts, true)).await.unwrap();
625 dash.push(&video_frame(pts + 500, false)).await.unwrap();
626 }
627 dash.finish().await.unwrap();
628
629 let mpd =
630 String::from_utf8(store.get("live/dash/manifest.mpd").await.unwrap().to_vec()).unwrap();
631 assert!(mpd.contains("<MPD"));
632 assert!(mpd.contains("type=\"static\"")); assert!(mpd.contains("<SegmentTimeline>"));
634 assert!(store.get("live/dash/seg0.m4s").await.is_ok());
635 }
636}