Skip to main content

vsd_mp4/pssh/
parser.rs

1/*
2    REFERENCES
3    ----------
4
5    1. https://github.com/shaka-project/shaka-player/blob/4e933116984beb630d31ce7a0b8c9bc6f8b48c06/lib/util/pssh.js
6    2. https://github.com/shaka-project/shaka-packager/blob/56e227267c9091a0f65b4d92d9064dda4557f3a7/packager/tools/pssh/pssh-box.py
7    3. https://github.com/shaka-project/shaka-player/blob/b441518943241693fa2df03196be6ee707c8511e/lib/dash/content_protection.js
8    4. https://github.com/rlaphoenix/pywidevine/blob/master/pywidevine/pssh.py
9
10*/
11
12use crate::{
13    bail, data,
14    error::Result,
15    parser,
16    parser::{Mp4Parser, ParsedBox},
17    pssh::{playready, widevine},
18};
19use base64::Engine;
20
21const COMMON_SYSTEM_ID: &str = "1077efecc0b24d02ace33c1e52e2fb4b";
22const PLAYREADY_SYSTEM_ID: &str = "9a04f07998404286ab92e65be0885f95";
23const WIDEVINE_SYSTEM_ID: &str = "edef8ba979d64acea3c827dcd51d21ed";
24
25/// Parsed PSSH box content containing PSSH data chunks from an MP4 initialization segment.
26#[derive(Debug)]
27pub struct PsshBox {
28    /// The collection of parsed PSSH data blocks.
29    pub boxes: Vec<PsshData>,
30}
31
32impl PsshBox {
33    /// Parses PSSH boxes from an MP4 initialization or media segment.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if box parsing fails, if required PSSH boxes are malformed,
38    /// or if there are unrecognized PSSH versions.
39    pub fn from_init(data: &[u8]) -> Result<Self> {
40        let boxes = data!(Vec::new());
41
42        Mp4Parser::new()
43            .base_box("moov", parser::children)
44            .base_box("moof", parser::children)
45            .full_box("pssh", {
46                let boxes = boxes.clone();
47                move |mut box_| {
48                    Self::parse(&mut box_, &mut boxes.borrow_mut())?;
49                    Ok(())
50                }
51            })
52            .parse(data, false, false)?;
53
54        Ok(Self {
55            boxes: boxes.take(),
56        })
57    }
58
59    fn parse(box_: &mut ParsedBox, boxes: &mut Vec<PsshData>) -> Result<()> {
60        let Some(box_version) = box_.version else {
61            bail!("PSSH boxes are full boxes and must have a valid version.");
62        };
63
64        if box_.flags.is_none() {
65            bail!("PSSH boxes are full boxes and must have a valid flag.");
66        }
67
68        if box_version > 1 {
69            bail!("Unrecognized PSSH version found!");
70        }
71
72        let system_id = hex::encode(box_.reader.read_bytes_u8(16)?);
73
74        if box_version > 0 {
75            let mut data = PsshData {
76                data: box_.full_data(),
77                key_ids: Vec::new(),
78                system_id: if system_id == COMMON_SYSTEM_ID {
79                    SystemId::Common
80                } else {
81                    SystemId::Other(system_id.to_owned())
82                },
83            };
84            let num_key_ids = box_.reader.read_u32()?;
85
86            for _ in 0..num_key_ids {
87                let key_id = hex::encode(box_.reader.read_bytes_u8(16)?);
88                data.key_ids.push(key_id);
89            }
90
91            boxes.push(data);
92        }
93
94        let pssh_data_size = box_.reader.read_u32()?;
95        let pssh_data = box_.reader.read_bytes_u8(pssh_data_size as usize)?;
96        let mut key_ids = Vec::new();
97
98        match system_id.as_str() {
99            PLAYREADY_SYSTEM_ID => key_ids = playready::parse_key_ids(&pssh_data)?,
100            WIDEVINE_SYSTEM_ID => key_ids = widevine::parse_key_ids(&pssh_data)?,
101            _ => (),
102        }
103
104        boxes.push(PsshData {
105            data: box_.full_data(),
106            key_ids,
107            system_id: match system_id.as_str() {
108                PLAYREADY_SYSTEM_ID => SystemId::PlayReady,
109                WIDEVINE_SYSTEM_ID => SystemId::WideVine,
110                _ => SystemId::Other(system_id.to_owned()),
111            },
112        });
113        Ok(())
114    }
115}
116
117/// The DRM system identifier used in a PSSH box.
118#[derive(Debug, Clone, PartialEq)]
119pub enum SystemId {
120    /// Common SystemID (e.g. W3C Common Encryption).
121    Common,
122    /// Custom or unrecognized system identifier.
123    Other(String),
124    /// Microsoft PlayReady SystemID.
125    PlayReady,
126    /// Google Widevine SystemID.
127    WideVine,
128}
129
130impl std::fmt::Display for SystemId {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(
133            f,
134            "{}",
135            match self {
136                SystemId::Common => "cen",
137                SystemId::Other(x) => x,
138                SystemId::PlayReady => "prd",
139                SystemId::WideVine => "wvd",
140            }
141        )
142    }
143}
144
145/// The PSSH payload parsed from a `pssh` box, containing its binary data and keys.
146#[derive(Debug, Clone)]
147pub struct PsshData {
148    /// The full raw binary data of the parsed PSSH box.
149    pub data: Vec<u8>,
150    /// The hex-encoded Key IDs extracted from the PSSH box or payload.
151    pub key_ids: Vec<String>,
152    /// The DRM system associated with this PSSH box.
153    pub system_id: SystemId,
154}
155
156impl PartialEq for PsshData {
157    fn eq(&self, other: &Self) -> bool {
158        self.data == other.data
159    }
160}
161
162impl PsshData {
163    /// Encodes the binary PSSH box data into a base64 string.
164    pub fn as_base64(&self) -> String {
165        base64::engine::general_purpose::STANDARD.encode(&self.data)
166    }
167}