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/// CENC protection signalling for the manifest (`ContentProtection`).
30#[derive(Debug, Clone)]
31pub struct Protection {
32    /// Scheme value: `cenc` or `cbcs`.
33    pub scheme: String,
34    /// 16-byte default Key ID, rendered as a dashed UUID.
35    pub default_kid: [u8; 16],
36}
37
38/// A complete on-demand presentation.
39#[derive(Debug, Clone, Default)]
40pub struct Manifest {
41    /// Total media duration in seconds.
42    pub duration_seconds: f64,
43    /// All representations, grouped by kind into adaptation sets at render time.
44    pub representations: Vec<Representation>,
45    /// When set, emit `ContentProtection` elements (encrypted content).
46    pub protection: Option<Protection>,
47}
48
49impl Manifest {
50    /// Serialize to an MPD XML string (static / on-demand profile).
51    pub fn to_xml(&self) -> String {
52        let mut s = String::new();
53        s.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
54        // The `cenc:` namespace is only needed when signalling protection.
55        let cenc_ns = if self.protection.is_some() {
56            " xmlns:cenc=\"urn:mpeg:cenc:2013\""
57        } else {
58            ""
59        };
60        // SegmentTemplate + SegmentTimeline is the "live" profile, even for
61        // static (VOD) presentations.
62        let _ = writeln!(
63            s,
64            concat!(
65                "<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\"{} ",
66                "profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" ",
67                "type=\"static\" mediaPresentationDuration=\"{}\" ",
68                "minBufferTime=\"PT2S\">"
69            ),
70            cenc_ns,
71            iso8601_duration(self.duration_seconds),
72        );
73        s.push_str("  <Period>\n");
74
75        for (kind, content_type) in [
76            (MediaKind::Video, "video"),
77            (MediaKind::Audio, "audio"),
78            (MediaKind::Text, "text"),
79        ] {
80            let reps: Vec<&Representation> = self
81                .representations
82                .iter()
83                .filter(|r| r.stream.kind == kind)
84                .collect();
85            if reps.is_empty() {
86                continue;
87            }
88            let _ = writeln!(
89                s,
90                "    <AdaptationSet contentType=\"{}\" segmentAlignment=\"true\">",
91                content_type
92            );
93            if let Some(p) = &self.protection {
94                render_content_protection(&mut s, p);
95            }
96            for r in reps {
97                render_representation(&mut s, r);
98            }
99            s.push_str("    </AdaptationSet>\n");
100        }
101
102        s.push_str("  </Period>\n</MPD>\n");
103        s
104    }
105}
106
107/// Emit the standard `mp4protection` ContentProtection with the default KID.
108fn render_content_protection(s: &mut String, p: &Protection) {
109    let _ = writeln!(
110        s,
111        concat!(
112            "      <ContentProtection ",
113            "schemeIdUri=\"urn:mpeg:dash:mp4protection:2011\" ",
114            "value=\"{}\" cenc:default_KID=\"{}\"/>"
115        ),
116        p.scheme,
117        kid_uuid(&p.default_kid),
118    );
119}
120
121/// Format a 16-byte KID as a dashed UUID (8-4-4-4-12).
122fn kid_uuid(kid: &[u8; 16]) -> String {
123    let h: String = kid.iter().map(|b| format!("{b:02x}")).collect();
124    format!(
125        "{}-{}-{}-{}-{}",
126        &h[0..8],
127        &h[8..12],
128        &h[12..16],
129        &h[16..20],
130        &h[20..32]
131    )
132}
133
134fn render_representation(s: &mut String, r: &Representation) {
135    let codec = r.stream.rfc6381();
136    let bandwidth = r.stream.bitrate.unwrap_or(0);
137    let _ = write!(
138        s,
139        "      <Representation id=\"{}\" codecs=\"{}\"",
140        r.id, codec
141    );
142    if let Some((w, h)) = r.stream.resolution {
143        let _ = write!(s, " width=\"{}\" height=\"{}\"", w, h);
144    }
145    if let Some(rate) = r.stream.sample_rate {
146        let _ = write!(s, " audioSamplingRate=\"{}\"", rate);
147    }
148    let _ = writeln!(s, " bandwidth=\"{}\">", bandwidth);
149
150    let _ = writeln!(
151        s,
152        "        <SegmentTemplate timescale=\"{}\" initialization=\"{}\" media=\"{}\" startNumber=\"1\">",
153        r.timescale, r.init, r.media
154    );
155    s.push_str("          <SegmentTimeline>\n");
156    render_timeline(s, &r.segment_durations);
157    s.push_str("          </SegmentTimeline>\n");
158    s.push_str("        </SegmentTemplate>\n");
159    s.push_str("      </Representation>\n");
160}
161
162/// Emit `<S>` entries, collapsing runs of equal durations with `r=`.
163fn render_timeline(s: &mut String, durations: &[u64]) {
164    let mut t = 0u64;
165    let mut i = 0;
166    let mut first = true;
167    while i < durations.len() {
168        let d = durations[i];
169        let mut run = 1;
170        while i + run < durations.len() && durations[i + run] == d {
171            run += 1;
172        }
173        s.push_str("            <S");
174        if first {
175            let _ = write!(s, " t=\"{t}\"");
176            first = false;
177        }
178        let _ = write!(s, " d=\"{d}\"");
179        if run > 1 {
180            let _ = write!(s, " r=\"{}\"", run - 1);
181        }
182        s.push_str("/>\n");
183        t += d * run as u64;
184        i += run;
185    }
186}
187
188/// Format seconds as an ISO 8601 duration (e.g. `PT1M30.500S`).
189fn iso8601_duration(seconds: f64) -> String {
190    let total_ms = (seconds * 1000.0).round() as u64;
191    let h = total_ms / 3_600_000;
192    let m = (total_ms % 3_600_000) / 60_000;
193    let s = (total_ms % 60_000) as f64 / 1000.0;
194    let mut out = String::from("PT");
195    if h > 0 {
196        let _ = write!(out, "{}H", h);
197    }
198    if m > 0 {
199        let _ = write!(out, "{}M", m);
200    }
201    let _ = write!(out, "{}S", s);
202    out
203}