Skip to main content

sheathe_package/
knobs.rs

1//! Extra packaging knobs that map 1:1 onto Shaka Packager flags.
2
3use anyhow::{Context, Result};
4use std::path::PathBuf;
5
6use crate::descriptor::StreamDescriptor;
7
8/// HLS / DASH / mux / DRM / IO knobs layered on [`crate::PackageOptions`].
9#[derive(Debug, Clone)]
10pub struct PackageKnobs {
11    /// `--hls_base_url`.
12    pub hls_base_url: Option<String>,
13    /// `--hls_media_sequence_number` initial value (live window still offsets).
14    pub hls_media_sequence_number: u64,
15    /// `--hls_start_time_offset`.
16    pub hls_start_time_offset: Option<f64>,
17    /// `--create_session_keys`.
18    pub create_session_keys: bool,
19    /// `--add_program_date_time` (also implied for live/event).
20    pub add_program_date_time: bool,
21    /// `--closed_captions`.
22    pub closed_captions: Vec<ClosedCaption>,
23    /// `--use_legacy_vp9_codec_string`.
24    pub use_legacy_vp9_codec_string: bool,
25    /// `--dash_add_last_segment_number_when_needed`.
26    pub dash_add_last_segment_number: bool,
27    /// `--segment_template_constant_duration`.
28    pub segment_template_constant_duration: bool,
29    /// `--use_dovi_supplemental_codecs`.
30    pub use_dovi_supplemental_codecs: bool,
31    /// `--mvex_before_trak`.
32    pub mvex_before_trak: bool,
33    /// `--strip_parameter_set_nalus` (avc1/hvc1 vs avc3/hev1). Stored for muxers.
34    pub strip_parameter_set_nalus: bool,
35    /// `--crypt_byte_block` (pattern schemes). Default 1.
36    pub crypt_byte_block: u8,
37    /// `--skip_byte_block` (pattern schemes). Default 9.
38    pub skip_byte_block: u8,
39    /// `--playready_extra_header_data`.
40    pub playready_extra_header_data: Option<String>,
41    /// `--protection_scheme aes128` — HLS AES-128 full-segment CBC.
42    pub hls_aes128: bool,
43    /// `--keys label=AUDIO:key_id=…:key=…`.
44    pub labeled_keys: Vec<LabeledKey>,
45    /// `--decrypt` / `--enable_raw_key` decryption of encrypted inputs.
46    pub decrypt_input: bool,
47    /// Widevine license-server fetch.
48    pub widevine: Option<WidevineConfig>,
49    /// CPIX / SPEKE.
50    pub cpix: Option<CpixConfig>,
51    /// `--ignore_http_output_failures`.
52    pub ignore_http_output_failures: bool,
53    /// `--user_agent`.
54    pub user_agent: Option<String>,
55    /// `--ca_file`.
56    pub ca_file: Option<PathBuf>,
57    /// `--client_cert_file`.
58    pub client_cert_file: Option<PathBuf>,
59    /// `--client_cert_private_key_file`.
60    pub client_cert_key_file: Option<PathBuf>,
61    /// `--client_cert_private_key_password`.
62    pub client_cert_key_password: Option<String>,
63    /// `--disable_peer_verification`.
64    pub disable_peer_verification: bool,
65    /// Keep rewriting live manifests until interrupted.
66    pub live_rewrite: bool,
67    /// Stream descriptors (when non-empty, they replace bare input paths).
68    pub descriptors: Vec<StreamDescriptor>,
69    /// Pixel thresholds for auto DRM labels (Shaka defaults).
70    pub max_sd_pixels: u32,
71    pub max_hd_pixels: u32,
72    pub max_uhd1_pixels: u32,
73}
74
75impl Default for PackageKnobs {
76    fn default() -> Self {
77        Self {
78            hls_base_url: None,
79            hls_media_sequence_number: 0,
80            hls_start_time_offset: None,
81            create_session_keys: false,
82            add_program_date_time: false,
83            closed_captions: Vec::new(),
84            use_legacy_vp9_codec_string: false,
85            dash_add_last_segment_number: false,
86            segment_template_constant_duration: false,
87            use_dovi_supplemental_codecs: false,
88            mvex_before_trak: false,
89            strip_parameter_set_nalus: true,
90            crypt_byte_block: 1,
91            skip_byte_block: 9,
92            playready_extra_header_data: None,
93            hls_aes128: false,
94            labeled_keys: Vec::new(),
95            decrypt_input: false,
96            widevine: None,
97            cpix: None,
98            ignore_http_output_failures: false,
99            user_agent: None,
100            ca_file: None,
101            client_cert_file: None,
102            client_cert_key_file: None,
103            client_cert_key_password: None,
104            disable_peer_verification: false,
105            live_rewrite: false,
106            descriptors: Vec::new(),
107            max_sd_pixels: 442_368,
108            max_hd_pixels: 2_073_600,
109            max_uhd1_pixels: 8_847_360,
110        }
111    }
112}
113
114/// One `--closed_captions` channel.
115#[derive(Debug, Clone)]
116pub struct ClosedCaption {
117    pub instream_id: String,
118    pub name: String,
119    pub language: Option<String>,
120    pub default: bool,
121    pub autoselect: bool,
122}
123
124impl ClosedCaption {
125    /// Parse Shaka `--closed_captions` (`channel=CC1,name=English,lang=eng;…`).
126    pub fn parse_list(spec: &str) -> Result<Vec<Self>> {
127        let mut out = Vec::new();
128        for channel in spec.split(';').filter(|s| !s.is_empty()) {
129            let mut cc = ClosedCaption {
130                instream_id: "CC1".into(),
131                name: "CC".into(),
132                language: None,
133                default: false,
134                autoselect: true,
135            };
136            for kv in channel.split(',') {
137                let Some((k, v)) = kv.split_once('=') else { continue };
138                match k.trim() {
139                    "channel" => cc.instream_id = v.trim().to_string(),
140                    "name" => cc.name = v.trim().to_string(),
141                    "lang" | "language" => cc.language = Some(v.trim().to_string()),
142                    "default" => cc.default = v.trim() == "yes" || v.trim() == "1",
143                    "autoselect" => cc.autoselect = v.trim() != "no" && v.trim() != "0",
144                    _ => {}
145                }
146            }
147            out.push(cc);
148        }
149        Ok(out)
150    }
151}
152
153/// A labeled raw key (`--keys`).
154#[derive(Debug, Clone)]
155pub struct LabeledKey {
156    pub label: String,
157    pub kid: [u8; 16],
158    pub key: [u8; 16],
159    pub iv: Option<[u8; 16]>,
160}
161
162impl LabeledKey {
163    /// Parse `--keys label=AUDIO:key_id=hex:key=hex,label=SD:…`.
164    pub fn parse_list(spec: &str) -> Result<Vec<Self>> {
165        let mut out = Vec::new();
166        for entry in spec.split(',') {
167            let entry = entry.trim();
168            if entry.is_empty() {
169                continue;
170            }
171            let mut label = String::new();
172            let mut kid = None;
173            let mut key = None;
174            let mut iv = None;
175            for kv in entry.split(':') {
176                let Some((k, v)) = kv.split_once('=') else { continue };
177                match k.trim() {
178                    "label" => label = v.trim().to_string(),
179                    "key_id" | "kid" => kid = Some(parse_hex16(v.trim())?),
180                    "key" => key = Some(parse_hex16(v.trim())?),
181                    "iv" => iv = Some(parse_hex16(v.trim())?),
182                    _ => {}
183                }
184            }
185            anyhow::ensure!(
186                !label.is_empty() && kid.is_some() && key.is_some(),
187                "keys entry needs label, key_id, key"
188            );
189            out.push(LabeledKey { label, kid: kid.unwrap(), key: key.unwrap(), iv });
190        }
191        Ok(out)
192    }
193}
194
195/// Widevine Common Encryption key-server request.
196#[derive(Debug, Clone)]
197pub struct WidevineConfig {
198    pub key_server_url: String,
199    pub content_id: Vec<u8>,
200    pub signer: String,
201    pub aes_signing_key: Option<Vec<u8>>,
202    pub aes_signing_iv: Option<Vec<u8>>,
203    pub rsa_signing_key_pem: Option<String>,
204    pub policy: String,
205    pub group_id: Option<Vec<u8>>,
206    pub enable_entitlement_license: bool,
207    pub decrypt: bool,
208}
209
210/// CPIX document / SPEKE exchange.
211#[derive(Debug, Clone)]
212pub struct CpixConfig {
213    pub path_or_url: String,
214    pub headers: Vec<(String, String)>,
215    pub private_key_pem: Option<String>,
216    pub request_file: Option<PathBuf>,
217    pub encrypt: bool,
218    pub decrypt: bool,
219}
220
221pub(crate) fn parse_hex16(s: &str) -> Result<[u8; 16]> {
222    let s = s.trim().trim_start_matches("0x");
223    anyhow::ensure!(s.len() == 32, "expected 32 hex chars, got {}", s.len());
224    let mut out = [0u8; 16];
225    for (i, b) in out.iter_mut().enumerate() {
226        *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).context("non-hex digit")?;
227    }
228    Ok(out)
229}
230
231/// Auto DRM label from resolution, matching Shaka defaults.
232pub fn drm_label_for(
233    kind: sheathe_core::MediaKind,
234    pixels: u32,
235    knobs: &PackageKnobs,
236) -> &'static str {
237    use sheathe_core::MediaKind;
238    match kind {
239        MediaKind::Audio | MediaKind::Text => "AUDIO",
240        MediaKind::Video => {
241            if pixels <= knobs.max_sd_pixels {
242                "SD"
243            } else if pixels <= knobs.max_hd_pixels {
244                "HD"
245            } else if pixels <= knobs.max_uhd1_pixels {
246                "UHD1"
247            } else {
248                "UHD2"
249            }
250        }
251    }
252}