use anyhow::{Context, Result};
use std::path::PathBuf;
use crate::descriptor::StreamDescriptor;
#[derive(Debug, Clone)]
pub struct PackageKnobs {
pub hls_base_url: Option<String>,
pub hls_media_sequence_number: u64,
pub hls_start_time_offset: Option<f64>,
pub create_session_keys: bool,
pub add_program_date_time: bool,
pub closed_captions: Vec<ClosedCaption>,
pub use_legacy_vp9_codec_string: bool,
pub dash_add_last_segment_number: bool,
pub segment_template_constant_duration: bool,
pub use_dovi_supplemental_codecs: bool,
pub mvex_before_trak: bool,
pub strip_parameter_set_nalus: bool,
pub crypt_byte_block: u8,
pub skip_byte_block: u8,
pub playready_extra_header_data: Option<String>,
pub hls_aes128: bool,
pub labeled_keys: Vec<LabeledKey>,
pub decrypt_input: bool,
pub widevine: Option<WidevineConfig>,
pub cpix: Option<CpixConfig>,
pub ignore_http_output_failures: bool,
pub user_agent: Option<String>,
pub ca_file: Option<PathBuf>,
pub client_cert_file: Option<PathBuf>,
pub client_cert_key_file: Option<PathBuf>,
pub client_cert_key_password: Option<String>,
pub disable_peer_verification: bool,
pub live_rewrite: bool,
pub descriptors: Vec<StreamDescriptor>,
pub max_sd_pixels: u32,
pub max_hd_pixels: u32,
pub max_uhd1_pixels: u32,
}
impl Default for PackageKnobs {
fn default() -> Self {
Self {
hls_base_url: None,
hls_media_sequence_number: 0,
hls_start_time_offset: None,
create_session_keys: false,
add_program_date_time: false,
closed_captions: Vec::new(),
use_legacy_vp9_codec_string: false,
dash_add_last_segment_number: false,
segment_template_constant_duration: false,
use_dovi_supplemental_codecs: false,
mvex_before_trak: false,
strip_parameter_set_nalus: true,
crypt_byte_block: 1,
skip_byte_block: 9,
playready_extra_header_data: None,
hls_aes128: false,
labeled_keys: Vec::new(),
decrypt_input: false,
widevine: None,
cpix: None,
ignore_http_output_failures: false,
user_agent: None,
ca_file: None,
client_cert_file: None,
client_cert_key_file: None,
client_cert_key_password: None,
disable_peer_verification: false,
live_rewrite: false,
descriptors: Vec::new(),
max_sd_pixels: 442_368,
max_hd_pixels: 2_073_600,
max_uhd1_pixels: 8_847_360,
}
}
}
#[derive(Debug, Clone)]
pub struct ClosedCaption {
pub instream_id: String,
pub name: String,
pub language: Option<String>,
pub default: bool,
pub autoselect: bool,
}
impl ClosedCaption {
pub fn parse_list(spec: &str) -> Result<Vec<Self>> {
let mut out = Vec::new();
for channel in spec.split(';').filter(|s| !s.is_empty()) {
let mut cc = ClosedCaption {
instream_id: "CC1".into(),
name: "CC".into(),
language: None,
default: false,
autoselect: true,
};
for kv in channel.split(',') {
let Some((k, v)) = kv.split_once('=') else { continue };
match k.trim() {
"channel" => cc.instream_id = v.trim().to_string(),
"name" => cc.name = v.trim().to_string(),
"lang" | "language" => cc.language = Some(v.trim().to_string()),
"default" => cc.default = v.trim() == "yes" || v.trim() == "1",
"autoselect" => cc.autoselect = v.trim() != "no" && v.trim() != "0",
_ => {}
}
}
out.push(cc);
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct LabeledKey {
pub label: String,
pub kid: [u8; 16],
pub key: [u8; 16],
pub iv: Option<[u8; 16]>,
}
impl LabeledKey {
pub fn parse_list(spec: &str) -> Result<Vec<Self>> {
let mut out = Vec::new();
for entry in spec.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let mut label = String::new();
let mut kid = None;
let mut key = None;
let mut iv = None;
for kv in entry.split(':') {
let Some((k, v)) = kv.split_once('=') else { continue };
match k.trim() {
"label" => label = v.trim().to_string(),
"key_id" | "kid" => kid = Some(parse_hex16(v.trim())?),
"key" => key = Some(parse_hex16(v.trim())?),
"iv" => iv = Some(parse_hex16(v.trim())?),
_ => {}
}
}
anyhow::ensure!(
!label.is_empty() && kid.is_some() && key.is_some(),
"keys entry needs label, key_id, key"
);
out.push(LabeledKey { label, kid: kid.unwrap(), key: key.unwrap(), iv });
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct WidevineConfig {
pub key_server_url: String,
pub content_id: Vec<u8>,
pub signer: String,
pub aes_signing_key: Option<Vec<u8>>,
pub aes_signing_iv: Option<Vec<u8>>,
pub rsa_signing_key_pem: Option<String>,
pub policy: String,
pub group_id: Option<Vec<u8>>,
pub enable_entitlement_license: bool,
pub decrypt: bool,
}
#[derive(Debug, Clone)]
pub struct CpixConfig {
pub path_or_url: String,
pub headers: Vec<(String, String)>,
pub private_key_pem: Option<String>,
pub request_file: Option<PathBuf>,
pub encrypt: bool,
pub decrypt: bool,
}
pub(crate) fn parse_hex16(s: &str) -> Result<[u8; 16]> {
let s = s.trim().trim_start_matches("0x");
anyhow::ensure!(s.len() == 32, "expected 32 hex chars, got {}", s.len());
let mut out = [0u8; 16];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).context("non-hex digit")?;
}
Ok(out)
}
pub fn drm_label_for(
kind: sheathe_core::MediaKind,
pixels: u32,
knobs: &PackageKnobs,
) -> &'static str {
use sheathe_core::MediaKind;
match kind {
MediaKind::Audio | MediaKind::Text => "AUDIO",
MediaKind::Video => {
if pixels <= knobs.max_sd_pixels {
"SD"
} else if pixels <= knobs.max_hd_pixels {
"HD"
} else if pixels <= knobs.max_uhd1_pixels {
"UHD1"
} else {
"UHD2"
}
}
}
}