1use sheathe_core::{MediaKind, StreamInfo};
10use std::fmt::Write as _;
11
12#[derive(Debug, Clone)]
14pub enum SegmentAddressing {
15 Template {
17 init: String,
19 media: String,
21 start_number: u32,
23 presentation_time_offset: u64,
25 availability_time_offset: Option<f64>,
27 },
28 Base(SegmentBaseInfo),
33}
34
35#[derive(Debug, Clone)]
37pub struct SegmentBaseInfo {
38 pub base_url: String,
40 pub init_range: (u64, u64),
42 pub index_range: Option<(u64, u64)>,
44 pub media_ranges: Vec<(u64, u64)>,
48}
49
50#[derive(Debug, Clone)]
52pub struct Representation {
53 pub id: String,
55 pub stream: StreamInfo,
57 pub timescale: u32,
59 pub segment_durations: Vec<u64>,
61 pub addressing: SegmentAddressing,
63 pub max_playout_rate: Option<f64>,
65}
66
67impl Representation {
68 pub fn new(
70 id: impl Into<String>,
71 stream: StreamInfo,
72 init: impl Into<String>,
73 media: impl Into<String>,
74 timescale: u32,
75 segment_durations: Vec<u64>,
76 ) -> Self {
77 Self {
78 id: id.into(),
79 stream,
80 timescale,
81 segment_durations,
82 addressing: SegmentAddressing::Template {
83 init: init.into(),
84 media: media.into(),
85 start_number: 1,
86 presentation_time_offset: 0,
87 availability_time_offset: None,
88 },
89 max_playout_rate: None,
90 }
91 }
92
93 pub fn on_demand(
95 id: impl Into<String>,
96 stream: StreamInfo,
97 timescale: u32,
98 segment_durations: Vec<u64>,
99 base: SegmentBaseInfo,
100 ) -> Self {
101 Self {
102 id: id.into(),
103 stream,
104 timescale,
105 segment_durations,
106 addressing: SegmentAddressing::Base(base),
107 max_playout_rate: None,
108 }
109 }
110
111 pub fn init(&self) -> Option<&str> {
113 match &self.addressing {
114 SegmentAddressing::Template { init, .. } => Some(init.as_str()),
115 SegmentAddressing::Base(_) => None,
116 }
117 }
118
119 pub fn set_start_number(&mut self, n: u32) {
121 if let SegmentAddressing::Template { start_number, .. } = &mut self.addressing {
122 *start_number = n;
123 }
124 }
125
126 pub fn set_presentation_time_offset(&mut self, pto: u64) {
128 if let SegmentAddressing::Template { presentation_time_offset, .. } = &mut self.addressing {
129 *presentation_time_offset = pto;
130 }
131 }
132
133 pub fn set_availability_time_offset(&mut self, ato: Option<f64>) {
135 if let SegmentAddressing::Template { availability_time_offset, .. } = &mut self.addressing {
136 *availability_time_offset = ato;
137 }
138 }
139}
140
141#[derive(Debug, Clone)]
143pub struct Protection {
144 pub scheme: String,
146 pub default_kid: [u8; 16],
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum MpdType {
153 #[default]
155 Static,
156 Dynamic,
158}
159
160#[derive(Debug, Clone)]
162pub struct UtcTiming {
163 pub scheme_id_uri: String,
165 pub value: String,
167}
168
169impl UtcTiming {
170 pub fn http_iso(url: impl Into<String>) -> Self {
172 Self { scheme_id_uri: "urn:mpeg:dash:utc:http-iso:2014".into(), value: url.into() }
173 }
174
175 pub fn direct(iso_time: impl Into<String>) -> Self {
177 Self { scheme_id_uri: "urn:mpeg:dash:utc:direct:2014".into(), value: iso_time.into() }
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct DashEvent {
184 pub id: Option<String>,
186 pub presentation_time: u64,
188 pub duration: Option<u64>,
190 pub message_data: Option<String>,
192}
193
194#[derive(Debug, Clone)]
196pub struct EventStream {
197 pub scheme_id_uri: String,
199 pub value: Option<String>,
201 pub timescale: u32,
203 pub events: Vec<DashEvent>,
205}
206
207impl EventStream {
208 pub fn scte35_bin(timescale: u32, events: Vec<DashEvent>) -> Self {
210 Self {
211 scheme_id_uri: "urn:scte:scte35:2014:xml+bin".into(),
212 value: None,
213 timescale,
214 events,
215 }
216 }
217}
218
219#[derive(Debug, Clone)]
221pub struct Period {
222 pub id: String,
224 pub start_seconds: Option<f64>,
226 pub duration_seconds: Option<f64>,
228 pub representations: Vec<Representation>,
230 pub event_streams: Vec<EventStream>,
232}
233
234impl Period {
235 pub fn single(
237 duration_seconds: Option<f64>,
238 representations: Vec<Representation>,
239 event_streams: Vec<EventStream>,
240 ) -> Self {
241 Self {
242 id: "0".into(),
243 start_seconds: Some(0.0),
244 duration_seconds,
245 representations,
246 event_streams,
247 }
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
253pub enum DashProfile {
254 #[default]
256 Live,
257 OnDemand,
259}
260
261#[derive(Debug, Clone)]
263pub struct Manifest {
264 pub mpd_type: MpdType,
266 pub profile: DashProfile,
268 pub duration_seconds: Option<f64>,
270 pub availability_start_time: Option<String>,
272 pub publish_time: Option<String>,
274 pub minimum_update_period: Option<f64>,
276 pub time_shift_buffer_depth: Option<f64>,
278 pub suggested_presentation_delay: Option<f64>,
280 pub utc_timing: Option<UtcTiming>,
282 pub periods: Vec<Period>,
284 pub protection: Option<Protection>,
286}
287
288impl Default for Manifest {
289 fn default() -> Self {
290 Self {
291 mpd_type: MpdType::Static,
292 profile: DashProfile::Live,
293 duration_seconds: None,
294 availability_start_time: None,
295 publish_time: None,
296 minimum_update_period: None,
297 time_shift_buffer_depth: None,
298 suggested_presentation_delay: None,
299 utc_timing: None,
300 periods: Vec::new(),
301 protection: None,
302 }
303 }
304}
305
306impl Manifest {
307 pub fn static_vod(
312 duration_seconds: f64,
313 representations: Vec<Representation>,
314 protection: Option<Protection>,
315 ) -> Self {
316 Self {
317 mpd_type: MpdType::Static,
318 duration_seconds: Some(duration_seconds),
319 periods: vec![Period::single(Some(duration_seconds), representations, Vec::new())],
320 protection,
321 ..Self::default()
322 }
323 }
324
325 pub fn dynamic_live(
327 availability_start_time: impl Into<String>,
328 publish_time: impl Into<String>,
329 time_shift_buffer_depth: f64,
330 minimum_update_period: f64,
331 suggested_presentation_delay: f64,
332 representations: Vec<Representation>,
333 protection: Option<Protection>,
334 ) -> Self {
335 Self {
336 mpd_type: MpdType::Dynamic,
337 availability_start_time: Some(availability_start_time.into()),
338 publish_time: Some(publish_time.into()),
339 minimum_update_period: Some(minimum_update_period),
340 time_shift_buffer_depth: Some(time_shift_buffer_depth),
341 suggested_presentation_delay: Some(suggested_presentation_delay),
342 utc_timing: Some(UtcTiming::http_iso("https://time.akamai.com/?iso")),
343 periods: vec![Period::single(None, representations, Vec::new())],
344 protection,
345 ..Self::default()
346 }
347 }
348
349 pub fn to_xml(&self) -> String {
351 let mut s = String::new();
352 s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
353 let cenc_ns =
354 if self.protection.is_some() { " xmlns:cenc=\"urn:mpeg:cenc:2013\"" } else { "" };
355 let type_str = match self.mpd_type {
356 MpdType::Static => "static",
357 MpdType::Dynamic => "dynamic",
358 };
359 let profile_uri = match self.profile {
360 DashProfile::Live => "urn:mpeg:dash:profile:isoff-live:2011",
361 DashProfile::OnDemand => "urn:mpeg:dash:profile:isoff-on-demand:2011",
362 };
363 let _ = write!(
364 s,
365 concat!(
366 "<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\"{} ",
367 "profiles=\"{}\" ",
368 "type=\"{}\" minBufferTime=\"PT2S\""
369 ),
370 cenc_ns, profile_uri, type_str,
371 );
372 if let Some(d) = self.duration_seconds {
373 let _ = write!(s, " mediaPresentationDuration=\"{}\"", iso8601_duration(d));
374 }
375 if let Some(t) = &self.availability_start_time {
376 let _ = write!(s, " availabilityStartTime=\"{}\"", xml_escape(t));
377 }
378 if let Some(t) = &self.publish_time {
379 let _ = write!(s, " publishTime=\"{}\"", xml_escape(t));
380 }
381 if let Some(p) = self.minimum_update_period {
382 let _ = write!(s, " minimumUpdatePeriod=\"{}\"", iso8601_duration(p));
383 }
384 if let Some(d) = self.time_shift_buffer_depth {
385 let _ = write!(s, " timeShiftBufferDepth=\"{}\"", iso8601_duration(d));
386 }
387 if let Some(d) = self.suggested_presentation_delay {
388 let _ = write!(s, " suggestedPresentationDelay=\"{}\"", iso8601_duration(d));
389 }
390 s.push_str(">\n");
391
392 if let Some(utc) = &self.utc_timing {
393 let _ = writeln!(
394 s,
395 " <UTCTiming schemeIdUri=\"{}\" value=\"{}\"/>",
396 xml_escape(&utc.scheme_id_uri),
397 xml_escape(&utc.value),
398 );
399 }
400
401 for period in &self.periods {
402 render_period(&mut s, period, self.protection.as_ref());
403 }
404
405 s.push_str("</MPD>\n");
406 s
407 }
408}
409
410fn render_period(s: &mut String, period: &Period, protection: Option<&Protection>) {
411 let _ = write!(s, " <Period id=\"{}\"", xml_escape(&period.id));
412 if let Some(start) = period.start_seconds {
413 let _ = write!(s, " start=\"{}\"", iso8601_duration(start));
414 }
415 if let Some(dur) = period.duration_seconds {
416 let _ = write!(s, " duration=\"{}\"", iso8601_duration(dur));
417 }
418 s.push_str(">\n");
419
420 for es in &period.event_streams {
421 render_event_stream(s, es);
422 }
423
424 for (kind, content_type) in
427 [(MediaKind::Video, "video"), (MediaKind::Audio, "audio"), (MediaKind::Text, "text")]
428 {
429 let normal: Vec<&Representation> = period
430 .representations
431 .iter()
432 .filter(|r| r.stream.kind == kind && r.max_playout_rate.is_none())
433 .collect();
434 if !normal.is_empty() {
435 render_adaptation_set(s, content_type, &normal, protection, false);
436 }
437
438 if kind == MediaKind::Video {
439 let trick: Vec<&Representation> = period
440 .representations
441 .iter()
442 .filter(|r| r.stream.kind == MediaKind::Video && r.max_playout_rate.is_some())
443 .collect();
444 if !trick.is_empty() {
445 render_adaptation_set(s, "video", &trick, protection, true);
446 }
447 }
448 }
449
450 s.push_str(" </Period>\n");
451}
452
453fn render_adaptation_set(
454 s: &mut String,
455 content_type: &str,
456 reps: &[&Representation],
457 protection: Option<&Protection>,
458 trick_play: bool,
459) {
460 let _ = writeln!(
461 s,
462 " <AdaptationSet contentType=\"{}\" segmentAlignment=\"true\">",
463 content_type
464 );
465 if trick_play {
466 s.push_str(
470 " <EssentialProperty schemeIdUri=\"http://dashif.org/guidelines/trickmode\" value=\"1\"/>\n",
471 );
472 }
473 if let Some(p) = protection {
474 render_content_protection(s, p);
475 }
476 for r in reps {
477 render_representation(s, r);
478 }
479 s.push_str(" </AdaptationSet>\n");
480}
481
482fn render_content_protection(s: &mut String, p: &Protection) {
483 let _ = writeln!(
484 s,
485 concat!(
486 " <ContentProtection ",
487 "schemeIdUri=\"urn:mpeg:dash:mp4protection:2011\" ",
488 "value=\"{}\" cenc:default_KID=\"{}\"/>"
489 ),
490 p.scheme,
491 kid_uuid(&p.default_kid),
492 );
493}
494
495fn render_event_stream(s: &mut String, es: &EventStream) {
496 let _ = write!(
497 s,
498 " <EventStream schemeIdUri=\"{}\" timescale=\"{}\"",
499 xml_escape(&es.scheme_id_uri),
500 es.timescale
501 );
502 if let Some(v) = &es.value {
503 let _ = write!(s, " value=\"{}\"", xml_escape(v));
504 }
505 s.push_str(">\n");
506 for ev in &es.events {
507 let _ = write!(s, " <Event presentationTime=\"{}\"", ev.presentation_time);
508 if let Some(d) = ev.duration {
509 let _ = write!(s, " duration=\"{d}\"");
510 }
511 if let Some(id) = &ev.id {
512 let _ = write!(s, " id=\"{}\"", xml_escape(id));
513 }
514 if let Some(msg) = &ev.message_data {
515 let _ = writeln!(s, ">{}</Event>", xml_escape(msg));
516 } else {
517 s.push_str("/>\n");
518 }
519 }
520 s.push_str(" </EventStream>\n");
521}
522
523fn kid_uuid(kid: &[u8; 16]) -> String {
525 let h: String = kid.iter().map(|b| format!("{b:02x}")).collect();
526 format!("{}-{}-{}-{}-{}", &h[0..8], &h[8..12], &h[12..16], &h[16..20], &h[20..32])
527}
528
529fn render_representation(s: &mut String, r: &Representation) {
530 let codec = r.stream.rfc6381();
531 let bandwidth = r.stream.bitrate.unwrap_or(0);
532 let _ = write!(s, " <Representation id=\"{}\" codecs=\"{}\"", r.id, codec);
533 if let Some((w, h)) = r.stream.resolution {
534 let _ = write!(s, " width=\"{}\" height=\"{}\"", w, h);
535 }
536 if let Some(rate) = r.stream.sample_rate {
537 let _ = write!(s, " audioSamplingRate=\"{}\"", rate);
538 }
539 if let Some(mpr) = r.max_playout_rate {
540 let _ = write!(s, " maxPlayoutRate=\"{mpr}\"");
541 }
542 let _ = writeln!(s, " bandwidth=\"{}\">", bandwidth);
543
544 match &r.addressing {
545 SegmentAddressing::Template {
546 init,
547 media,
548 start_number,
549 presentation_time_offset,
550 availability_time_offset,
551 } => {
552 let _ = write!(
553 s,
554 " <SegmentTemplate timescale=\"{}\" initialization=\"{}\" media=\"{}\" startNumber=\"{}\"",
555 r.timescale, init, media, start_number
556 );
557 if *presentation_time_offset > 0 {
558 let _ = write!(s, " presentationTimeOffset=\"{presentation_time_offset}\"");
559 }
560 if let Some(ato) = availability_time_offset {
561 let _ = write!(
562 s,
563 " availabilityTimeOffset=\"{ato}\" availabilityTimeComplete=\"false\""
564 );
565 }
566 s.push_str(">\n");
567 s.push_str(" <SegmentTimeline>\n");
568 render_timeline(s, &r.segment_durations, *presentation_time_offset);
569 s.push_str(" </SegmentTimeline>\n");
570 s.push_str(" </SegmentTemplate>\n");
571 }
572 SegmentAddressing::Base(base) => {
573 let _ = writeln!(s, " <BaseURL>{}</BaseURL>", xml_escape(&base.base_url));
574 if base.media_ranges.is_empty() {
575 let _ = write!(s, " <SegmentBase timescale=\"{}\"", r.timescale);
577 if let Some((a, b)) = base.index_range {
578 let _ = write!(s, " indexRange=\"{a}-{b}\"");
579 }
580 s.push_str(">\n");
581 let (ia, ib) = base.init_range;
582 let _ = writeln!(s, " <Initialization range=\"{ia}-{ib}\"/>");
583 s.push_str(" </SegmentBase>\n");
584 } else {
585 let _ = writeln!(s, " <SegmentList timescale=\"{}\">", r.timescale);
588 let (ia, ib) = base.init_range;
589 let _ = writeln!(s, " <Initialization range=\"{ia}-{ib}\"/>");
590 for (a, b) in &base.media_ranges {
591 let _ = writeln!(s, " <SegmentURL mediaRange=\"{a}-{b}\"/>");
592 }
593 s.push_str(" </SegmentList>\n");
594 }
595 }
596 }
597 s.push_str(" </Representation>\n");
598}
599
600fn render_timeline(s: &mut String, durations: &[u64], start_t: u64) {
602 let mut t = start_t;
603 let mut i = 0;
604 let mut first = true;
605 while i < durations.len() {
606 let d = durations[i];
607 let mut run = 1;
608 while i + run < durations.len() && durations[i + run] == d {
609 run += 1;
610 }
611 s.push_str(" <S");
612 if first {
613 let _ = write!(s, " t=\"{t}\"");
614 first = false;
615 }
616 let _ = write!(s, " d=\"{d}\"");
617 if run > 1 {
618 let _ = write!(s, " r=\"{}\"", run - 1);
619 }
620 s.push_str("/>\n");
621 t += d * run as u64;
622 i += run;
623 }
624}
625
626pub fn iso8601_duration(seconds: f64) -> String {
628 let total_ms = (seconds * 1000.0).round() as i64;
629 let sign = if total_ms < 0 { "-" } else { "" };
630 let total_ms = total_ms.unsigned_abs();
631 let h = total_ms / 3_600_000;
632 let m = (total_ms % 3_600_000) / 60_000;
633 let sec = (total_ms % 60_000) as f64 / 1000.0;
634 let mut out = format!("{sign}PT");
635 if h > 0 {
636 let _ = write!(out, "{h}H");
637 }
638 if m > 0 {
639 let _ = write!(out, "{m}M");
640 }
641 let _ = write!(out, "{sec}S");
642 out
643}
644
645fn xml_escape(s: &str) -> String {
646 s.replace('&', "&")
647 .replace('<', "<")
648 .replace('>', ">")
649 .replace('"', """)
650 .replace('\'', "'")
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use sheathe_core::{Codec, Timescale};
657
658 fn video_stream() -> StreamInfo {
659 StreamInfo {
660 kind: MediaKind::Video,
661 codec: Codec::H264,
662 timescale: Timescale(90000),
663 resolution: Some((1280, 720)),
664 sample_rate: None,
665 bitrate: Some(2_500_000),
666 codec_string: Some("avc1.64001f".into()),
667 }
668 }
669
670 fn audio_stream() -> StreamInfo {
671 StreamInfo {
672 kind: MediaKind::Audio,
673 codec: Codec::Aac,
674 timescale: Timescale(48000),
675 resolution: None,
676 sample_rate: Some(48000),
677 bitrate: Some(128_000),
678 codec_string: Some("mp4a.40.2".into()),
679 }
680 }
681
682 #[test]
683 fn static_vod_mpd_shape() {
684 let rep = Representation::new(
685 "0",
686 video_stream(),
687 "init_0.mp4",
688 "seg_0_$Number$.m4s",
689 90000,
690 vec![540_000, 540_000, 270_000],
691 );
692 let xml = Manifest::static_vod(15.0, vec![rep], None).to_xml();
693 assert!(xml.contains("type=\"static\""));
694 assert!(xml.contains("mediaPresentationDuration=\"PT15S\""));
695 assert!(xml.contains("<Period id=\"0\""));
696 assert!(xml.contains("startNumber=\"1\""));
697 assert!(xml.contains("d=\"540000\" r=\"1\""));
698 assert!(xml.contains("d=\"270000\""));
699 assert!(!xml.contains("availabilityStartTime"));
700 }
701
702 #[test]
703 fn dynamic_live_mpd_shape() {
704 let mut rep = Representation::new(
705 "0",
706 video_stream(),
707 "init_0.mp4",
708 "seg_0_$Number$.m4s",
709 90000,
710 vec![540_000, 540_000, 540_000],
711 );
712 rep.set_start_number(10);
713 rep.set_presentation_time_offset(9 * 540_000);
714 let xml = Manifest::dynamic_live(
715 "2026-07-18T00:00:00Z",
716 "2026-07-18T00:01:00Z",
717 30.0,
718 2.0,
719 10.0,
720 vec![rep],
721 None,
722 )
723 .to_xml();
724 assert!(xml.contains("type=\"dynamic\""));
725 assert!(xml.contains("availabilityStartTime=\"2026-07-18T00:00:00Z\""));
726 assert!(xml.contains("publishTime=\"2026-07-18T00:01:00Z\""));
727 assert!(xml.contains("minimumUpdatePeriod=\"PT2S\""));
728 assert!(xml.contains("timeShiftBufferDepth=\"PT30S\""));
729 assert!(xml.contains("suggestedPresentationDelay=\"PT10S\""));
730 assert!(xml.contains("<UTCTiming"));
731 assert!(xml.contains("startNumber=\"10\""));
732 assert!(xml.contains("presentationTimeOffset=\"4860000\""));
733 assert!(!xml.contains("mediaPresentationDuration"));
734 assert!(!xml.contains("#EXT-X-ENDLIST")); }
736
737 #[test]
738 fn multi_period_and_scte35_event_stream() {
739 let v = Representation::new(
740 "v0",
741 video_stream(),
742 "init_v0.mp4",
743 "seg_v0_$Number$.m4s",
744 90000,
745 vec![900_000],
746 );
747 let a = Representation::new(
748 "a0",
749 audio_stream(),
750 "init_a0.mp4",
751 "seg_a0_$Number$.m4s",
752 48000,
753 vec![480_000],
754 );
755 let events = EventStream::scte35_bin(
756 90000,
757 vec![DashEvent {
758 id: Some("1".into()),
759 presentation_time: 0,
760 duration: Some(900_000),
761 message_data: Some("BASE64SCTE".into()),
762 }],
763 );
764 let m = Manifest {
765 mpd_type: MpdType::Static,
766 duration_seconds: Some(20.0),
767 periods: vec![
768 Period {
769 id: "p0".into(),
770 start_seconds: Some(0.0),
771 duration_seconds: Some(10.0),
772 representations: vec![v.clone()],
773 event_streams: vec![events],
774 },
775 Period {
776 id: "p1".into(),
777 start_seconds: Some(10.0),
778 duration_seconds: Some(10.0),
779 representations: vec![a],
780 event_streams: Vec::new(),
781 },
782 ],
783 ..Manifest::default()
784 };
785 let xml = m.to_xml();
786 assert!(xml.contains("<Period id=\"p0\""));
787 assert!(xml.contains("<Period id=\"p1\""));
788 assert!(xml.contains("start=\"PT10S\""));
789 assert!(xml.contains("schemeIdUri=\"urn:scte:scte35:2014:xml+bin\""));
790 assert!(xml.contains("BASE64SCTE"));
791 }
792
793 #[test]
794 fn trick_play_and_ll_dash_fields() {
795 let mut main = Representation::new(
796 "0",
797 video_stream(),
798 "init_0.mp4",
799 "seg_0_$Number$.m4s",
800 90000,
801 vec![540_000],
802 );
803 main.set_availability_time_offset(Some(3.5));
804 let mut trick = Representation::new(
805 "0_trick",
806 video_stream(),
807 "init_0_trick.mp4",
808 "seg_0_trick_$Number$.m4s",
809 90000,
810 vec![540_000],
811 );
812 trick.max_playout_rate = Some(8.0);
813 let xml = Manifest::static_vod(6.0, vec![main, trick], None).to_xml();
814 assert!(xml.contains("availabilityTimeOffset=\"3.5\""));
815 assert!(xml.contains("availabilityTimeComplete=\"false\""));
816 assert!(xml.contains("maxPlayoutRate=\"8\""));
817 assert!(xml.contains("http://dashif.org/guidelines/trickmode"));
818 assert_eq!(xml.matches("contentType=\"video\"").count(), 2);
820 }
821
822 #[test]
823 fn on_demand_segment_list_shape() {
824 let rep = Representation::on_demand(
825 "0",
826 video_stream(),
827 90000,
828 vec![540_000, 540_000],
829 SegmentBaseInfo {
830 base_url: "rep_0.mp4".into(),
831 init_range: (0, 799),
832 index_range: None,
833 media_ranges: vec![(800, 1999), (2000, 3199)],
834 },
835 );
836 let mut m = Manifest::static_vod(12.0, vec![rep], None);
837 m.profile = DashProfile::OnDemand;
838 let xml = m.to_xml();
839 assert!(xml.contains("isoff-on-demand:2011"));
840 assert!(xml.contains("<BaseURL>rep_0.mp4</BaseURL>"));
841 assert!(xml.contains("<SegmentList"));
842 assert!(xml.contains("range=\"0-799\""));
843 assert!(xml.contains("mediaRange=\"800-1999\""));
844 assert!(xml.contains("mediaRange=\"2000-3199\""));
845 assert!(!xml.contains("SegmentTemplate"));
846 }
847
848 #[test]
849 fn iso8601_formats() {
850 assert_eq!(iso8601_duration(0.0), "PT0S");
851 assert_eq!(iso8601_duration(90.5), "PT1M30.5S");
852 assert_eq!(iso8601_duration(3661.0), "PT1H1M1S");
853 }
854}