pssh-tools
Version:
Tools to generate PSSH Data and PSSH Box
260 lines • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodePsshData = exports.decodePssh = exports.getPsshHeader = exports.system = void 0;
const path = require("path");
const protobuf = require("protobufjs");
exports.system = {
WIDEVINE: { id: 'EDEF8BA979D64ACEA3C827DCD51D21ED', name: 'Widevine' },
PLAYREADY: { id: '9A04F07998404286AB92E65BE0885F95', name: 'PlayReady' },
MARLIN: { id: '5E629AF538DA4063897797FFBD9902D4', name: 'Marlin' },
COMMON: { id: '1077EFECC0B24D02ACE33C1E52E2FB4B', name: 'Common' }
};
const decodeWidevineHeader = (data) => {
const protoFile = path.join(__dirname, 'proto', 'WidevineCencHeader.proto');
const root = protobuf.loadSync(protoFile);
const WidevineCencHeader = root.lookupType('proto.WidevineCencHeader');
const message = WidevineCencHeader.decode(data);
const header = WidevineCencHeader.toObject(message, {
enums: String,
bytes: String,
defaults: false,
arrays: false
});
return header;
};
const createPsshHeader = (version) => {
const psshHeaderBuffer = Buffer.from('pssh');
const versionBuffer = Buffer.alloc(2);
versionBuffer.writeInt16LE(version, 0);
const flagBuffer = Buffer.alloc(2);
flagBuffer.writeInt16BE(0, 0);
return Buffer.concat([psshHeaderBuffer, versionBuffer, flagBuffer]);
};
const getPsshHeader = (request) => {
const pssh = [];
const keyIds = request.keyIds || [];
// Set default to 0 for Widevine backward compatibility
let version = 0;
if (request.systemId !== exports.system.WIDEVINE.id && keyIds.length > 0) {
version = 1;
}
// pssh header
const psshHeader = createPsshHeader(version);
pssh.push(psshHeader);
// system id
const systemIdBuffer = Buffer.from(request.systemId, 'hex');
pssh.push(systemIdBuffer);
// key ids
if (version === 1) {
const keyCountBuffer = Buffer.alloc(4);
keyCountBuffer.writeInt32BE(keyIds.length, 0);
pssh.push(keyCountBuffer);
const kidsBufferArray = [];
for (let i = 0; i < keyIds.length; i++) {
kidsBufferArray.push(Buffer.from(keyIds[i], 'hex'));
}
const kidsBuffer = Buffer.concat(kidsBufferArray);
if (kidsBuffer.length > 0) {
pssh.push(kidsBuffer);
}
}
// data
const dataBuffer = Buffer.from(request.data, 'base64');
const dataSizeBuffer = Buffer.alloc(4);
dataSizeBuffer.writeInt32BE(dataBuffer.length, 0);
pssh.push(dataSizeBuffer);
pssh.push(dataBuffer);
// total size
const psshSizeBuffer = Buffer.alloc(4);
let totalLength = 4;
pssh.forEach((data) => {
totalLength += data.length;
});
psshSizeBuffer.writeInt32BE(totalLength, 0);
pssh.unshift(psshSizeBuffer);
return Buffer.concat(pssh).toString('base64');
};
exports.getPsshHeader = getPsshHeader;
const decodePssh = (data) => {
const result = {};
const decodedData = Buffer.from(data, 'base64');
// pssh header
const psshSize = Buffer.alloc(4);
decodedData.copy(psshSize, 0, 0, 4);
const psshHeader = Buffer.alloc(4);
decodedData.copy(psshHeader, 0, 4, 8);
// fullbox header
const headerVersion = Buffer.alloc(2);
decodedData.copy(headerVersion, 0, 8, 10);
const psshVersion = headerVersion.readInt16LE(0);
const headerFlag = Buffer.alloc(2);
decodedData.copy(headerFlag, 0, 10, 12);
// system id
const systemId = Buffer.alloc(16);
decodedData.copy(systemId, 0, 12, 28);
let dataStartPosition = 28;
let keyCountInt = 0;
if (psshVersion === 1) {
// key count
const keyCount = Buffer.alloc(4);
decodedData.copy(keyCount, 0, 28, 32);
keyCountInt = keyCount.readInt32BE(0);
if (keyCountInt > 0) {
result.keyIds = [];
for (let i = 0; i < keyCountInt; i++) {
// key id
const keyId = Buffer.alloc(16);
decodedData.copy(keyId, 0, 32 + (i * 16), 32 + ((i + 1) * 16));
result.keyIds.push(keyId.toString('hex'));
}
}
dataStartPosition = 32 + (16 * keyCountInt);
}
// data size
const dataSize = Buffer.alloc(4);
decodedData.copy(dataSize, 0, dataStartPosition, dataStartPosition + dataSize.length);
const psshDataSize = dataSize.readInt32BE(0);
// data
const psshData = Buffer.alloc(psshDataSize);
decodedData.copy(psshData, 0, dataStartPosition + dataSize.length, dataStartPosition + dataSize.length + psshData.length);
let systemName = '';
let widevineKeyCount = 0;
switch (systemId.toString('hex').toUpperCase()) {
case exports.system.WIDEVINE.id:
systemName = exports.system.WIDEVINE.name;
result.dataObject = decodeWVData(psshData);
widevineKeyCount = result.dataObject.widevineKeyCount || 0;
break;
case exports.system.PLAYREADY.id:
systemName = exports.system.PLAYREADY.name;
result.dataObject = decodePRData(psshData);
break;
case exports.system.MARLIN.id:
systemName = exports.system.MARLIN.name;
break;
default:
systemName = 'common';
}
result.systemId = systemId;
result.systemName = systemName;
result.version = psshVersion;
result.keyCount = keyCountInt + widevineKeyCount;
result.printPssh = () => {
// pssh version
const psshArray = [`PSSH Box v${psshVersion}`];
// system id
psshArray.push(` System ID: ${systemName} ${stringHexToGuid(systemId.toString('hex'))}`);
// key ids
if (result.keyIds && result.keyIds.length) {
psshArray.push(` Key IDs (${result.keyIds.length}):`);
result.keyIds.forEach((key) => {
const keyGuid = stringHexToGuid(key);
psshArray.push(` ${keyGuid}`);
});
}
// pssh data size
psshArray.push(` PSSH Data (size: ${psshDataSize}):`);
if (psshDataSize > 0) {
psshArray.push(` ${systemName} Data:`);
// widevine data
if (systemName === exports.system.WIDEVINE.name && result.dataObject) {
const dataObject = result.dataObject;
if (dataObject.keyId) {
psshArray.push(` Key IDs (${dataObject.keyId.length})`);
dataObject.keyId.forEach((key) => {
const keyGuid = stringHexToGuid(key);
psshArray.push(` ${keyGuid}`);
});
}
if (dataObject.provider) {
psshArray.push(` Provider: ${dataObject.provider}`);
}
if (dataObject.contentId) {
psshArray.push(' Content ID');
psshArray.push(` - UTF-8: ${Buffer.from(dataObject.contentId, 'hex').toString('utf8')}`);
psshArray.push(` - HEX : ${dataObject.contentId}`);
}
}
// playready data
if (systemName === exports.system.PLAYREADY.name && result.dataObject) {
const dataObject = result.dataObject;
psshArray.push(` Record size(${dataObject.recordSize})`);
if (dataObject.recordType) {
switch (dataObject.recordType) {
case 1:
psshArray.push(` Record Type: Rights Management Header (${dataObject.recordType})`);
break;
case 3:
psshArray.push(` Record Type: Embedded License Store (${dataObject.recordType})`);
break;
}
}
if (dataObject.recordXml) {
psshArray.push(' Record XML:');
psshArray.push(` ${dataObject.recordXml}`);
}
}
}
// line break
psshArray.push('\n');
return psshArray.join('\n');
};
return result;
};
exports.decodePssh = decodePssh;
const decodeWVData = (psshData) => {
// cenc header
const header = decodeWidevineHeader(psshData);
const wvData = {};
if (header.keyId && header.keyId.length > 0) {
wvData.widevineKeyCount = header.keyId.length;
const decodedKeys = header.keyId.map((key) => {
return Buffer.from(key, 'base64').toString('hex');
});
wvData.keyId = decodedKeys;
}
if (header.provider) {
wvData.provider = header.provider;
}
if (header.contentId) {
wvData.contentId = Buffer.from(header.contentId, 'base64').toString('hex').toUpperCase();
}
return wvData;
};
const decodePRData = (psshData) => {
// pro header
const proHeader = Buffer.alloc(10);
psshData.copy(proHeader, 0, 0, 10);
const proHeaderLength = proHeader.readInt32LE(0);
// let proRecordCount = proHeader.readInt16LE(4)
const proRecordType = proHeader.readInt16LE(6);
const proDataLength = proHeader.readInt16LE(8);
const proData = Buffer.alloc(proDataLength);
psshData.copy(proData, 0, 10, proHeaderLength);
return {
recordSize: proDataLength,
recordType: proRecordType,
recordXml: proData.toString('utf16le')
};
};
const stringHexToGuid = (value) => {
const guidArray = [];
guidArray.push(value.slice(0, 8));
guidArray.push(value.slice(8, 12));
guidArray.push(value.slice(12, 16));
guidArray.push(value.slice(16, 20));
guidArray.push(value.slice(20, 32));
return guidArray.join('-');
};
const decodePsshData = (targetSystem, data) => {
const dataBuffer = Buffer.from(data, 'base64');
if (exports.system.WIDEVINE.name === targetSystem) {
return decodeWVData(dataBuffer);
}
if (exports.system.PLAYREADY.name === targetSystem) {
return decodePRData(dataBuffer);
}
return null;
};
exports.decodePsshData = decodePsshData;
//# sourceMappingURL=tools.js.map