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 =
56            if self.protection.is_some() { " xmlns:cenc=\"urn:mpeg:cenc:2013\"" } else { "" };
57        // SegmentTemplate + SegmentTimeline is the "live" profile, even for
58        // static (VOD) presentations.
59        let _ = writeln!(
60            s,
61            concat!(
62                "<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\"{} ",
63                "profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" ",
64                "type=\"static\" mediaPresentationDuration=\"{}\" ",
65                "minBufferTime=\"PT2S\">"
66            ),
67            cenc_ns,
68            iso8601_duration(self.duration_seconds),
69        );
70        s.push_str("  <Period>\n");
71
72        for (kind, content_type) in
73            [(MediaKind::Video, "video"), (MediaKind::Audio, "audio"), (MediaKind::Text, "text")]
74        {
75            let reps: Vec<&Representation> =
76                self.representations.iter().filter(|r| r.stream.kind == kind).collect();
77            if reps.is_empty() {
78                continue;
79            }
80            let _ = writeln!(
81                s,
82                "    <AdaptationSet contentType=\"{}\" segmentAlignment=\"true\">",
83                content_type
84            );
85            if let Some(p) = &self.protection {
86                render_content_protection(&mut s, p);
87            }
88            for r in reps {
89                render_representation(&mut s, r);
90            }
91            s.push_str("    </AdaptationSet>\n");
92        }
93
94        s.push_str("  </Period>\n</MPD>\n");
95        s
96    }
97}
98
99/// Emit the standard `mp4protection` ContentProtection with the default KID.
100fn render_content_protection(s: &mut String, p: &Protection) {
101    let _ = writeln!(
102        s,
103        concat!(
104            "      <ContentProtection ",
105            "schemeIdUri=\"urn:mpeg:dash:mp4protection:2011\" ",
106            "value=\"{}\" cenc:default_KID=\"{}\"/>"
107        ),
108        p.scheme,
109        kid_uuid(&p.default_kid),
110    );
111}
112
113/// Format a 16-byte KID as a dashed UUID (8-4-4-4-12).
114fn kid_uuid(kid: &[u8; 16]) -> String {
115    let h: String = kid.iter().map(|b| format!("{b:02x}")).collect();
116    format!("{}-{}-{}-{}-{}", &h[0..8], &h[8..12], &h[12..16], &h[16..20], &h[20..32])
117}
118
119fn render_representation(s: &mut String, r: &Representation) {
120    let codec = r.stream.rfc6381();
121    let bandwidth = r.stream.bitrate.unwrap_or(0);
122    let _ = write!(s, "      <Representation id=\"{}\" codecs=\"{}\"", r.id, codec);
123    if let Some((w, h)) = r.stream.resolution {
124        let _ = write!(s, " width=\"{}\" height=\"{}\"", w, h);
125    }
126    if let Some(rate) = r.stream.sample_rate {
127        let _ = write!(s, " audioSamplingRate=\"{}\"", rate);
128    }
129    let _ = writeln!(s, " bandwidth=\"{}\">", bandwidth);
130
131    let _ = writeln!(
132        s,
133        "        <SegmentTemplate timescale=\"{}\" initialization=\"{}\" media=\"{}\" startNumber=\"1\">",
134        r.timescale, r.init, r.media
135    );
136    s.push_str("          <SegmentTimeline>\n");
137    render_timeline(s, &r.segment_durations);
138    s.push_str("          </SegmentTimeline>\n");
139    s.push_str("        </SegmentTemplate>\n");
140    s.push_str("      </Representation>\n");
141}
142
143/// Emit `<S>` entries, collapsing runs of equal durations with `r=`.
144fn render_timeline(s: &mut String, durations: &[u64]) {
145    let mut t = 0u64;
146    let mut i = 0;
147    let mut first = true;
148    while i < durations.len() {
149        let d = durations[i];
150        let mut run = 1;
151        while i + run < durations.len() && durations[i + run] == d {
152            run += 1;
153        }
154        s.push_str("            <S");
155        if first {
156            let _ = write!(s, " t=\"{t}\"");
157            first = false;
158        }
159        let _ = write!(s, " d=\"{d}\"");
160        if run > 1 {
161            let _ = write!(s, " r=\"{}\"", run - 1);
162        }
163        s.push_str("/>\n");
164        t += d * run as u64;
165        i += run;
166    }
167}
168
169/// Format seconds as an ISO 8601 duration (e.g. `PT1M30.500S`).
170fn iso8601_duration(seconds: f64) -> String {
171    let total_ms = (seconds * 1000.0).round() as u64;
172    let h = total_ms / 3_600_000;
173    let m = (total_ms % 3_600_000) / 60_000;
174    let s = (total_ms % 60_000) as f64 / 1000.0;
175    let mut out = String::from("PT");
176    if h > 0 {
177        let _ = write!(out, "{}H", h);
178    }
179    if m > 0 {
180        let _ = write!(out, "{}M", m);
181    }
182    let _ = write!(out, "{}S", s);
183    out
184}