import log from "../../../../log"; import arrayFind from "../../../../utils/array_find"; import type { IParsedRepresentation } from "../../types"; import type { IContentProtectionIntermediateRepresentation } from "../node_parser_types"; /** * Class whose purpose is to parse `` elements in a DASH MPD. * * This element has to be particularly considered because a ContentProtection * element can inherit another ContentProtection element coming before it yet * even after it in the MPD, through a system of "reference IDs". * * The idea here is that the main MPD parsing logic just needs to signal to this * class when a `ContentProtection` element is encountered - and to which * `Representation` it is associated, and this class will then perform the * ContentProtection-parsing operation as soon as it has all referenced * ContentProtection elements. * * @class ContentProtectionParser */ export default class ContentProtectionParser { /** * Stores `ContentProtection` elements that can be referenced, identified by * their `refId` attribute. */ private _refs: Map; /** * Storage of `ContentProtection` elements that could not yet have been parsed * due to not-yet resolved references. * * This is only possible if a `ContentProtection` element performs a * "forward-reference" to a later `ContentProtection` element that is not yet * known by this `ContentProtectionParser`. */ private _stored: Array< [IParsedRepresentation, IContentProtectionIntermediateRepresentation] >; constructor() { this._refs = new Map(); this._stored = []; } /** * Add new `IContentProtectionIntermediateRepresentation` objects that can * be relied on as a reference by later * `IContentProtectionIntermediateRepresentation` objects, without the need * to actually apply it to a Representation. * @param {Object} contentProtections */ public addReferences( contentProtections: IContentProtectionIntermediateRepresentation[], ): void { for (const contentProt of contentProtections) { if (contentProt.attributes.refId !== undefined) { this._refs.set(contentProt.attributes.refId, contentProt); } } } /** * Add a new `IContentProtectionIntermediateRepresentation` object that should * be parsed with the result linked to the given `IParsedRepresentation`. * @param {Object} representation * @param {Object} contentProt */ public add( representation: IParsedRepresentation, contentProt: IContentProtectionIntermediateRepresentation, ): void { if (!this._tryParsing(representation, contentProt, false)) { this._stored.push([representation, contentProt]); } if (contentProt.attributes.refId !== undefined) { this._refs.set(contentProt.attributes.refId, contentProt); this._resolveStoredRefs(false); } } /** * It is possible that even after parsing the full MPD, */ public finalize(): void { this._resolveStoredRefs(true); } /** * Try to parse all ContentProtection that are currently waiting due to a * referenced ContentProtection not being known yet. * * Return `true` if all ContentProtection references could have been found * and `false` if at least one wasn't. * * The `force` parameter indicate what should be done if a reference linked * to a ContentProtection couldn't be resolved: if `false`, we just keep that * ContentProtection aside for later, if `true` we parse it right now even if * information could be missing. * * @param {boolean} force * @returns {boolean} */ private _resolveStoredRefs(force: boolean): boolean { for (let i = this._stored.length - 1; i >= 0; i--) { const [representation, contentProt] = this._stored[i]; if (this._tryParsing(representation, contentProt, force) || force) { this._stored.splice(i, 1); } } return this._stored.length === 0; } /** * Parse the `IContentProtectionIntermediateRepresentation` given and add the * corresponding attributes to the given `IParsedRepresentation` when done. * * Because the `IContentProtectionIntermediateRepresentation` may be * referencing another `IContentProtectionIntermediateRepresentation`, this * method might not succeed to do so if the referenced * `IContentProtectionIntermediateRepresentation` has not yet been encountered. * * In that last scenario, this method returns `false` and: * - Either `force` is set to `true`, in which case what could be parsed * will still be set on the `IParsedRepresentation`. * - Either `force` is set to `false`, in which case the parsing of this * `IContentProtectionIntermediateRepresentation` is skipped. * @param {Object} representation * @param {Object} contentProt * @param {boolean} force * @returns {boolean} */ private _tryParsing( representation: IParsedRepresentation, contentProt: IContentProtectionIntermediateRepresentation, force: boolean, ): boolean { if (contentProt.attributes.ref === undefined) { // There's no reference, we can parse right away parseContentProtection(representation, contentProt); return true; } const referenced = this._getReferenced(contentProt.attributes.ref); if (referenced === undefined) { // Referenced ContentProtection not found, exit if (force) { log.warn("dash", "forcing the parsing of a referencing ContentProtection"); parseContentProtection(representation, contentProt); } return false; } // Referenced ContentProtection found, let's inherit its attributes contentProt.children.cencPssh.push(...referenced.children.cencPssh); if ( contentProt.attributes.keyId === undefined && referenced.attributes.keyId !== undefined ) { contentProt.attributes.keyId = referenced.attributes.keyId; } if ( contentProt.attributes.schemeIdUri === undefined && referenced.attributes.schemeIdUri !== undefined ) { contentProt.attributes.schemeIdUri = referenced.attributes.schemeIdUri; } if ( contentProt.attributes.value === undefined && referenced.attributes.value !== undefined ) { contentProt.attributes.value = referenced.attributes.value; } parseContentProtection(representation, contentProt); return true; } /** * Returns an `IContentProtectionIntermediateRepresentation` based on its * "refId". * Returns `undefined` if it is not known yet. * * @param {string} refId * @returns {Object|undefined} */ private _getReferenced( refId: string, ): IContentProtectionIntermediateRepresentation | undefined { return this._refs.get(refId); } } /** * Parses an `IContentProtectionIntermediateRepresentation` and update the * corresponding attributes on the given `IParsedRepresentation`. * @param {Object} representation * @param {Object} contentProtectionIr */ function parseContentProtection( representation: IParsedRepresentation, contentProtectionIr: IContentProtectionIntermediateRepresentation, ): void { let systemId: string | undefined; if ( contentProtectionIr.attributes.schemeIdUri !== undefined && contentProtectionIr.attributes.schemeIdUri.substring(0, 9) === "urn:uuid:" ) { systemId = contentProtectionIr.attributes.schemeIdUri .substring(9) .replace(/-/g, "") .toLowerCase(); } if ( contentProtectionIr.attributes.keyId !== undefined && contentProtectionIr.attributes.keyId.length > 0 ) { const kid = contentProtectionIr.attributes.keyId; if (representation.contentProtections === undefined) { representation.contentProtections = { keyIds: [kid], initData: [] }; } else if (representation.contentProtections.keyIds === undefined) { representation.contentProtections.keyIds = [kid]; } else { representation.contentProtections.keyIds.push(kid); } } if (systemId === undefined) { return; } const { cencPssh } = contentProtectionIr.children; const values: Array<{ systemId: string; data: Uint8Array }> = []; for (const data of cencPssh) { values.push({ systemId, data }); } if (values.length === 0) { return; } if (representation.contentProtections === undefined) { representation.contentProtections = { keyIds: [], initData: [{ type: "cenc", values }], }; return; } const cencInitData = arrayFind( representation.contentProtections.initData, (i) => i.type === "cenc", ); if (cencInitData === undefined) { representation.contentProtections.initData.push({ type: "cenc", values }); } else { cencInitData.values.push(...values); } }