diff --git a/docs/API.md b/docs/API.md index e389d920df6..b23b73719c6 100644 --- a/docs/API.md +++ b/docs/API.md @@ -75,6 +75,11 @@ - [`abrBandWidthUpFactor`](#abrbandwidthupfactor) - [`abrMaxWithRealBitrate`](#abrmaxwithrealbitrate) - [`minAutoBitrate`](#minautobitrate) + - [`emeEnabled`](#emeenabled) + - [`licenseXhrSetup`](#licensexhrsetup) + - [`drmSystem`](#drmsystem) + - [`widevineLicenseUrl`](#widevinelicenseurl) + - [`playreadyLicenseUrl`](#playreadylicenseurl) - [Video Binding/Unbinding API](#video-bindingunbinding-api) - [`hls.attachMedia(videoElement)`](#hlsattachmediavideoelement) - [`hls.detachMedia()`](#hlsdetachmedia) @@ -972,6 +977,47 @@ then if config value is set to `true`, ABR will use 2.5 Mb/s for this quality le Return the capping/min bandwidth value that could be used by automatic level selection algorithm. Useful when browser or tab of the browser is not in the focus and bandwidth drops +### `emeEnabled` + +(default: `false`) + +Whether or not to enable eme. + +### `licenseXhrSetup` + +(default: `undefined`) + +`XMLHttpRequest` customization callback for default XHR based loader. + +Parameter should be a function with two arguments `(xhr: XMLHttpRequest, url: string)`. +If `licenseXhrSetup` is specified, default loader will invoke it before calling `xhr.send()`. +This allows user to easily modify/setup XHR. See example below. + +```js + var config = { + licenseXhrSetup: function(xhr, url) { + xhr.setRequestHeader('Content-Type', 'text/xml; charset=utf-8'); //indicate resource is xml + } + } +``` + +### `drmSystem` + +(default: `undefined`) + +Which DRM system use. (`WIDEVINE` or `PLAYREADY`) + +### `widevineLicenseUrl` + +(default: `undefined`) + +Specify widevine license url. + +### `playreadyLicenseUrl` + +(default: `undefined`) + +Specify playready license url. ## Video Binding/Unbinding API diff --git a/src/config.js b/src/config.js index 22d31a52a0c..02c6b688dfb 100644 --- a/src/config.js +++ b/src/config.js @@ -90,6 +90,8 @@ export var hlsDefaultConfig = { minAutoBitrate: 0, // used by hls emeEnabled: false, // used by eme-controller widevineLicenseUrl: undefined, // used by eme-controller + drmSystem: undefined, // used by eme-controller + playreadyLicenseUrl: undefined, // used by eme-controller requestMediaKeySystemAccessFunc: requestMediaKeySystemAccess // used by eme-controller }; diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index df27575d5bc..57957de0aeb 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -7,7 +7,7 @@ import EventHandler from '../event-handler'; import Event from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; - +import { base64ToUint8Array, buildPlayReadyPSSHBox, makePlayreadyHeaders } from '../utils/eme-helper'; import { logger } from '../utils/logger'; const MAX_LICENSE_REQUEST_FAILURES = 3; @@ -20,6 +20,20 @@ enum KeySystems { PLAYREADY = 'com.microsoft.playready', } +enum DRMIdentifiers { + WIDEVINE = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + PLAYREADY = 'com.microsoft.playready' +} + +/* +* https://www.w3.org/TR/eme-initdata-registry/ +*/ +enum InitDataTypes { + COMMON_ENCRYPTION = 'cenc', + KEY_IDS = 'keyids', + WEBM = 'webm' +} + /** * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration * @param {Array} audioCodecs List of required audio codecs to support @@ -64,6 +78,7 @@ const createWidevineMediaKeySystemConfigurations = function (audioCodecs: string const getSupportedMediaKeySystemConfigurations = function (keySystem: KeySystems, audioCodecs: string[], videoCodecs: string[]): MediaKeySystemConfiguration[] { switch (keySystem) { case KeySystems.WIDEVINE: + case KeySystems.PLAYREADY: return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs); default: throw new Error(`Unknown key-system: ${keySystem}`); @@ -71,8 +86,8 @@ const getSupportedMediaKeySystemConfigurations = function (keySystem: KeySystems }; interface MediaKeysListItem { - mediaKeys?: MediaKeys, - mediaKeysSession?: MediaKeySession, + mediaKeys?: MediaKeys; + mediaKeysSession?: MediaKeySession; mediaKeysSessionInitialized: boolean; mediaKeySystemAccess: MediaKeySystemAccess; mediaKeySystemDomain: KeySystems; @@ -87,14 +102,20 @@ interface MediaKeysListItem { */ class EMEController extends EventHandler { private _widevineLicenseUrl: string; + private _playreadyLicenseUrl: string; private _licenseXhrSetup: (xhr: XMLHttpRequest, url: string) => void; - private _emeEnabled: boolean; - private _requestMediaKeySystemAccess: (keySystem: KeySystems, supportedConfigurations: MediaKeySystemConfiguration[]) => Promise - - private _mediaKeysList: MediaKeysListItem[] = [] + private _requestMediaKeySystemAccess: (keySystem: KeySystems, supportedConfigurations: MediaKeySystemConfiguration[]) => Promise; + private _mediaKeysList: MediaKeysListItem[] = []; private _media: HTMLMediaElement | null = null; private _hasSetMediaKeys: boolean = false; private _requestLicenseFailureCount: number = 0; + private _initData: Uint8Array | null = null; + private _initDataType: string = ''; + private _mediaKeys: MediaKeys | undefined; + private _audioCodecs: Array = []; + private _videoCodecs: Array = []; + private _haveKeySession: boolean = false; + private _selectedDrm: string = ''; /** * @constructs @@ -104,12 +125,14 @@ class EMEController extends EventHandler { super(hls, Event.MEDIA_ATTACHED, Event.MEDIA_DETACHED, - Event.MANIFEST_PARSED + Event.MANIFEST_PARSED, + Event.FRAG_LOADED ); this._widevineLicenseUrl = hls.config.widevineLicenseUrl; + this._playreadyLicenseUrl = hls.config.playreadyLicenseUrl; this._licenseXhrSetup = hls.config.licenseXhrSetup; - this._emeEnabled = hls.config.emeEnabled; + this._selectedDrm = hls.config.drmSystem; this._requestMediaKeySystemAccess = hls.config.requestMediaKeySystemAccessFunc; } @@ -122,6 +145,8 @@ class EMEController extends EventHandler { switch (keySystem) { case KeySystems.WIDEVINE: return this._widevineLicenseUrl; + case KeySystems.PLAYREADY: + return this._playreadyLicenseUrl; } throw new Error(`no license server URL configured for key-system "${keySystem}"`); @@ -176,19 +201,31 @@ class EMEController extends EventHandler { mediaKeySystemDomain: keySystem }; - this._mediaKeysList.push(mediaKeysListItem); + // If no MediaKeys exist, create one, otherwise re-use the same one + if (this._mediaKeysList.length === 0) { + mediaKeySystemAccess.createMediaKeys() + .then((mediaKeys) => { + this._mediaKeysList.push(mediaKeysListItem); + mediaKeysListItem.mediaKeys = mediaKeys; + this._mediaKeys = mediaKeys; + logger.log(`Media-keys created for key-system "${keySystem}"`); + this._onMediaKeysCreated(); + }) + .catch((err) => { + logger.error('Failed to create media-keys:', err); + }); - mediaKeySystemAccess.createMediaKeys() - .then((mediaKeys) => { - mediaKeysListItem.mediaKeys = mediaKeys; + return; + } - logger.log(`Media-keys created for key-system "${keySystem}"`); + this._mediaKeysList.push({ + mediaKeys: this._mediaKeys, + mediaKeysSessionInitialized: false, + mediaKeySystemAccess: mediaKeySystemAccess, + mediaKeySystemDomain: keySystem + }); - this._onMediaKeysCreated(); - }) - .catch((err) => { - logger.error('Failed to create media-keys:', err); - }); + this._onMediaKeysCreated(); } /** @@ -203,6 +240,7 @@ class EMEController extends EventHandler { if (!mediaKeysListItem.mediaKeysSession) { // mediaKeys is definitely initialized here mediaKeysListItem.mediaKeysSession = mediaKeysListItem.mediaKeys!.createSession(); + this._haveKeySession = true; this._onNewMediaKeySession(mediaKeysListItem.mediaKeysSession); } }); @@ -239,7 +277,7 @@ class EMEController extends EventHandler { * @param {string} initDataType * @param {ArrayBuffer|null} initData */ - private _onMediaEncrypted = (e: MediaEncryptedEvent) => { + private _onMediaEncrypted = (e: any) => { logger.log(`Media is encrypted using "${e.initDataType}" init data type`); this._attemptSetMediaKeys(); @@ -256,7 +294,8 @@ class EMEController extends EventHandler { if (!this._hasSetMediaKeys) { // FIXME: see if we can/want/need-to really to deal with several potential key-sessions? - const keysListItem = this._mediaKeysList[0]; + const keysListItem = this._getMediaKeys(); + if (!keysListItem || !keysListItem.mediaKeys) { logger.error('Fatal: Media is encrypted but no CDM access or no keys have been obtained yet'); this.hls.trigger(Event.ERROR, { @@ -268,18 +307,27 @@ class EMEController extends EventHandler { } logger.log('Setting keys for encrypted media'); - this._media.setMediaKeys(keysListItem.mediaKeys); this._hasSetMediaKeys = true; } } + /** + * @private + * @returns {MediaKeysListItem} + */ + private _getMediaKeys () { + return this._mediaKeysList[this._mediaKeysList.length - 1]; + } + /** * @private */ private _generateRequestWithPreferredKeySession (initDataType: string, initData: ArrayBuffer | null) { // FIXME: see if we can/want/need-to really to deal with several potential key-sessions? - const keysListItem = this._mediaKeysList[0]; + + const keysListItem = this._getMediaKeys(); + if (!keysListItem) { logger.error('Fatal: Media is encrypted but not any key-system access has been obtained yet'); this.hls.trigger(Event.ERROR, { @@ -357,7 +405,7 @@ class EMEController extends EventHandler { // Because we set responseType to ArrayBuffer here, callback is typed as handling only array buffers xhr.responseType = 'arraybuffer'; xhr.onreadystatechange = - this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback); + this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback); return xhr; } @@ -403,33 +451,21 @@ class EMEController extends EventHandler { * @private * @param {MediaKeysListItem} keysListItem * @param {ArrayBuffer} keyMessage - * @returns {ArrayBuffer} Challenge data posted to license server + * @returns {any} Challenge data posted to license server * @throws if KeySystem is unsupported */ - private _generateLicenseRequestChallenge (keysListItem: MediaKeysListItem, keyMessage: ArrayBuffer): ArrayBuffer { + private _generateLicenseRequestChallenge (keysListItem: MediaKeysListItem, keyMessage: ArrayBuffer) { switch (keysListItem.mediaKeySystemDomain) { - // case KeySystems.PLAYREADY: - // from https://github.com/MicrosoftEdge/Demos/blob/master/eme/scripts/demo.js - /* - if (this.licenseType !== this.LICENSE_TYPE_WIDEVINE) { - // For PlayReady CDMs, we need to dig the Challenge out of the XML. - var keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml'); - if (keyMessageXml.getElementsByTagName('Challenge')[0]) { - challenge = atob(keyMessageXml.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue); - } else { - throw 'Cannot find in key message'; - } - var headerNames = keyMessageXml.getElementsByTagName('name'); - var headerValues = keyMessageXml.getElementsByTagName('value'); - if (headerNames.length !== headerValues.length) { - throw 'Mismatched header / pair in key message'; - } - for (var i = 0; i < headerNames.length; i++) { - xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue); - } + case KeySystems.PLAYREADY: + // from https://github.com/MicrosoftEdge/Demos/blob/master/eme/scripts/demo.js + // For PlayReady CDMs, we need to dig the Challenge out of the XML. + const keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml'); + const challengeElement = keyMessageXml.querySelector('Challenge'); + + if (!challengeElement || !challengeElement.textContent) { + throw new Error('Cannot find in key message'); } - break; - */ + return atob(challengeElement.textContent); case KeySystems.WIDEVINE: // For Widevine CDMs, the challenge is the keyMessage. return keyMessage; @@ -446,7 +482,8 @@ class EMEController extends EventHandler { private _requestLicense (keyMessage: ArrayBuffer, callback: (data: ArrayBuffer) => void) { logger.log('Requesting content license for key-system'); - const keysListItem = this._mediaKeysList[0]; + const keysListItem = this._getMediaKeys(); + if (!keysListItem) { logger.error('Fatal error: Media is encrypted but no key-system access has been obtained yet'); this.hls.trigger(Event.ERROR, { @@ -460,8 +497,20 @@ class EMEController extends EventHandler { try { const url = this.getLicenseServerUrl(keysListItem.mediaKeySystemDomain); const xhr = this._createLicenseXhr(url, keyMessage, callback); - logger.log(`Sending license request to URL: ${url}`); const challenge = this._generateLicenseRequestChallenge(keysListItem, keyMessage); + + if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) { + const playReadyHeaders = makePlayreadyHeaders(keyMessage); + + if (playReadyHeaders.length > 0) { + playReadyHeaders.forEach((header) => { + xhr.setRequestHeader(header[0], header[1]); + }); + } + } + + logger.log(`Sending license request to URL: ${url}`); + xhr.send(challenge); } catch (e) { logger.error(`Failure requesting DRM license: ${e}`); @@ -473,17 +522,43 @@ class EMEController extends EventHandler { } } - onMediaAttached (data: { media: HTMLMediaElement; }) { - if (!this._emeEnabled) { - return; + /** + * @private + * @param {Array}drmInfo + */ + private _processInitData (drmInfo) { + const drmIdentifier = DRMIdentifiers[this._selectedDrm]; + + const selectedDrm = drmInfo.filter(levelkey => levelkey.format === drmIdentifier); + const levelkey = selectedDrm.shift(); + + const details = levelkey.reluri.split(','); + const encoding = details[0]; + const pssh = details[1]; + + if (drmIdentifier === 'com.microsoft.playready' && encoding.includes('base64')) { + this._initData = buildPlayReadyPSSHBox(base64ToUint8Array(pssh)); // Playready is particular about the pssh box, so it needs to be handcrafted. + } else if (drmIdentifier === 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed' && encoding.includes('base64')) { + this._initData = base64ToUint8Array(pssh); // Widevine pssh box } + this._initDataType = InitDataTypes.COMMON_ENCRYPTION; + } + + onMediaAttached (data: { media: HTMLMediaElement; }) { const media = data.media; // keep reference of media this._media = media; - media.addEventListener('encrypted', this._onMediaEncrypted); + // FIXME: also handle detaching media ! + if (!this._hasSetMediaKeys) { + media.addEventListener('encrypted', (e) => { + if (e.initData) { + this._onMediaEncrypted(e); + } + }); + } } onMediaDetached () { @@ -493,16 +568,24 @@ class EMEController extends EventHandler { } } - // TODO: Use manifest types here when they are defined onManifestParsed (data: any) { - if (!this._emeEnabled) { - return; - } + this._audioCodecs = data.levels.map((level) => level.audioCodec); + this._videoCodecs = data.levels.map((level) => level.videoCodec); + } - const audioCodecs = data.levels.map((level) => level.audioCodec); - const videoCodecs = data.levels.map((level) => level.videoCodec); + onFragLoaded (data) { + const frag = data.frag; - this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs); + // If new DRM keys exist, let's try to create MediaKeysObject, let's process initData + if (frag.foundKeys) { + this._attemptKeySystemAccess(KeySystems[this._selectedDrm], this._audioCodecs, this._videoCodecs); + this._processInitData(frag.drmInfo); + } + + // add initData and type if they are included in playlist, also wait for keysession + if (this._initData && this._haveKeySession) { + this._onMediaEncrypted({ initDataType: this._initDataType, initData: this._initData }); + } } } diff --git a/src/hls.js b/src/hls.js index 48b95825965..de0b8e083c1 100644 --- a/src/hls.js +++ b/src/hls.js @@ -198,7 +198,7 @@ export default class Hls extends Observer { } Controller = config.emeController; - if (Controller) { + if (Controller && config.emeEnabled) { const emeController = new Controller(this); /** diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index f499a9edafd..08139084eec 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -155,6 +155,8 @@ export default class M3U8Parser { let result; let i; + let drmInfo = []; + let firstPdtIndex = null; LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0; @@ -166,7 +168,7 @@ export default class M3U8Parser { // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 const title = (' ' + result[2]).slice(1); frag.title = title || null; - frag.tagList.push(title ? [ 'INF', duration, title ] : [ 'INF', duration ]); + frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]); } else if (result[3]) { // url if (Number.isFinite(frag.duration)) { const sn = currentSN++; @@ -178,15 +180,17 @@ export default class M3U8Parser { frag.cc = cc; frag.urlId = levelUrlId; frag.baseurl = baseurl; + frag.drmInfo = (drmInfo && drmInfo.length > 0) ? drmInfo : (prevFrag ? prevFrag.drmInfo : []); + frag.foundKeys = !!drmInfo.length; // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 frag.relurl = (' ' + result[3]).slice(1); assignProgramDateTime(frag, prevFrag); - level.fragments.push(frag); prevFrag = frag; totalduration += frag.duration; - frag = new Fragment(); + // once captured, array needs to be reset and rely on previous fragment until new keys are available + drmInfo = []; } } else if (result[4]) { // X-BYTERANGE frag.rawByteRange = (' ' + result[4]).slice(1); @@ -217,7 +221,7 @@ export default class M3U8Parser { switch (result[i]) { case '#': - frag.tagList.push(value2 ? [ value1, value2 ] : [ value1 ]); + frag.tagList.push(value2 ? [value1, value2] : [value1]); break; case 'PLAYLIST-TYPE': level.type = value1.toUpperCase(); @@ -252,15 +256,18 @@ export default class M3U8Parser { decryptiv = keyAttrs.hexadecimalInteger('IV'); if (decryptmethod) { levelkey = new LevelKey(); - if ((decrypturi) && (['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(decryptmethod) >= 0)) { + if ((decrypturi) && (['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC', 'SAMPLE-AES-CTR'].indexOf(decryptmethod) >= 0)) { levelkey.method = decryptmethod; // URI to get the key levelkey.baseuri = baseurl; levelkey.reluri = decrypturi; levelkey.key = null; + levelkey.format = keyAttrs.KEYFORMAT; // Initialization Vector (IV) levelkey.iv = decryptiv; } + + drmInfo.push(levelkey); } break; case 'START': @@ -302,6 +309,7 @@ export default class M3U8Parser { level.endSN = currentSN - 1; level.startCC = level.fragments[0] ? level.fragments[0].cc : 0; level.endCC = cc; + level.drmInfo = drmInfo; if (!level.initSegment && level.fragments.length) { // this is a bit lurky but HLS really has no other way to tell us diff --git a/src/utils/eme-helper.js b/src/utils/eme-helper.js new file mode 100644 index 00000000000..df712797f73 --- /dev/null +++ b/src/utils/eme-helper.js @@ -0,0 +1,71 @@ +/** + * @param {string} base64String base64 encoded string + * @returns {Uint8Array} + */ +export function base64ToUint8Array (base64String) { + let binaryString = window.atob(base64String); + let len = binaryString.length; + let bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + return bytes; +} + +/** + * https://www.w3.org/TR/eme-initdata-cenc/ + * @param {ArrayBuffer | ArrayBufferView} binary data from the URI in the manifest + * @returns {Uint8Array} + */ +export function buildPlayReadyPSSHBox (binary) { + // https://dashif.org/identifiers/protection/ playready uuid: 9a04f079-9840-4286-ab92-e65be0885f95 + const uuid = ['0x9A04F079', '0x98404286', '0xAB92E65B', '0xE0885F95']; + const psshBoxType = '0x70737368'; + const psshBoxVersion = 0; + const uriData = binary; + + // create an ArrayBuffer with size of uriData and 32 bytes in padding + const psshArray = new ArrayBuffer(uriData.length + 32); + + // create a data view with PSSH array buffer + const data = new DataView(psshArray); + + // convert to unsigned 8 bit array + const pssh = new Uint8Array(psshArray); + + data.setUint32(0, psshArray.byteLength); + data.setUint32(4, psshBoxType); + data.setUint32(8, psshBoxVersion); + data.setUint32(12, uuid[0]); + data.setUint32(16, uuid[1]); + data.setUint32(20, uuid[2]); + data.setUint32(24, uuid[3]); + data.setUint32(28, uriData.length); + + pssh.set(uriData, 32); + + return pssh; +} + +/* +* @param {ArrayBuffer} keyMessage +* @returns {Array<[string, string]>} playReadyHeaders +*/ +export function makePlayreadyHeaders (keyMessage) { + const xmlContent = String.fromCharCode.apply(null, new Uint16Array(keyMessage)); + const parser = new window.DOMParser(); + const keyMessageXml = parser.parseFromString(xmlContent, 'application/xml'); + const headers = keyMessageXml.querySelectorAll('HttpHeader'); + const playReadyHeaders = []; + + let header; + + for (let i = 0, len = headers.length; i < len; i++) { + header = headers[i]; + playReadyHeaders.push([header.querySelector('name').textContent, header.querySelector('value').textContent]); + } + + return playReadyHeaders; +} diff --git a/tests/test-streams.js b/tests/test-streams.js index 81f58d8ff99..29892af9d59 100644 --- a/tests/test-streams.js +++ b/tests/test-streams.js @@ -147,7 +147,8 @@ module.exports = { }, { widevineLicenseUrl: 'https://cwip-shaka-proxy.appspot.com/no_auth', - emeEnabled: true + emeEnabled: true, + drmSystem: 'WIDEVINE' } ), audioOnlyMultipleLevels: { diff --git a/tests/unit/controller/eme-controller.js b/tests/unit/controller/eme-controller.js index abbd3550b80..ee4a8c3ec06 100644 --- a/tests/unit/controller/eme-controller.js +++ b/tests/unit/controller/eme-controller.js @@ -35,21 +35,7 @@ describe('EMEController', function () { setupEach(); }); - it('should not do anything when `emeEnabled` is false (default)', function () { - let reqMediaKsAccessSpy = sinon.spy(); - - setupEach({ - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy - }); - - emeController.onMediaAttached({ media }); - emeController.onManifestParsed({ media }); - - expect(media.setMediaKeys.callCount).to.equal(0); - expect(reqMediaKsAccessSpy.callCount).to.equal(0); - }); - - it('should request keys when `emeEnabled` is true (but not set them)', function (done) { + it('should trigger key system error when bad encrypted data is received', function (done) { let reqMediaKsAccessSpy = sinon.spy(function () { return Promise.resolve({ // Media-keys mock @@ -57,50 +43,59 @@ describe('EMEController', function () { }); setupEach({ - emeEnabled: true, - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy + requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, + drmSystem: 'WIDEVINE' }); - emeController.onMediaAttached({ media }); - - expect(media.setMediaKeys.callCount).to.equal(0); - expect(reqMediaKsAccessSpy.callCount).to.equal(0); + let badData = { + initDataType: 'cenc', + initData: 'bad data' + }; + emeController.onMediaAttached({ media }); emeController.onManifestParsed({ levels: fakeLevels }); + media.emit('encrypted', badData); + setTimeout(function () { - expect(media.setMediaKeys.callCount).to.equal(0); - expect(reqMediaKsAccessSpy.callCount).to.equal(1); + expect(emeController.hls.trigger.args[0][1].details).to.equal(ErrorDetails.KEY_SYSTEM_NO_KEYS); + expect(emeController.hls.trigger.args[1][1].details).to.equal(ErrorDetails.KEY_SYSTEM_NO_ACCESS); done(); }, 0); }); - it('should trigger key system error when bad encrypted data is received', function (done) { - let reqMediaKsAccessSpy = sinon.spy(function () { + it('should retrieve PSSH data if it exists in manifest', function () { + let reqMediaKsAccessSpy = sinon.spy(() => { return Promise.resolve({ // Media-keys mock }); }); setupEach({ - emeEnabled: true, - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy + requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, + drmSystem: 'WIDEVINE' }); - let badData = { - initDataType: 'cenc', - initData: 'bad data' + const data = { + frag: { + foundKeys: true, + drmInfo: [{ + format: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + reluri: 'data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnNoYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=' + }] + } }; emeController.onMediaAttached({ media }); emeController.onManifestParsed({ levels: fakeLevels }); + emeController.onFragLoaded(data); - media.emit('encrypted', badData); + media.emit('encrypted', { + 'initDataType': emeController._initDataType, + 'initData': emeController._initData + }); - setTimeout(function () { - expect(emeController.hls.trigger.args[0][1].details).to.equal(ErrorDetails.KEY_SYSTEM_NO_KEYS); - expect(emeController.hls.trigger.args[1][1].details).to.equal(ErrorDetails.KEY_SYSTEM_NO_ACCESS); - done(); - }, 0); + expect(emeController._initDataType).to.equal('cenc'); + expect(62).to.equal(emeController._initData.byteLength); }); }); diff --git a/tests/unit/utils/eme-helper.js b/tests/unit/utils/eme-helper.js new file mode 100644 index 00000000000..63f4ceab4d4 --- /dev/null +++ b/tests/unit/utils/eme-helper.js @@ -0,0 +1,11 @@ +import { base64ToUint8Array } from '../../../src/utils/eme-helper'; +import assert from 'assert'; + +describe('base64 to arraybuffer util', function () { + let base64String = 'AAAA'; + it('converts base 64 encoded string to arraybuffer', function () { + let bytes = base64ToUint8Array(base64String); + assert(Object.prototype.toString.call(bytes), '[object Uint8Array]'); + assert(bytes.toString(), '0,0,0'); + }); +});