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