import { Parser } from 'binary-parser'; import { Buffer } from 'buffer'; import WRMHeader from './wrmheader'; const SYSTEM_ID = Buffer.from('9a04f07998404286ab92e65be0885f95', 'hex'); const PSSHBox = new Parser() .uint32('length') .string('pssh', { length: 4, assert: 'pssh' }) .uint32('fullbox') .buffer('system_id', { length: 16 }) .uint32('data_length') .buffer('data', { length: 'data_length', }); const PlayreadyObject = new Parser() .useContextVars() .uint16('type') .uint16('length') .choice('data', { tag: 'type', choices: { 1: new Parser().string('data', { length: function () { return (this as any).$parent.length; }, encoding: 'utf16le', }), }, defaultChoice: new Parser().buffer('data', { length: function () { return (this as any).$parent.length; }, }), }); const PlayreadyHeader = new Parser() .uint32('length') .uint16('record_count') .array('records', { length: 'record_count', type: PlayreadyObject, }); function isPlayreadyPsshBox(data: Buffer): boolean { if (data.length < 28) return false; return data.subarray(12, 28).equals(SYSTEM_ID); } function isUtf16(data: Buffer): boolean { for (let i = 1; i < data.length; i += 2) { if (data[i] !== 0) { return false; } } return true; } function* getWrmHeaders(wrm_header: any): IterableIterator { for (const record of wrm_header.records) { if (record.type === 1 && typeof record.data === 'string') { yield record.data; } } } export class PSSH { public wrm_headers: string[]; constructor(data: string | Buffer) { if (!data) { throw new Error('Data must not be empty'); } if (typeof data === 'string') { try { data = Buffer.from(data, 'base64'); } catch (e) { throw new Error(`Could not decode data as Base64: ${e}`); } } try { if (isPlayreadyPsshBox(data)) { const pssh_box = PSSHBox.parse(data); const psshData = pssh_box.data; if (isUtf16(psshData)) { this.wrm_headers = [psshData.toString('utf16le')]; } else if (isUtf16(psshData.subarray(6))) { this.wrm_headers = [psshData.subarray(6).toString('utf16le')]; } else if (isUtf16(psshData.subarray(10))) { this.wrm_headers = [psshData.subarray(10).toString('utf16le')]; } else { const playready_header = PlayreadyHeader.parse(psshData); this.wrm_headers = Array.from(getWrmHeaders(playready_header)); } } else { if (isUtf16(data)) { this.wrm_headers = [data.toString('utf16le')]; } else if (isUtf16(data.subarray(6))) { this.wrm_headers = [data.subarray(6).toString('utf16le')]; } else if (isUtf16(data.subarray(10))) { this.wrm_headers = [data.subarray(10).toString('utf16le')]; } else { const playready_header = PlayreadyHeader.parse(data); this.wrm_headers = Array.from(getWrmHeaders(playready_header)); } } } catch (e) { throw new Error( 'Could not parse data as a PSSH Box nor a PlayReadyHeader' ); } } // Header downgrade public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { return this.wrm_headers.map( downgrade_to_v4 ? this.downgradePSSH : (_) => _ ); } private downgradePSSH(wrm_header: string): string { const header = new WRMHeader(wrm_header); return header.to_v4_0_0_0(); } }