#![allow(dead_code)]
use crate::error::{PackagerError, PackagerResult};
use crate::pssh::{
build_cenc_pssh, build_fairplay_pssh, build_playready_pssh, build_widevine_pssh, DrmSystem,
PsshBox,
};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
#[derive(Debug, Clone)]
pub struct DrmSystemConfig {
pub system: DrmSystem,
pub key_id: [u8; 16],
pub content_id: Vec<u8>,
pub la_url: Option<String>,
pub key_server_uri: Option<String>,
}
impl DrmSystemConfig {
#[must_use]
pub fn widevine(key_id: [u8; 16], content_id: Vec<u8>) -> Self {
Self {
system: DrmSystem::Widevine,
key_id,
content_id,
la_url: None,
key_server_uri: None,
}
}
#[must_use]
pub fn playready(key_id: [u8; 16]) -> Self {
Self {
system: DrmSystem::PlayReady,
key_id,
content_id: Vec::new(),
la_url: None,
key_server_uri: None,
}
}
#[must_use]
pub fn fairplay(key_id: [u8; 16], key_server_uri: impl Into<String>) -> Self {
Self {
system: DrmSystem::FairPlay,
key_id,
content_id: Vec::new(),
la_url: None,
key_server_uri: Some(key_server_uri.into()),
}
}
#[must_use]
pub fn with_la_url(mut self, url: impl Into<String>) -> Self {
self.la_url = Some(url.into());
self
}
}
#[derive(Debug, Clone)]
pub struct DrmPackagerConfig {
pub systems: Vec<DrmSystemConfig>,
pub embed_pssh_in_init: bool,
pub include_pssh_in_mpd: bool,
}
impl Default for DrmPackagerConfig {
fn default() -> Self {
Self {
systems: Vec::new(),
embed_pssh_in_init: true,
include_pssh_in_mpd: true,
}
}
}
impl DrmPackagerConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_system(mut self, system: DrmSystemConfig) -> Self {
self.systems.push(system);
self
}
#[must_use]
pub fn has_drm(&self) -> bool {
!self.systems.is_empty()
}
pub fn validate(&self) -> PackagerResult<()> {
for sys in &self.systems {
if sys.system == DrmSystem::FairPlay && sys.key_server_uri.is_none() {
return Err(PackagerError::DrmFailed(
"FairPlay requires a key server URI".to_string(),
));
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct GeneratedPssh {
pub system: DrmSystem,
pub pssh_box: PsshBox,
pub encoded: Vec<u8>,
pub base64: String,
}
pub struct DrmPackager {
config: DrmPackagerConfig,
generated: Vec<GeneratedPssh>,
}
impl DrmPackager {
pub fn new(config: DrmPackagerConfig) -> PackagerResult<Self> {
config.validate()?;
Ok(Self {
config,
generated: Vec::new(),
})
}
pub fn generate_pssh_boxes(&mut self) -> PackagerResult<&[GeneratedPssh]> {
self.generated.clear();
for sys_config in &self.config.systems {
let pssh_box = match sys_config.system {
DrmSystem::Widevine => {
build_widevine_pssh(&sys_config.key_id, &sys_config.content_id)
}
DrmSystem::PlayReady => build_playready_pssh(&sys_config.key_id),
DrmSystem::FairPlay => {
let uri = sys_config
.key_server_uri
.as_deref()
.unwrap_or("skd://default");
build_fairplay_pssh(&sys_config.key_id, uri)
}
DrmSystem::Marlin | DrmSystem::CommonEncryption => {
build_cenc_pssh(&[sys_config.key_id])
}
};
let encoded = pssh_box.encode();
let base64 = BASE64.encode(&encoded);
self.generated.push(GeneratedPssh {
system: sys_config.system,
pssh_box,
encoded,
base64,
});
}
Ok(&self.generated)
}
#[must_use]
pub fn pssh_boxes(&self) -> &[GeneratedPssh] {
&self.generated
}
pub fn inject_into_init_segment(&self, init_segment: &[u8]) -> PackagerResult<Vec<u8>> {
if self.generated.is_empty() {
return Ok(init_segment.to_vec());
}
let moov_offset = find_box_offset(init_segment, b"moov").ok_or_else(|| {
PackagerError::DrmFailed("No moov box found in init segment".to_string())
})?;
let moov_size = u32::from_be_bytes(
init_segment[moov_offset..moov_offset + 4]
.try_into()
.map_err(|_| PackagerError::DrmFailed("Failed to read moov size".to_string()))?,
) as usize;
let moov_end = moov_offset + moov_size;
let mut pssh_data = Vec::new();
for gen in &self.generated {
pssh_data.extend_from_slice(&gen.encoded);
}
let new_moov_size = moov_size + pssh_data.len();
let mut out = Vec::with_capacity(init_segment.len() + pssh_data.len());
out.extend_from_slice(&init_segment[..moov_offset]);
out.extend_from_slice(&(new_moov_size as u32).to_be_bytes());
out.extend_from_slice(&init_segment[moov_offset + 4..moov_end]);
out.extend_from_slice(&pssh_data);
if moov_end < init_segment.len() {
out.extend_from_slice(&init_segment[moov_end..]);
}
Ok(out)
}
#[must_use]
pub fn content_protection_xml(&self) -> String {
let mut xml = String::new();
if let Some(first) = self.config.systems.first() {
let kid_hex = hex::encode(first.key_id);
let kid_uuid = format!(
"{}-{}-{}-{}-{}",
&kid_hex[0..8],
&kid_hex[8..12],
&kid_hex[12..16],
&kid_hex[16..20],
&kid_hex[20..32],
);
xml.push_str(&format!(
" <ContentProtection schemeIdUri=\"urn:mpeg:dash:mp4protection:2011\" \
value=\"cenc\" cenc:default_KID=\"{kid_uuid}\"/>\n"
));
}
for gen in &self.generated {
let system_uuid = gen.pssh_box.system_id_uuid();
xml.push_str(&format!(
" <ContentProtection schemeIdUri=\"urn:uuid:{system_uuid}\""
));
let la_url = self
.config
.systems
.iter()
.find(|s| s.system == gen.system)
.and_then(|s| s.la_url.as_deref());
if let Some(url) = la_url {
xml.push_str(&format!(" value=\"{url}\""));
}
if self.config.include_pssh_in_mpd {
xml.push_str(">\n");
xml.push_str(&format!(" <cenc:pssh>{}</cenc:pssh>\n", gen.base64));
xml.push_str(" </ContentProtection>\n");
} else {
xml.push_str("/>\n");
}
}
xml
}
#[must_use]
pub fn hls_ext_x_key_tags(&self) -> String {
let mut tags = String::new();
for sys_config in &self.config.systems {
match sys_config.system {
DrmSystem::FairPlay => {
let uri = sys_config
.key_server_uri
.as_deref()
.unwrap_or("skd://default");
let kid_hex = hex::encode(sys_config.key_id);
tags.push_str(&format!(
"#EXT-X-KEY:METHOD=SAMPLE-AES,URI=\"{uri}\",\
KEYFORMAT=\"com.apple.streamingkeydelivery\",\
KEYFORMATVERSIONS=\"1\",\
KEYID=0x{kid_hex}\n"
));
}
DrmSystem::Widevine => {
if let Some(gen) = self
.generated
.iter()
.find(|g| g.system == DrmSystem::Widevine)
{
let kid_hex = hex::encode(sys_config.key_id);
let la_url = sys_config
.la_url
.as_deref()
.unwrap_or("https://proxy.uat.widevine.com/proxy");
tags.push_str(&format!(
"#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"data:text/plain;base64,{}\",\
KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\",\
KEYFORMATVERSIONS=\"1\",\
KEYID=0x{kid_hex}\n",
gen.base64
));
let _ = la_url; }
}
DrmSystem::PlayReady => {
if let Some(gen) = self
.generated
.iter()
.find(|g| g.system == DrmSystem::PlayReady)
{
let kid_hex = hex::encode(sys_config.key_id);
tags.push_str(&format!(
"#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"data:text/plain;base64,{}\",\
KEYFORMAT=\"com.microsoft.playready\",\
KEYFORMATVERSIONS=\"1\",\
KEYID=0x{kid_hex}\n",
gen.base64
));
}
}
_ => {}
}
}
tags
}
#[must_use]
pub fn config(&self) -> &DrmPackagerConfig {
&self.config
}
}
fn find_box_offset(data: &[u8], fourcc: &[u8; 4]) -> Option<usize> {
let mut i = 0;
while i + 8 <= data.len() {
let size = u32::from_be_bytes(data[i..i + 4].try_into().ok()?) as usize;
if size < 8 {
break;
}
if &data[i + 4..i + 8] == fourcc {
return Some(i);
}
if i + size <= data.len() {
if let Some(inner) = find_box_offset(&data[i + 8..i + size], fourcc) {
return Some(i + 8 + inner);
}
}
i += size;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::isobmff_writer::write_init_segment;
use crate::pssh;
fn test_key_id() -> [u8; 16] {
[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10,
]
}
#[test]
fn test_widevine_config() {
let cfg = DrmSystemConfig::widevine(test_key_id(), b"content-1".to_vec());
assert_eq!(cfg.system, DrmSystem::Widevine);
assert_eq!(cfg.content_id, b"content-1");
}
#[test]
fn test_playready_config() {
let cfg = DrmSystemConfig::playready(test_key_id());
assert_eq!(cfg.system, DrmSystem::PlayReady);
}
#[test]
fn test_fairplay_config() {
let cfg = DrmSystemConfig::fairplay(test_key_id(), "skd://key.example.com");
assert_eq!(cfg.system, DrmSystem::FairPlay);
assert_eq!(cfg.key_server_uri.as_deref(), Some("skd://key.example.com"));
}
#[test]
fn test_config_with_la_url() {
let cfg = DrmSystemConfig::widevine(test_key_id(), Vec::new())
.with_la_url("https://lic.example.com");
assert_eq!(cfg.la_url.as_deref(), Some("https://lic.example.com"));
}
#[test]
fn test_drm_packager_config_empty() {
let cfg = DrmPackagerConfig::new();
assert!(!cfg.has_drm());
assert!(cfg.validate().is_ok());
}
#[test]
fn test_drm_packager_config_with_systems() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()))
.with_system(DrmSystemConfig::playready(test_key_id()));
assert!(cfg.has_drm());
assert_eq!(cfg.systems.len(), 2);
}
#[test]
fn test_drm_packager_config_fairplay_requires_uri() {
let cfg = DrmPackagerConfig::new().with_system(DrmSystemConfig {
system: DrmSystem::FairPlay,
key_id: test_key_id(),
content_id: Vec::new(),
la_url: None,
key_server_uri: None, });
assert!(cfg.validate().is_err());
}
#[test]
fn test_generate_pssh_widevine() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), b"test".to_vec()));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
let boxes = packager.generate_pssh_boxes().expect("should succeed");
assert_eq!(boxes.len(), 1);
assert_eq!(boxes[0].system, DrmSystem::Widevine);
assert!(!boxes[0].encoded.is_empty());
assert!(!boxes[0].base64.is_empty());
}
#[test]
fn test_generate_pssh_playready() {
let cfg = DrmPackagerConfig::new().with_system(DrmSystemConfig::playready(test_key_id()));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
let boxes = packager.generate_pssh_boxes().expect("should succeed");
assert_eq!(boxes.len(), 1);
assert_eq!(boxes[0].system, DrmSystem::PlayReady);
}
#[test]
fn test_generate_pssh_fairplay() {
let cfg = DrmPackagerConfig::new().with_system(DrmSystemConfig::fairplay(
test_key_id(),
"skd://test.com/key",
));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
let boxes = packager.generate_pssh_boxes().expect("should succeed");
assert_eq!(boxes.len(), 1);
assert_eq!(boxes[0].system, DrmSystem::FairPlay);
}
#[test]
fn test_generate_multi_drm() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()))
.with_system(DrmSystemConfig::playready(test_key_id()))
.with_system(DrmSystemConfig::fairplay(test_key_id(), "skd://test.com"));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
let boxes = packager.generate_pssh_boxes().expect("should succeed");
assert_eq!(boxes.len(), 3);
}
#[test]
fn test_pssh_roundtrip_decode() {
let cfg = DrmPackagerConfig::new().with_system(DrmSystemConfig::widevine(
test_key_id(),
b"content".to_vec(),
));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
let boxes = packager.generate_pssh_boxes().expect("should succeed");
let decoded = PsshBox::decode(&boxes[0].encoded).expect("should decode");
assert_eq!(decoded.system_id, pssh::WIDEVINE_SYSTEM_ID);
}
#[test]
fn test_inject_into_init_segment() {
let init_cfg = crate::isobmff_writer::InitConfig::new(1920, 1080, 90_000, *b"av01");
let init = write_init_segment(&init_cfg);
let drm_cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let mut packager = DrmPackager::new(drm_cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let injected = packager
.inject_into_init_segment(&init)
.expect("should succeed");
assert!(injected.len() > init.len());
assert_eq!(&injected[4..8], b"ftyp");
let found_pssh = find_box_offset(&injected, b"pssh");
assert!(found_pssh.is_some());
}
#[test]
fn test_inject_no_moov_error() {
let drm_cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let mut packager = DrmPackager::new(drm_cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let result = packager.inject_into_init_segment(&[0u8; 16]);
assert!(result.is_err());
}
#[test]
fn test_inject_empty_pssh_passthrough() {
let init_cfg = crate::isobmff_writer::InitConfig::new(1920, 1080, 90_000, *b"av01");
let init = write_init_segment(&init_cfg);
let drm_cfg = DrmPackagerConfig::new(); let packager = DrmPackager::new(drm_cfg).expect("should succeed");
let result = packager
.inject_into_init_segment(&init)
.expect("should succeed");
assert_eq!(result, init);
}
#[test]
fn test_inject_multi_drm_pssh() {
let init_cfg = crate::isobmff_writer::InitConfig::new(1280, 720, 90_000, *b"vp09");
let init = write_init_segment(&init_cfg);
let drm_cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()))
.with_system(DrmSystemConfig::playready(test_key_id()));
let mut packager = DrmPackager::new(drm_cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let injected = packager
.inject_into_init_segment(&init)
.expect("should succeed");
let moov_off = find_box_offset(&injected, b"moov").expect("moov should exist");
let moov_size = u32::from_be_bytes(
injected[moov_off..moov_off + 4]
.try_into()
.expect("4 bytes"),
) as usize;
let moov_content = &injected[moov_off + 8..moov_off + moov_size];
let pssh_results = PsshBox::scan_all(moov_content);
let pssh_ok: Vec<_> = pssh_results.into_iter().filter_map(|r| r.ok()).collect();
assert_eq!(pssh_ok.len(), 2);
}
#[test]
fn test_content_protection_xml_widevine() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let xml = packager.content_protection_xml();
assert!(xml.contains("urn:mpeg:dash:mp4protection:2011"));
assert!(xml.contains("cenc:default_KID"));
assert!(xml.contains("urn:uuid:"));
assert!(xml.contains("cenc:pssh"));
}
#[test]
fn test_content_protection_xml_with_la_url() {
let cfg = DrmPackagerConfig::new().with_system(
DrmSystemConfig::widevine(test_key_id(), Vec::new())
.with_la_url("https://lic.example.com"),
);
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let xml = packager.content_protection_xml();
assert!(xml.contains("https://lic.example.com"));
}
#[test]
fn test_content_protection_xml_no_pssh_in_mpd() {
let mut cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
cfg.include_pssh_in_mpd = false;
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let xml = packager.content_protection_xml();
assert!(!xml.contains("cenc:pssh"));
}
#[test]
fn test_hls_ext_x_key_fairplay() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::fairplay(test_key_id(), "skd://test.com"));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let tags = packager.hls_ext_x_key_tags();
assert!(tags.contains("#EXT-X-KEY:METHOD=SAMPLE-AES"));
assert!(tags.contains("com.apple.streamingkeydelivery"));
assert!(tags.contains("skd://test.com"));
assert!(tags.contains("KEYID=0x"));
}
#[test]
fn test_hls_ext_x_key_widevine() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let tags = packager.hls_ext_x_key_tags();
assert!(tags.contains("#EXT-X-KEY:METHOD=SAMPLE-AES-CTR"));
assert!(tags.contains("urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"));
}
#[test]
fn test_hls_ext_x_key_playready() {
let cfg = DrmPackagerConfig::new().with_system(DrmSystemConfig::playready(test_key_id()));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let tags = packager.hls_ext_x_key_tags();
assert!(tags.contains("com.microsoft.playready"));
}
#[test]
fn test_hls_ext_x_key_multi_drm() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::fairplay(test_key_id(), "skd://test.com"))
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let tags = packager.hls_ext_x_key_tags();
assert!(tags.contains("com.apple.streamingkeydelivery"));
assert!(tags.contains("urn:uuid:edef8ba9"));
}
#[test]
fn test_hls_ext_x_key_empty_no_drm() {
let cfg = DrmPackagerConfig::new();
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
let tags = packager.hls_ext_x_key_tags();
assert!(tags.is_empty());
}
#[test]
fn test_drm_packager_config_accessor() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let packager = DrmPackager::new(cfg).expect("should succeed");
assert!(packager.config().has_drm());
}
#[test]
fn test_pssh_boxes_before_generate() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let packager = DrmPackager::new(cfg).expect("should succeed");
assert!(packager.pssh_boxes().is_empty());
}
#[test]
fn test_generate_regenerates() {
let cfg = DrmPackagerConfig::new()
.with_system(DrmSystemConfig::widevine(test_key_id(), Vec::new()));
let mut packager = DrmPackager::new(cfg).expect("should succeed");
packager.generate_pssh_boxes().expect("should succeed");
assert_eq!(packager.pssh_boxes().len(), 1);
packager.generate_pssh_boxes().expect("should succeed");
assert_eq!(packager.pssh_boxes().len(), 1);
}
}