UNPKG

shaka-player

Version:
443 lines (401 loc) 13.4 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.drm.FairPlay'); goog.require('goog.Uri'); goog.require('goog.asserts'); goog.require('shaka.drm.DrmUtils'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Uint8ArrayUtils'); /** * @summary A set of FairPlay utility functions. * @export */ shaka.drm.FairPlay = class { /** * Check if FairPlay is supported. * * @return {!Promise<boolean>} * @export */ static async isFairPlaySupported() { const config = { initDataTypes: ['cenc', 'sinf', 'skd'], videoCapabilities: [ { contentType: 'video/mp4; codecs="avc1.42E01E"', }, ], }; try { await navigator.requestMediaKeySystemAccess('com.apple.fps', [config]); return true; } catch (err) { return false; } } /** * Using the default method, extract a content ID from the init data. This is * based on the FairPlay example documentation. * * @param {!BufferSource} initData * @return {string} * @export */ static defaultGetContentId(initData) { const uriString = shaka.util.StringUtils.fromBytesAutoDetect(initData); // The domain of that URI is the content ID according to Apple's FPS // sample. const uri = new goog.Uri(uriString); return uri.getDomain(); } /** * @param {string} uriString * @return {?string} */ static defaultGetKeyId(uriString) { const uri = new goog.Uri(uriString); const contentId = uri.getDomain(); const keyId = contentId.replace(/-/g, '').toLowerCase(); if (keyId.length === 32 && /^[0-9a-f]+$/.test(keyId)) { return keyId; } return null; } /** * Transforms the init data buffer using the given data. The format is: * * <pre> * [4 bytes] initDataSize * [initDataSize bytes] initData * [4 bytes] contentIdSize * [contentIdSize bytes] contentId * [4 bytes] certSize * [certSize bytes] cert * </pre> * * @param {!BufferSource} initData * @param {!BufferSource|string} contentId * @param {?BufferSource} cert The server certificate; this will throw if not * provided. * @return {!Uint8Array} * @export */ static initDataTransform(initData, contentId, cert) { if (!cert || !cert.byteLength) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.DRM, shaka.util.Error.Code.SERVER_CERTIFICATE_REQUIRED); } // From that, we build a new init data to use in the session. This is // composed of several parts. First, the init data as a UTF-16 sdk:// URL. // Second, a 4-byte LE length followed by the content ID in UTF-16-LE. // Third, a 4-byte LE length followed by the certificate. /** @type {BufferSource} */ let contentIdArray; if (typeof contentId == 'string') { contentIdArray = shaka.util.StringUtils.toUTF16(contentId, /* littleEndian= */ true); } else { contentIdArray = contentId; } // The init data we get is a UTF-8 string; convert that to a UTF-16 string. const sdkUri = shaka.util.StringUtils.fromBytesAutoDetect(initData); const utf16 = shaka.util.StringUtils.toUTF16(sdkUri, /* littleEndian= */ true); const rebuiltInitData = new Uint8Array( 12 + utf16.byteLength + contentIdArray.byteLength + cert.byteLength); let offset = 0; /** @param {BufferSource} array */ const append = (array) => { rebuiltInitData.set(shaka.util.BufferUtils.toUint8(array), offset); offset += array.byteLength; }; /** @param {BufferSource} array */ const appendWithLength = (array) => { const view = shaka.util.BufferUtils.toDataView(rebuiltInitData); const value = array.byteLength; view.setUint32(offset, value, /* littleEndian= */ true); offset += 4; append(array); }; appendWithLength(utf16); appendWithLength(contentIdArray); appendWithLength(cert); goog.asserts.assert( offset == rebuiltInitData.length, 'Inconsistent init data length'); return rebuiltInitData; } /** * Basic initDataTransform configuration. * * @param {!Uint8Array} initData * @param {string} initDataType * @param {?shaka.extern.DrmInfo} drmInfo * @return {!Uint8Array} * @private */ static basicInitDataTransform_(initData, initDataType, drmInfo) { if (initDataType !== 'skd') { return initData; } const StringUtils = shaka.util.StringUtils; const cert = drmInfo.serverCertificate; const initDataAsString = StringUtils.fromBytesAutoDetect(initData); const contentId = initDataAsString.split('skd://').pop(); return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert); } /** * Verimatrix initDataTransform configuration. * * @param {!Uint8Array} initData * @param {string} initDataType * @param {?shaka.extern.DrmInfo} drmInfo * @return {!Uint8Array} * @export */ static verimatrixInitDataTransform(initData, initDataType, drmInfo) { return shaka.drm.FairPlay.basicInitDataTransform_( initData, initDataType, drmInfo); } /** * EZDRM initDataTransform configuration. * * @param {!Uint8Array} initData * @param {string} initDataType * @param {?shaka.extern.DrmInfo} drmInfo * @return {!Uint8Array} * @export */ static ezdrmInitDataTransform(initData, initDataType, drmInfo) { if (initDataType !== 'skd') { return initData; } const StringUtils = shaka.util.StringUtils; const cert = drmInfo.serverCertificate; const initDataAsString = StringUtils.fromBytesAutoDetect(initData); const contentId = initDataAsString.split(';').pop(); return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert); } /** * Conax initDataTransform configuration. * * @param {!Uint8Array} initData * @param {string} initDataType * @param {?shaka.extern.DrmInfo} drmInfo * @return {!Uint8Array} * @export */ static conaxInitDataTransform(initData, initDataType, drmInfo) { if (initDataType !== 'skd') { return initData; } const StringUtils = shaka.util.StringUtils; const cert = drmInfo.serverCertificate; const initDataAsString = StringUtils.fromBytesAutoDetect(initData); const skdValue = initDataAsString.split('skd://').pop().split('?').shift(); const stringToArray = (string) => { // 2 bytes for each char const buffer = new ArrayBuffer(string.length * 2); const array = shaka.util.BufferUtils.toUint16(buffer); for (let i = 0, strLen = string.length; i < strLen; i++) { array[i] = string.charCodeAt(i); } return array; }; const contentId = stringToArray(window.atob(skdValue)); return shaka.drm.FairPlay.initDataTransform(initData, contentId, cert); } /** * ExpressPlay initDataTransform configuration. * * @param {!Uint8Array} initData * @param {string} initDataType * @param {?shaka.extern.DrmInfo} drmInfo * @return {!Uint8Array} * @export */ static expressplayInitDataTransform(initData, initDataType, drmInfo) { return shaka.drm.FairPlay.basicInitDataTransform_( initData, initDataType, drmInfo); } /** * Mux initDataTransform configuration. * * @param {!Uint8Array} initData * @param {string} initDataType * @param {?shaka.extern.DrmInfo} drmInfo * @return {!Uint8Array} * @export */ static muxInitDataTransform(initData, initDataType, drmInfo) { return shaka.drm.FairPlay.basicInitDataTransform_( initData, initDataType, drmInfo); } /** * Verimatrix FairPlay request. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shaka.extern.Request} request * @param {shaka.extern.RequestContext=} context * @export */ static verimatrixFairPlayRequest(type, request, context) { if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) { return; } const drmInfo = request.drmInfo; if (!drmInfo || !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) { return; } const body = /** @type {!(ArrayBuffer|ArrayBufferView)} */(request.body); const originalPayload = shaka.util.BufferUtils.toUint8(body); const base64Payload = shaka.util.Uint8ArrayUtils.toBase64(originalPayload); request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; request.body = shaka.util.StringUtils.toUTF8('spc=' + base64Payload); } /** * Set content-type to application/octet-stream in a FairPlay request. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shaka.extern.Request} request * @param {shaka.extern.RequestContext=} context * @private */ static octetStreamFairPlayRequest_(type, request, context) { if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) { return; } const drmInfo = request.drmInfo; if (!drmInfo || !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) { return; } request.headers['Content-Type'] = 'application/octet-stream'; } /** * EZDRM FairPlay request. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shaka.extern.Request} request * @param {shaka.extern.RequestContext=} context * @export */ static ezdrmFairPlayRequest(type, request, context) { shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request); } /** * Conax FairPlay request. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shaka.extern.Request} request * @param {shaka.extern.RequestContext=} context * @export */ static conaxFairPlayRequest(type, request, context) { shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request); } /** * ExpressPlay FairPlay request. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shaka.extern.Request} request * @param {shaka.extern.RequestContext=} context * @export */ static expressplayFairPlayRequest(type, request, context) { if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) { return; } const drmInfo = request.drmInfo; if (!drmInfo || !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) { return; } shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request); } /** * Mux FairPlay request. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shaka.extern.Request} request * @param {shaka.extern.RequestContext=} context * @export */ static muxFairPlayRequest(type, request, context) { shaka.drm.FairPlay.octetStreamFairPlayRequest_(type, request); } /** * Common FairPlay response transform for some DRMs providers. * * @param {shaka.net.NetworkingEngine.RequestType} type * @param {shaka.extern.Response} response * @param {shaka.extern.RequestContext=} context * @export */ static commonFairPlayResponse(type, response, context) { if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) { return; } const drmInfo = response.originalRequest.drmInfo; if (!drmInfo || !shaka.drm.DrmUtils.isFairPlayKeySystem(drmInfo.keySystem)) { return; } // In Apple's docs, responses can be of the form: // '\n<ckc>base64encoded</ckc>\n' or 'base64encoded' // We have also seen responses in JSON format from some of our partners. // In all of these text-based formats, the CKC data is base64-encoded. let responseText; try { // Convert it to text for further processing. responseText = shaka.util.StringUtils.fromUTF8(response.data); } catch (error) { // Assume it's not a text format of any kind and leave it alone. return; } let licenseProcessing = false; // Trim whitespace. responseText = responseText.trim(); // Look for <ckc> wrapper and remove it. if (responseText.substr(0, 5) === '<ckc>' && responseText.substr(-6) === '</ckc>') { responseText = responseText.slice(5, -6); licenseProcessing = true; } if (!licenseProcessing) { // Look for a JSON wrapper and remove it. try { const responseObject = /** @type {!Object} */(JSON.parse(responseText)); if (responseObject['ckc']) { responseText = responseObject['ckc']; licenseProcessing = true; } if (responseObject['CkcMessage']) { responseText = responseObject['CkcMessage']; licenseProcessing = true; } if (responseObject['License']) { responseText = responseObject['License']; licenseProcessing = true; } } catch (err) { // It wasn't JSON. Fall through with other transformations. } } if (licenseProcessing) { // Decode the base64-encoded data into the format the browser expects. // It's not clear why FairPlay license servers don't just serve this // directly. response.data = shaka.util.BufferUtils.toArrayBuffer( shaka.util.Uint8ArrayUtils.fromBase64(responseText)); } } };