Skip to main content

sheathe_dash/
lib.rs

1//! MPEG-DASH (ISO/IEC 23009-1) manifest generation for **sheathe**.
2//!
3//! Mirrors Shaka Packager's `mpd` library: it takes the set of packaged
4//! representations and emits a static (on-demand) `.mpd` using
5//! `SegmentTemplate` + `SegmentTimeline`, which describes each segment's exact
6//! duration — correct even when the last segment is short. Output is
7//! differential-tested against Shaka Packager's MPD on a sample corpus.
8
9use sheathe_core::{MediaKind, StreamInfo};
10use std::fmt::Write as _;
11
12/// One selectable rendition within an adaptation set.
13#[derive(Debug, Clone)]
14pub struct Representation {
15    /// A unique id within the manifest (also used in segment URLs).
16    pub id: String,
17    /// The stream this representation carries.
18    pub stream: StreamInfo,
19    /// Initialization segment URL.
20    pub init: String,
21    /// Media segment URL template (may contain `$Number$`).
22    pub media: String,
23    /// Timescale the segment durations are expressed in.
24    pub timescale: u32,
25    /// Per-segment durations, in `timescale` ticks, in order.
26    pub segment_durations: Vec<u64>,
27}
28
29/// A complete on-demand presentation.
30#[derive(Debug, Clone, Default)]
31pub struct Manifest {
32    /// Total media duration in seconds.
33    pub duration_seconds: f64,
34    /// All representations, grouped by kind into adaptation sets at render time.
35    pub representations: Vec<Representation>,
36}
37
38impl Manifest {
39    /// Serialize to an MPD XML string (static / on-demand profile).
40    pub fn to_xml(&self) -> String {
41        let mut s = String::new();
42        s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
43        // SegmentTemplate + SegmentTimeline is the "live" profile, even for
44        // static (VOD) presentations.
45        let _ = writeln!(
46            s,
47            concat!(
48                "<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" ",
49                "profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" ",
50                "type=\"static\" mediaPresentationDuration=\"{}\" ",
51                "minBufferTime=\"PT2S\">"
52            ),
53            iso8601_duration(self.duration_seconds),
54        );
55        s.push_str("  <Period>\n");
56
57        for (kind, content_type) in [
58            (MediaKind::Video, "video"),
59            (MediaKind::Audio, "audio"),
60            (MediaKind::Text, "text"),
61        ] {
62            let reps: Vec<&Representation> = self
63                .representations
64                .iter()
65                .filter(|r| r.stream.kind == kind)
66                .collect();
67            if reps.is_empty() {
68                continue;
69            }
70            let _ = writeln!(
71                s,
72                "    <AdaptationSet contentType=\"{}\" segmentAlignment=\"true\">",
73                content_type
74            );
75            for r in reps {
76                render_representation(&mut s, r);
77            }
78            s.push_str("    </AdaptationSet>\n");
79        }
80
81        s.push_str("  </Period>\n</MPD>\n");
82        s
83    }
84}
85
86fn render_representation(s: &mut String, r: &Representation) {
87    let codec = r.stream.rfc6381();
88    let bandwidth = r.stream.bitrate.unwrap_or(0);
89    let _ = write!(
90        s,
91        "      <Representation id=\"{}\" codecs=\"{}\"",
92        r.id, codec
93    );
94    if let Some((w, h)) = r.stream.resolution {
95        let _ = write!(s, " width=\"{}\" height=\"{}\"", w, h);
96    }
97    if let Some(rate) = r.stream.sample_rate {
98        let _ = write!(s, " audioSamplingRate=\"{}\"", rate);
99    }
100    let _ = writeln!(s, " bandwidth=\"{}\">", bandwidth);
101
102    let _ = writeln!(
103        s,
104        "        <SegmentTemplate timescale=\"{}\" initialization=\"{}\" media=\"{}\" startNumber=\"1\">",
105        r.timescale, r.init, r.media
106    );
107    s.push_str("          <SegmentTimeline>\n");
108    render_timeline(s, &r.segment_durations);
109    s.push_str("          </SegmentTimeline>\n");
110    s.push_str("        </SegmentTemplate>\n");
111    s.push_str("      </Representation>\n");
112}
113
114/// Emit `<S>` entries, collapsing runs of equal durations with `r=`.
115fn render_timeline(s: &mut String, durations: &[u64]) {
116    let mut t = 0u64;
117    let mut i = 0;
118    let mut first = true;
119    while i < durations.len() {
120        let d = durations[i];
121        let mut run = 1;
122        while i + run < durations.len() && durations[i + run] == d {
123            run += 1;
124        }
125        s.push_str("            <S");
126        if first {
127            let _ = write!(s, " t=\"{t}\"");
128            first = false;
129        }
130        let _ = write!(s, " d=\"{d}\"");
131        if run > 1 {
132            let _ = write!(s, " r=\"{}\"", run - 1);
133        }
134        s.push_str("/>\n");
135        t += d * run as u64;
136        i += run;
137    }
138}
139
140/// Format seconds as an ISO 8601 duration (e.g. `PT1M30.500S`).
141fn iso8601_duration(seconds: f64) -> String {
142    let total_ms = (seconds * 1000.0).round() as u64;
143    let h = total_ms / 3_600_000;
144    let m = (total_ms % 3_600_000) / 60_000;
145    let s = (total_ms % 60_000) as f64 / 1000.0;
146    let mut out = String::from("PT");
147    if h > 0 {
148        let _ = write!(out, "{}H", h);
149    }
150    if m > 0 {
151        let _ = write!(out, "{}M", m);
152    }
153    let _ = write!(out, "{}S", s);
154    out
155}