2019/lib/eme/encryptedMediaPortability.js (210 lines of code) (raw):

/** * @license * Copyright 2018 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; /* The code tries to wrapper the EME versions with or without webkit prefix */ function prefixedAttributeName(obj, suffix, opt_preprefix) { suffix = suffix.toLowerCase(); if (opt_preprefix === undefined) { opt_preprefix = ''; } for (var attr in obj) { var lattr = attr.toLowerCase(); if (lattr.indexOf(opt_preprefix) == 0 && lattr.indexOf(suffix, lattr.length - suffix.length) != -1) { return attr; } } return null; } function normalizeAttribute(obj, suffix, opt_preprefix) { if (opt_preprefix === undefined) { opt_preprefix = ''; } var attr = prefixedAttributeName(obj, suffix, opt_preprefix); if (attr) { obj[opt_preprefix + suffix] = obj[attr]; } } function stringToArray(s) { var array = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) { array[i] = s.charCodeAt(i); } return array; } function arrayToString(a) { return String.fromCharCode.apply(String, a); } function parsePlayReadyKeyMessage(message) { // message is UTF-16LE, let's throw out every second element. var message_ascii = new Array(); for (var i = 0; i < message.length; i += 2) { message_ascii.push(message[i]); } var str = String.fromCharCode.apply(String, message_ascii); var doc = (new DOMParser()).parseFromString(str, 'text/xml'); return stringToArray( atob(doc.childNodes[0].childNodes[0].childNodes[0].childNodes[0].data)); } /** * Counts pssh atoms in init data. * @param {ArrayBuffer} initData Init data for the media segment. * @return {Number} Returns the count of pssh atoms in initData. */ function countPsshAtoms(initData) { // Accessing the Uint8Array's underlying ArrayBuffer is impossible, so we // copy it to a new one for parsing. var abuf = new ArrayBuffer(initData.length); var view = new Uint8Array(abuf); view.set(initData); var psshCount = 0; var dv = new DataView(abuf); var pos = 0; while (pos < abuf.byteLength) { var box_size = dv.getUint32(pos, false); var type = dv.getUint32(pos + 4, false); if (type == 0x70737368) { psshCount++; } pos += box_size; } return psshCount; } /** * Extracts the BMFF Key ID from the init data of the segment. * @param {ArrayBuffer} initData Init data for the media segment. * @param {boolean} clearkey Method will return clear key ID if true. * @param {boolean} widevine Method will return widevien key ID if true. * @return {ArrayBuffer} Returns the BMFF Key ID if found, otherwise false. */ function extractBMFFKeyID(initData, clearkey, widevine) { // Accessing the Uint8Array's underlying ArrayBuffer is impossible, so we // copy it to a new one for parsing. var abuf = new ArrayBuffer(initData.length); var view = new Uint8Array(abuf); view.set(initData); var dv = new DataView(abuf); var pos = 0; while (pos < abuf.byteLength) { var box_size = dv.getUint32(pos, false); var type = dv.getUint32(pos + 4, false); if (type != 0x70737368) throw 'Box type ' + type.toString(16) + ' not equal to "pssh"'; // Scan for Clear Key header. if (clearkey) { if ((dv.getUint32(pos + 12, false) == 0x58147ec8) && (dv.getUint32(pos + 16, false) == 0x04234659) && (dv.getUint32(pos + 20, false) == 0x92e6f52c) && (dv.getUint32(pos + 24, false) == 0x5ce8c3cc)) { var size = dv.getUint32(pos + 28, false); if (size != 16) throw 'Unexpected KID size ' + size; return new Uint8Array(abuf.slice(pos + 32, pos + 32 + size)); } } // Scan for Widevine header. if (widevine) { if ((dv.getUint32(pos + 12, false) == 0xedef8ba9) && (dv.getUint32(pos + 16, false) == 0x79d64ace) && (dv.getUint32(pos + 20, false) == 0xa3c827dc) && (dv.getUint32(pos + 24, false) == 0xd51d21ed)) { return new Uint8Array(abuf.slice(pos + 36, pos + 52)); } } pos += box_size; } return false; } /** * Extracts the BMFF Clear Key ID from the init data of the segment. * @param {ArrayBuffer} initData Init data for the media segment. * @return {ArrayBuffer} Returns the BMFF ClearKey ID if found, otherwise the * initData itself. */ function extractBMFFClearKeyID(initData) { return extractBMFFKeyID(initData, true, false); } /** * Extracts the BMFF Clear Key ID from the init data of the segment. * @param {ArrayBuffer} initData Init data for the media segment. * @return {ArrayBuffer} Returns the BMFF ClearKey ID if found, otherwise the * initData itself. */ function extractBMFFWidevineKeyID(initData) { return extractBMFFKeyID(initData, false, true); } /** * Add BMFF Clear Key ID to initialization data. * @param {ArrayBuffer} initData Init data for the media segment. * @param {ArrayBuffer} clearKeyId Clear Key ID to add to initData. * @return {ArrayBuffer} Returns initData with a Clear Key ID added. */ function addBMFFClearKeyID(initData, clearkeyId) { // Accessing the Uint8Array's underlying ArrayBuffer is impossible, so we // copy it to a new one for parsing. var abuf = new ArrayBuffer(initData.length); var view = new Uint8Array(abuf); view.set(initData); // Find end of last pssh atom. var dv = new DataView(abuf); var pssh_end; var pos = 0; while (pos < abuf.byteLength) { var box_size = dv.getUint32(pos, false); var type = dv.getUint32(pos + 4, false); pos += box_size; if (type == 0x70737368) pssh_end = pos; } var clearkeyPssh = new Uint8Array(48); clearkeyPssh.set([0x00, 0x00, 0x00, 0x30, 0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, 0x58, 0x14, 0x7e, 0xc8, 0x04, 0x23, 0x46, 0x59, 0x92, 0xe6, 0xf5, 0x2c, 0x5c, 0xe8, 0xc3, 0xcc, 0x00, 0x00, 0x00, 0x10]); clearkeyPssh.set(clearkeyId, 32); var newInitData = new Uint8Array(abuf.byteLength + clearkeyPssh.byteLength); newInitData.set(new Uint8Array(abuf.slice(0, pssh_end)), 0); newInitData.set(clearkeyPssh, pssh_end); newInitData.set(abuf.slice(pssh_end), pssh_end + clearkeyPssh.byteLength); return newInitData; } function base64_encode(arr) { var b64dict = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var b64pad = "="; var i; var hexStr = ""; for (i = 0; i < arr.length; i++) { var hex = arr[i].toString(16); hexStr += hex.length == 1 ? "0" + hex : hex; } var dest = ""; var c; for (i = 0; i+3 <= hexStr.length; i+=3) { c = parseInt(hexStr.substring(i, i+3), 16); dest += b64dict.charAt(c >> 6) + b64dict.charAt(c & 63); } if (i+1 == hexStr.length) { c = parseInt(hexStr.substring(i, i+1), 16); dest += b64dict.charAt(c << 2); } else if (i+2 == hexStr.length) { c = parseInt(hexStr.substring(i, i+2), 16); dest += b64dict.charAt(c >> 2) + b64dict.charAt((c & 3) << 4); } while ((dest.length & 3) > 0) { dest += b64pad; } return dest; } /** * Caution: This function does not work with Cobalt browser. */ function base64_decode(base64Str) { var binaryStr = atob(base64Str); var arr = stringToArray(binaryStr); return arr.buffer; } function b64tob64url(s) { var b64urlStr = removePadding(s); b64urlStr = b64urlStr.replace(/\+/g, "-"); b64urlStr = b64urlStr.replace(/\//g, "_"); return b64urlStr; } function removePadding(s) { return s.replace(/\=/g, ""); } function base64NoPadding_encode(arr) { return removePadding(base64_encode(arr)); } function base64url_encode(arr) { return b64tob64url(base64_encode(arr)); } (function() { var dlog = function() { var forward = window.dlog || console.log.bind(console); forward.apply(this, arguments); }; var proto = window.HTMLMediaElement.prototype; if (proto.addKey || proto.setMediaKeys) { return; // Non-prefix version, needn't wrap. } if (!proto.webkitAddKey) { dlog(1, 'EME is not available'); return; // EME is not available at all. } proto.generateKeyRequest = function(keySystem, initData) { return proto.webkitGenerateKeyRequest.call(this, keySystem, initData); }; proto.addKey = function(keySystem, key, initData, sessionId) { return proto.webkitAddKey.call(this, keySystem, key, initData, sessionId); }; proto.cancelKeyRequest = function(keySystem, sessionId) { return proto.webkitCancelKeyRequest.call(this, keySystem, sessionId); }; var ael = proto.addEventListener; var eventWrapper = function(listener, e) { listener.call(this, e); }; proto.addEventListener = function(type, listener, useCaptures) { var re = /^(keyadded|keyerror|keymessage|needkey)$/; var match = re.exec(type); if (match) { ael.call(this, 'webkit' + type, eventWrapper.bind(this, listener), useCaptures); } else { ael.call(this, type, listener, useCaptures); } }; })();