From 4fb5fdb61d1833b034ac39a6cd1be65d33804d42 Mon Sep 17 00:00:00 2001 From: Sri Date: Wed, 12 Sep 2018 21:35:13 -0700 Subject: [PATCH 01/23] add support for pssh in manifest --- src/controller/eme-controller.js | 58 +++++++++++++++++++++++++++----- src/loader/m3u8-parser.js | 7 ++-- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index d387ff9c487..0bf3d963095 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -73,6 +73,18 @@ const getSupportedMediaKeySystemConfigurations = function (keySystem, audioCodec } }; +function base64ToArrayBuffer (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; +} + /** * Controller to deal with encrypted media extensions (EME) * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API @@ -88,7 +100,9 @@ class EMEController extends EventHandler { constructor (hls) { super(hls, Event.MEDIA_ATTACHED, - Event.MANIFEST_PARSED + Event.MANIFEST_PARSED, + Event.LEVEL_LOADED, + Event.FRAG_LOADED ); this._widevineLicenseUrl = hls.config.widevineLicenseUrl; @@ -195,7 +209,6 @@ class EMEController extends EventHandler { mediaKeysListItem.mediaKeys = mediaKeys; logger.log(`Media-keys created for key-system "${keySystem}"`); - this._onMediaKeysCreated(); }) .catch((err) => { @@ -240,7 +253,6 @@ class EMEController extends EventHandler { _onMediaEncrypted (initDataType, initData) { logger.log(`Media is encrypted using "${initDataType}" init data type`); - this._isMediaEncrypted = true; this._mediaEncryptionInitDataType = initDataType; this._mediaEncryptionInitData = initData; @@ -356,7 +368,7 @@ class EMEController extends EventHandler { xhr.responseType = 'arraybuffer'; xhr.onreadystatechange = - this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback); + this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback); return xhr; } @@ -469,10 +481,11 @@ class EMEController extends EventHandler { this._media = media; // FIXME: also handle detaching media ! - - media.addEventListener('encrypted', (e) => { - this._onMediaEncrypted(e.initDataType, e.initData); - }); + if (!this._hasSetMediaKeys) { + media.addEventListener('encrypted', (e) => { + this._onMediaEncrypted(e.initDataType, e.initData); + }); + } } onManifestParsed (data) { @@ -485,6 +498,35 @@ class EMEController extends EventHandler { this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs); } + + onFragLoaded (data) { + if (!this._emeEnabled) { + return; + } + + // add initData and type if they are not included in playlist + if (this.initData && !this._hasSetMediaKeys) { + this._onMediaEncrypted(this.initDataType, this.initData); + } + } + + onLevelLoaded (data) { + if (!this._emeEnabled) { + return; + } + + if (data.details && data.details.levelkey) { + const levelkey = data.details.levelkey; + const details = levelkey.reluri.split(','); + const encoding = details[0]; + const pssh = details[1]; + + if (encoding.includes('base64')) { + this.initDataType = 'cenc'; + this.initData = base64ToArrayBuffer(pssh); + } + } + } } export default EMEController; diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index e184069755b..f80119e9559 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -166,7 +166,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++; @@ -217,7 +217,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,7 +252,7 @@ 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; @@ -302,6 +302,7 @@ export default class M3U8Parser { level.endSN = currentSN - 1; level.startCC = level.fragments[0] ? level.fragments[0].cc : 0; level.endCC = cc; + level.levelkey = levelkey; if (!level.initSegment && level.fragments.length) { // this is a bit lurky but HLS really has no other way to tell us From b1e50f87a52a4c2995eeb58d2e8ec6cb5f359dcd Mon Sep 17 00:00:00 2001 From: Sri Date: Thu, 13 Sep 2018 10:08:37 -0700 Subject: [PATCH 02/23] move util function into it's own file --- src/controller/eme-controller.js | 14 +------------- src/utils/base64toArrayBuffer.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 src/utils/base64toArrayBuffer.js diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 0bf3d963095..199589b2587 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -7,7 +7,7 @@ import EventHandler from '../event-handler'; import Event from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; - +import { base64ToArrayBuffer } from '../utils/base64toArrayBuffer'; import { logger } from '../utils/logger'; const { XMLHttpRequest } = window; @@ -73,18 +73,6 @@ const getSupportedMediaKeySystemConfigurations = function (keySystem, audioCodec } }; -function base64ToArrayBuffer (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; -} - /** * Controller to deal with encrypted media extensions (EME) * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API diff --git a/src/utils/base64toArrayBuffer.js b/src/utils/base64toArrayBuffer.js new file mode 100644 index 00000000000..6f711a2c4a6 --- /dev/null +++ b/src/utils/base64toArrayBuffer.js @@ -0,0 +1,11 @@ +export function base64ToArrayBuffer (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; +} From 3b423f5ae336beae7d400079f486592967684e2c Mon Sep 17 00:00:00 2001 From: Sri Date: Thu, 13 Sep 2018 10:23:11 -0700 Subject: [PATCH 03/23] add docs, clean up names --- src/controller/eme-controller.js | 16 +++++++++++----- src/utils/base64toArrayBuffer.js | 4 ++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 199589b2587..34689425a9c 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -106,6 +106,9 @@ class EMEController extends EventHandler { this._isMediaEncrypted = false; this._requestLicenseFailureCount = 0; + + this._initData = null; + this._initDataType = ''; } /** @@ -487,17 +490,20 @@ class EMEController extends EventHandler { this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs); } - onFragLoaded (data) { + onFragLoaded () { if (!this._emeEnabled) { return; } // add initData and type if they are not included in playlist - if (this.initData && !this._hasSetMediaKeys) { - this._onMediaEncrypted(this.initDataType, this.initData); + if (this._initData && !this._hasSetMediaKeys) { + this._onMediaEncrypted(this._initDataType, this._initData); } } + /** + * @param {object} data + */ onLevelLoaded (data) { if (!this._emeEnabled) { return; @@ -510,8 +516,8 @@ class EMEController extends EventHandler { const pssh = details[1]; if (encoding.includes('base64')) { - this.initDataType = 'cenc'; - this.initData = base64ToArrayBuffer(pssh); + this._initDataType = 'cenc'; + this._initData = base64ToArrayBuffer(pssh); } } } diff --git a/src/utils/base64toArrayBuffer.js b/src/utils/base64toArrayBuffer.js index 6f711a2c4a6..aa8f80df50e 100644 --- a/src/utils/base64toArrayBuffer.js +++ b/src/utils/base64toArrayBuffer.js @@ -1,3 +1,7 @@ +/** + * @param {string} base64String base64 encoded string + * @returns {ArrayBuffer} + */ export function base64ToArrayBuffer (base64String) { let binaryString = window.atob(base64String); let len = binaryString.length; From 5d2a4acdf5b0d37997e9df12b6a9313b0af7c797 Mon Sep 17 00:00:00 2001 From: Sri Date: Thu, 13 Sep 2018 13:58:23 -0700 Subject: [PATCH 04/23] add unit tests --- src/controller/eme-controller.js | 2 +- tests/unit/controller/eme-controller.js | 39 ++++++++++++++++++++++++- tests/unit/utils/base64toArrayBuffer.js | 11 +++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/unit/utils/base64toArrayBuffer.js diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 34689425a9c..157b7e5aad2 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -495,7 +495,7 @@ class EMEController extends EventHandler { return; } - // add initData and type if they are not included in playlist + // add initData and type if they are included in playlist if (this._initData && !this._hasSetMediaKeys) { this._onMediaEncrypted(this._initDataType, this._initData); } diff --git a/tests/unit/controller/eme-controller.js b/tests/unit/controller/eme-controller.js index 320b42534b9..177a5195990 100644 --- a/tests/unit/controller/eme-controller.js +++ b/tests/unit/controller/eme-controller.js @@ -4,7 +4,6 @@ import EventEmitter from 'events'; import { ErrorTypes, ErrorDetails } from '../../../src/errors'; import assert from 'assert'; - const sinon = require('sinon'); const MediaMock = function () { @@ -107,4 +106,42 @@ describe('EMEController', () => { done(); }, 0); }); + + it('should retrieve PSSH data if it exists in manifest', (done) => { + let reqMediaKsAccessSpy = sinon.spy(() => { + return Promise.resolve({ + // Media-keys mock + }); + }); + + setupEach({ + emeEnabled: true, + requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy + }); + + const data = { + details: { + levelkey: { + reluri: 'data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnNoYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=' + } + } + }; + + emeController.onMediaAttached({ media }); + emeController.onManifestParsed({ levels: fakeLevels }); + emeController.onLevelLoaded(data); + + media.emit('encrypted', { + 'initDataType': emeController._initDataType, + 'initData': emeController._initData + }); + + assert.equal(emeController._initDataType, 'cenc'); + assert.equal(62, emeController._initData.byteLength); + + setTimeout(() => { + assert.equal(emeController._isMediaEncrypted, true); + done(); + }, 0); + }); }); diff --git a/tests/unit/utils/base64toArrayBuffer.js b/tests/unit/utils/base64toArrayBuffer.js new file mode 100644 index 00000000000..c08d022bb88 --- /dev/null +++ b/tests/unit/utils/base64toArrayBuffer.js @@ -0,0 +1,11 @@ +import { base64ToArrayBuffer } from '../../../src/utils/base64toArrayBuffer'; +import assert from 'assert'; + +describe('base64 to arraybuffer util', function () { + let base64String = 'AAAA'; + it('converts base 64 encoded string to arraybuffer', function () { + let bytes = base64ToArrayBuffer(base64String); + assert(Object.prototype.toString.call(bytes), '[object Uint8Array]'); + assert(bytes.toString(), '0,0,0'); + }); +}); From fef140950920c41134b9e55ca07956e2401654c6 Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 14 Sep 2018 17:28:28 -0700 Subject: [PATCH 05/23] add support for multi-drm manifests --- src/config.js | 1 + src/controller/eme-controller.js | 35 ++++++++++++++++++++++---------- src/loader/m3u8-parser.js | 6 +++++- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/config.js b/src/config.js index 18ff6a27040..d016e3f08e8 100644 --- a/src/config.js +++ b/src/config.js @@ -89,6 +89,7 @@ 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 requestMediaKeySystemAccessFunc: requestMediaKeySystemAccess // used by eme-controller }; diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 157b7e5aad2..4a71b61c023 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -22,6 +22,11 @@ const KeySystems = { PLAYREADY: 'com.microsoft.playready' }; +const DRMIdentifiers = { + WIDEVINE: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + PLAYREADY: 'com.microsoft.playready' +}; + /** * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration * @param {Array} audioCodecs List of required audio codecs to support @@ -96,6 +101,7 @@ class EMEController extends EventHandler { this._widevineLicenseUrl = hls.config.widevineLicenseUrl; this._licenseXhrSetup = hls.config.licenseXhrSetup; this._emeEnabled = hls.config.emeEnabled; + this._selectedDrm = hls.config.drmSystem; this._requestMediaKeySystemAccess = hls.config.requestMediaKeySystemAccessFunc; @@ -216,6 +222,7 @@ class EMEController extends EventHandler { this._mediaKeysList.forEach((mediaKeysListItem) => { if (!mediaKeysListItem.mediaKeysSession) { mediaKeysListItem.mediaKeysSession = mediaKeysListItem.mediaKeys.createSession(); + this._mediaKeysCreated = true; this._onNewMediaKeySession(mediaKeysListItem.mediaKeysSession); } }); @@ -487,7 +494,7 @@ class EMEController extends EventHandler { const audioCodecs = data.levels.map((level) => level.audioCodec); const videoCodecs = data.levels.map((level) => level.videoCodec); - this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs); + this._attemptKeySystemAccess(KeySystems[this._selectedDrm], audioCodecs, videoCodecs); } onFragLoaded () { @@ -496,7 +503,7 @@ class EMEController extends EventHandler { } // add initData and type if they are included in playlist - if (this._initData && !this._hasSetMediaKeys) { + if (this._initData && !this._hasSetMediaKeys && this._mediaKeysCreated) { this._onMediaEncrypted(this._initDataType, this._initData); } } @@ -509,16 +516,22 @@ class EMEController extends EventHandler { return; } - if (data.details && data.details.levelkey) { - const levelkey = data.details.levelkey; - const details = levelkey.reluri.split(','); - const encoding = details[0]; - const pssh = details[1]; + if (data.details && data.details.drmInfo && data.details.drmInfo.length > 0) { + const drmInfo = data.details.drmInfo; - if (encoding.includes('base64')) { - this._initDataType = 'cenc'; - this._initData = base64ToArrayBuffer(pssh); - } + drmInfo.forEach((levelkey) => { + const details = levelkey.reluri.split(','); + const encoding = details[0]; + const pssh = details[1]; + + if (levelkey.format === DRMIdentifiers[this._selectedDrm]) { + if (encoding.includes('base64')) { + this._initData = base64ToArrayBuffer(pssh); + } + + this._initDataType = 'cenc'; + } + }); } } } diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index f80119e9559..34f82ca0448 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -154,6 +154,7 @@ export default class M3U8Parser { let frag = new Fragment(); let result; let i; + let drmInfo = []; let firstPdtIndex = null; @@ -258,9 +259,12 @@ export default class M3U8Parser { 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,7 +306,7 @@ export default class M3U8Parser { level.endSN = currentSN - 1; level.startCC = level.fragments[0] ? level.fragments[0].cc : 0; level.endCC = cc; - level.levelkey = levelkey; + level.drmInfo = drmInfo; if (!level.initSegment && level.fragments.length) { // this is a bit lurky but HLS really has no other way to tell us From 671356877b136fa1a0ca2a1bd8c75b1d9a3fe268 Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 14 Sep 2018 17:44:01 -0700 Subject: [PATCH 06/23] add docs --- docs/API.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/API.md b/docs/API.md index ca69dc57b7d..cf7f95380df 100644 --- a/docs/API.md +++ b/docs/API.md @@ -74,6 +74,10 @@ - [`abrBandWidthUpFactor`](#abrbandwidthupfactor) - [`abrMaxWithRealBitrate`](#abrmaxwithrealbitrate) - [`minAutoBitrate`](#minautobitrate) + - [`emeEnabled`](#emeenabled) + - [`licenseXhrSetup`](#licensexhrsetup) + - [`drmSystem`](#drmsystem) + - [`widevineLicenseUrl`](#widevinelicenseurl) - [Video Binding/Unbinding API](#video-bindingunbinding-api) - [`hls.attachMedia(videoElement)`](#hlsattachmediavideoelement) - [`hls.detachMedia()`](#hlsdetachmedia) @@ -964,6 +968,42 @@ 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. ## Video Binding/Unbinding API From 294d8af0d71416add1625aa6fe3a8313fbe765f2 Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 14 Sep 2018 18:10:25 -0700 Subject: [PATCH 07/23] add playready support --- docs/API.md | 8 ++- src/config.js | 1 + src/controller/eme-controller.js | 58 ++++++++++--------- src/utils/base64toArrayBuffer.js | 15 ----- src/utils/eme-helper.js | 50 ++++++++++++++++ tests/unit/controller/eme-controller.js | 14 +++-- .../{base64toArrayBuffer.js => eme-helper.js} | 2 +- 7 files changed, 100 insertions(+), 48 deletions(-) delete mode 100644 src/utils/base64toArrayBuffer.js create mode 100644 src/utils/eme-helper.js rename tests/unit/utils/{base64toArrayBuffer.js => eme-helper.js} (81%) diff --git a/docs/API.md b/docs/API.md index cf7f95380df..94cad62d5b3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -78,6 +78,7 @@ - [`licenseXhrSetup`](#licensexhrsetup) - [`drmSystem`](#drmsystem) - [`widevineLicenseUrl`](#widevinelicenseurl) + - [`playreadyLicenseUrl`](#playreadylicenseurl) - [Video Binding/Unbinding API](#video-bindingunbinding-api) - [`hls.attachMedia(videoElement)`](#hlsattachmediavideoelement) - [`hls.detachMedia()`](#hlsdetachmedia) @@ -998,13 +999,18 @@ This allows user to easily modify/setup XHR. See example below. 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 ### `hls.attachMedia(videoElement)` diff --git a/src/config.js b/src/config.js index d016e3f08e8..d43fb9d680a 100644 --- a/src/config.js +++ b/src/config.js @@ -90,6 +90,7 @@ export var hlsDefaultConfig = { 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.js b/src/controller/eme-controller.js index 4a71b61c023..f62b18dcd30 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -7,10 +7,10 @@ import EventHandler from '../event-handler'; import Event from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; -import { base64ToArrayBuffer } from '../utils/base64toArrayBuffer'; +import { base64ToArrayBuffer, buildPlayReadyPSSHBox } from '../utils/eme-helper'; import { logger } from '../utils/logger'; -const { XMLHttpRequest } = window; +const { XMLHttpRequest, atob, DOMParser } = window; const MAX_LICENSE_REQUEST_FAILURES = 3; @@ -128,6 +128,8 @@ class EMEController extends EventHandler { case KeySystems.WIDEVINE: url = this._widevineLicenseUrl; break; + case KeySystems.PLAYREADY: + url = this._playreadyLicenseUrl; default: url = null; break; @@ -222,7 +224,7 @@ class EMEController extends EventHandler { this._mediaKeysList.forEach((mediaKeysListItem) => { if (!mediaKeysListItem.mediaKeysSession) { mediaKeysListItem.mediaKeysSession = mediaKeysListItem.mediaKeys.createSession(); - this._mediaKeysCreated = true; + this._haveKeySession = true; this._onNewMediaKeySession(mediaKeysListItem.mediaKeysSession); } }); @@ -410,32 +412,32 @@ class EMEController extends EventHandler { * @param {ArrayBuffer} keyMessage * @returns {ArrayBuffer} Challenge data posted to license server */ - _generateLicenseRequestChallenge (keysListItem, keyMessage) { + _generateLicenseRequestChallenge (keysListItem, keyMessage, xhr) { let challenge; if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) { logger.error('PlayReady is not supported (yet)'); // 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); - } - } - */ + // 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'); + + if (keyMessageXml.getElementsByTagName('Challenge')[0]) { + challenge = atob(keyMessageXml.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue); + } else { + throw Error('Cannot find in key message'); + } + + const headerNames = keyMessageXml.getElementsByTagName('name'); + const headerValues = keyMessageXml.getElementsByTagName('value'); + + if (headerNames.length !== headerValues.length) { + throw Error('Mismatched header / pair in key message'); + } + + for (let i = 0; i < headerNames.length; i++) { + xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue); + } } else if (keysListItem.mediaKeySystemDomain === KeySystems.WIDEVINE) { // For Widevine CDMs, the challenge is the keyMessage. challenge = keyMessage; @@ -465,7 +467,7 @@ class EMEController extends EventHandler { logger.log(`Sending license request to URL: ${url}`); - xhr.send(this._generateLicenseRequestChallenge(keysListItem, keyMessage)); + xhr.send(this._generateLicenseRequestChallenge(keysListItem, keyMessage, xhr)); } onMediaAttached (data) { @@ -503,7 +505,7 @@ class EMEController extends EventHandler { } // add initData and type if they are included in playlist - if (this._initData && !this._hasSetMediaKeys && this._mediaKeysCreated) { + if (this._initData && !this._hasSetMediaKeys && this._haveKeySession) { this._onMediaEncrypted(this._initDataType, this._initData); } } @@ -526,7 +528,11 @@ class EMEController extends EventHandler { if (levelkey.format === DRMIdentifiers[this._selectedDrm]) { if (encoding.includes('base64')) { - this._initData = base64ToArrayBuffer(pssh); + if (DRMIdentifiers[this._selectedDrm] === 'com.microsoft.playready') { + this._initData = buildPlayReadyPSSHBox(base64ToArrayBuffer(pssh)); + } else { + this._initData = base64ToArrayBuffer(pssh); + } } this._initDataType = 'cenc'; diff --git a/src/utils/base64toArrayBuffer.js b/src/utils/base64toArrayBuffer.js deleted file mode 100644 index aa8f80df50e..00000000000 --- a/src/utils/base64toArrayBuffer.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @param {string} base64String base64 encoded string - * @returns {ArrayBuffer} - */ -export function base64ToArrayBuffer (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; -} diff --git a/src/utils/eme-helper.js b/src/utils/eme-helper.js new file mode 100644 index 00000000000..8d397dfc2be --- /dev/null +++ b/src/utils/eme-helper.js @@ -0,0 +1,50 @@ +/** + * @param {string} base64String base64 encoded string + * @returns {ArrayBuffer} + */ +export function base64ToArrayBuffer (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} binary data from the URI in the manifest + * @returns {ArrayBuffer} + */ +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; +} diff --git a/tests/unit/controller/eme-controller.js b/tests/unit/controller/eme-controller.js index 177a5195990..39bf08cbc00 100644 --- a/tests/unit/controller/eme-controller.js +++ b/tests/unit/controller/eme-controller.js @@ -61,7 +61,8 @@ describe('EMEController', () => { setupEach({ emeEnabled: true, - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy + requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, + drmSystem: 'WIDEVINE' }); emeController.onMediaAttached({ media }); @@ -87,7 +88,8 @@ describe('EMEController', () => { setupEach({ emeEnabled: true, - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy + requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, + drmSystem: 'WIDEVINE' }); let badData = { @@ -116,14 +118,16 @@ describe('EMEController', () => { setupEach({ emeEnabled: true, - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy + requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, + drmSystem: 'WIDEVINE' }); const data = { details: { - levelkey: { + drmInfo: [{ + format: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', reluri: 'data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnNoYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=' - } + }] } }; diff --git a/tests/unit/utils/base64toArrayBuffer.js b/tests/unit/utils/eme-helper.js similarity index 81% rename from tests/unit/utils/base64toArrayBuffer.js rename to tests/unit/utils/eme-helper.js index c08d022bb88..99c253bd7b0 100644 --- a/tests/unit/utils/base64toArrayBuffer.js +++ b/tests/unit/utils/eme-helper.js @@ -1,4 +1,4 @@ -import { base64ToArrayBuffer } from '../../../src/utils/base64toArrayBuffer'; +import { base64ToArrayBuffer } from '../../../src/utils/eme-helper'; import assert from 'assert'; describe('base64 to arraybuffer util', function () { From 82368098ebaf36f1b8cd7e768177165214c290d7 Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 14 Sep 2018 18:35:53 -0700 Subject: [PATCH 08/23] clean up --- src/controller/eme-controller.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index f62b18dcd30..7da14386def 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -72,6 +72,7 @@ const createWidevineMediaKeySystemConfigurations = function (audioCodecs, videoC const getSupportedMediaKeySystemConfigurations = function (keySystem, audioCodecs, videoCodecs) { switch (keySystem) { case KeySystems.WIDEVINE: + case KeySystems.PLAYREADY: return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs); default: throw Error('Unknown key-system: ' + keySystem); @@ -99,6 +100,7 @@ class EMEController extends EventHandler { ); 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; @@ -130,6 +132,7 @@ class EMEController extends EventHandler { break; case KeySystems.PLAYREADY: url = this._playreadyLicenseUrl; + break; default: url = null; break; @@ -416,8 +419,6 @@ class EMEController extends EventHandler { let challenge; if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) { - logger.error('PlayReady is not supported (yet)'); - // 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'); @@ -529,9 +530,11 @@ class EMEController extends EventHandler { if (levelkey.format === DRMIdentifiers[this._selectedDrm]) { if (encoding.includes('base64')) { if (DRMIdentifiers[this._selectedDrm] === 'com.microsoft.playready') { - this._initData = buildPlayReadyPSSHBox(base64ToArrayBuffer(pssh)); + this._initData = buildPlayReadyPSSHBox(base64ToArrayBuffer(pssh)); // Playready is particular about the pssh box, so it needs to be handcrafted. + } else if (DRMIdentifiers[this._selectedDrm] === 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') { + this._initData = base64ToArrayBuffer(pssh); // Widevine pssh box } else { - this._initData = base64ToArrayBuffer(pssh); + logger.log('not supported'); } } From d77f6d008e2fba9c88a2f6c55cba4704e0f37eb1 Mon Sep 17 00:00:00 2001 From: Sri Date: Mon, 17 Sep 2018 13:31:16 -0700 Subject: [PATCH 09/23] move playready header logic to separate function --- src/controller/eme-controller.js | 38 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 7da14386def..fa316590fb9 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -415,7 +415,7 @@ class EMEController extends EventHandler { * @param {ArrayBuffer} keyMessage * @returns {ArrayBuffer} Challenge data posted to license server */ - _generateLicenseRequestChallenge (keysListItem, keyMessage, xhr) { + _generateLicenseRequestChallenge (keysListItem, keyMessage) { let challenge; if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) { @@ -428,17 +428,6 @@ class EMEController extends EventHandler { } else { throw Error('Cannot find in key message'); } - - const headerNames = keyMessageXml.getElementsByTagName('name'); - const headerValues = keyMessageXml.getElementsByTagName('value'); - - if (headerNames.length !== headerValues.length) { - throw Error('Mismatched header / pair in key message'); - } - - for (let i = 0; i < headerNames.length; i++) { - xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue); - } } else if (keysListItem.mediaKeySystemDomain === KeySystems.WIDEVINE) { // For Widevine CDMs, the challenge is the keyMessage. challenge = keyMessage; @@ -449,6 +438,24 @@ class EMEController extends EventHandler { return challenge; } + /** + * @param {ArrayBuffer} keyMessage + * @param {XMLHttpRequest} xhr + */ + _setPlayreadyHeaders (keyMessage, xhr) { + const keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml'); + const headerNames = keyMessageXml.getElementsByTagName('name'); + const headerValues = keyMessageXml.getElementsByTagName('value'); + + if (headerNames.length !== headerValues.length) { + throw Error('Mismatched header / pair in key message'); + } + + for (let i = 0; i < headerNames.length; i++) { + xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue); + } + } + _requestLicense (keyMessage, callback) { logger.log('Requesting content license for key-system'); @@ -465,10 +472,15 @@ class EMEController extends EventHandler { const url = this.getLicenseServerUrl(keysListItem.mediaKeySystemDomain); const xhr = this._createLicenseXhr(url, keyMessage, callback); + const challenge = this._generateLicenseRequestChallenge(keysListItem, keyMessage); + + if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) { + this._setPlayreadyHeaders(keyMessage, xhr); + } logger.log(`Sending license request to URL: ${url}`); - xhr.send(this._generateLicenseRequestChallenge(keysListItem, keyMessage, xhr)); + xhr.send(challenge); } onMediaAttached (data) { From d04049e809be8053de8d7f3e2f9c309b075f0b7a Mon Sep 17 00:00:00 2001 From: Sri Date: Mon, 17 Sep 2018 15:06:34 -0700 Subject: [PATCH 10/23] update playready challenge logic --- src/controller/eme-controller.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index fa316590fb9..e66c9144121 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -422,9 +422,10 @@ class EMEController extends EventHandler { // 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 (keyMessageXml.getElementsByTagName('Challenge')[0]) { - challenge = atob(keyMessageXml.getElementsByTagName('Challenge')[0].childNodes[0].nodeValue); + if (challengeElement) { + challenge = atob(challengeElement.textContent); } else { throw Error('Cannot find in key message'); } @@ -444,16 +445,11 @@ class EMEController extends EventHandler { */ _setPlayreadyHeaders (keyMessage, xhr) { const keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml'); - const headerNames = keyMessageXml.getElementsByTagName('name'); - const headerValues = keyMessageXml.getElementsByTagName('value'); + const headers = keyMessageXml.getElementsByTagName('HttpHeader'); - if (headerNames.length !== headerValues.length) { - throw Error('Mismatched header / pair in key message'); - } - - for (let i = 0; i < headerNames.length; i++) { - xhr.setRequestHeader(headerNames[i].childNodes[0].nodeValue, headerValues[i].childNodes[0].nodeValue); - } + headers.forEach((header) => { + xhr.setRequestHeader(header.querySelector('name').textContent, header.querySelector('value').textContent); + }); } _requestLicense (keyMessage, callback) { From 685e6df445a5f51e3cc22af78679d5d9bf0ee498 Mon Sep 17 00:00:00 2001 From: Sri Date: Mon, 17 Sep 2018 15:48:54 -0700 Subject: [PATCH 11/23] fix: use for loop instead of forEach --- src/controller/eme-controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index e66c9144121..ce95e8e6d7d 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -446,10 +446,12 @@ class EMEController extends EventHandler { _setPlayreadyHeaders (keyMessage, xhr) { const keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml'); const headers = keyMessageXml.getElementsByTagName('HttpHeader'); + let header; - headers.forEach((header) => { + for (let i = 0, len = headers.length; i < len; i++) { + header = headers[i]; xhr.setRequestHeader(header.querySelector('name').textContent, header.querySelector('value').textContent); - }); + } } _requestLicense (keyMessage, callback) { From 7c670cc9f4bca2dd6b36bdd71b24666c16aafbb5 Mon Sep 17 00:00:00 2001 From: Sri Date: Wed, 19 Sep 2018 16:45:09 -0700 Subject: [PATCH 12/23] move playready header logic to separate file --- src/controller/eme-controller.js | 25 ++++++++----------------- src/utils/eme-helper.js | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index ce95e8e6d7d..a512bce55f8 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -7,7 +7,7 @@ import EventHandler from '../event-handler'; import Event from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; -import { base64ToArrayBuffer, buildPlayReadyPSSHBox } from '../utils/eme-helper'; +import { base64ToArrayBuffer, buildPlayReadyPSSHBox, makePlayreadyHeaders } from '../utils/eme-helper'; import { logger } from '../utils/logger'; const { XMLHttpRequest, atob, DOMParser } = window; @@ -439,21 +439,6 @@ class EMEController extends EventHandler { return challenge; } - /** - * @param {ArrayBuffer} keyMessage - * @param {XMLHttpRequest} xhr - */ - _setPlayreadyHeaders (keyMessage, xhr) { - const keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml'); - const headers = keyMessageXml.getElementsByTagName('HttpHeader'); - let header; - - for (let i = 0, len = headers.length; i < len; i++) { - header = headers[i]; - xhr.setRequestHeader(header.querySelector('name').textContent, header.querySelector('value').textContent); - } - } - _requestLicense (keyMessage, callback) { logger.log('Requesting content license for key-system'); @@ -473,7 +458,13 @@ class EMEController extends EventHandler { const challenge = this._generateLicenseRequestChallenge(keysListItem, keyMessage); if (keysListItem.mediaKeySystemDomain === KeySystems.PLAYREADY) { - this._setPlayreadyHeaders(keyMessage, xhr); + const playReadyHeaders = makePlayreadyHeaders(keyMessage); + + if (playReadyHeaders.length > 0) { + playReadyHeaders.forEach((header) => { + xhr.setRequestHeader(header.name, header.value); + }); + } } logger.log(`Sending license request to URL: ${url}`); diff --git a/src/utils/eme-helper.js b/src/utils/eme-helper.js index 8d397dfc2be..0e10e8627e6 100644 --- a/src/utils/eme-helper.js +++ b/src/utils/eme-helper.js @@ -48,3 +48,27 @@ export function buildPlayReadyPSSHBox (binary) { return pssh; } + +/* +* @param {ArrayBuffer} keyMessage +* @returns {Array} 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.getElementsByTagName('HttpHeader'); + const playReadyHeaders = []; + + let header; + + for (let i = 0, len = headers.length; i < len; i++) { + header = headers[i]; + playReadyHeaders.pushd({ + 'name': header.querySelector('name').textContent, + 'value': header.querySelector('value').textContent + }); + } + + return playReadyHeaders; +} From 04015189b427045e845647a525c58f27f4b5bda7 Mon Sep 17 00:00:00 2001 From: Sri Date: Wed, 19 Sep 2018 16:48:49 -0700 Subject: [PATCH 13/23] fix typo --- src/utils/eme-helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/eme-helper.js b/src/utils/eme-helper.js index 0e10e8627e6..feb2459fab1 100644 --- a/src/utils/eme-helper.js +++ b/src/utils/eme-helper.js @@ -57,14 +57,14 @@ 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.getElementsByTagName('HttpHeader'); + const headers = keyMessageXml.querySelectorAll('HttpHeader'); const playReadyHeaders = []; let header; for (let i = 0, len = headers.length; i < len; i++) { header = headers[i]; - playReadyHeaders.pushd({ + playReadyHeaders.push({ 'name': header.querySelector('name').textContent, 'value': header.querySelector('value').textContent }); From 00025d14b9e4a599579e93d5daa7e3d4a1942134 Mon Sep 17 00:00:00 2001 From: Sri Date: Thu, 20 Sep 2018 14:45:18 -0700 Subject: [PATCH 14/23] don't initialize emeController if eme isn't enabled --- src/controller/eme-controller.js | 16 ---------------- src/hls.js | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index a512bce55f8..007f04b9dfc 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -473,10 +473,6 @@ class EMEController extends EventHandler { } onMediaAttached (data) { - if (!this._emeEnabled) { - return; - } - const media = data.media; // keep reference of media @@ -491,10 +487,6 @@ class EMEController extends EventHandler { } onManifestParsed (data) { - if (!this._emeEnabled) { - return; - } - const audioCodecs = data.levels.map((level) => level.audioCodec); const videoCodecs = data.levels.map((level) => level.videoCodec); @@ -502,10 +494,6 @@ class EMEController extends EventHandler { } onFragLoaded () { - if (!this._emeEnabled) { - return; - } - // add initData and type if they are included in playlist if (this._initData && !this._hasSetMediaKeys && this._haveKeySession) { this._onMediaEncrypted(this._initDataType, this._initData); @@ -516,10 +504,6 @@ class EMEController extends EventHandler { * @param {object} data */ onLevelLoaded (data) { - if (!this._emeEnabled) { - return; - } - if (data.details && data.details.drmInfo && data.details.drmInfo.length > 0) { const drmInfo = data.details.drmInfo; diff --git a/src/hls.js b/src/hls.js index 9555c277e24..72fbf8abdbf 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); /** From 09d9660adf848c8e9a13ac58fef3c78e873d678b Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 26 Oct 2018 16:30:25 -0700 Subject: [PATCH 15/23] simplify playready headers --- src/controller/eme-controller.js | 2 +- src/utils/eme-helper.js | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 007f04b9dfc..2de93eaa2ba 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -462,7 +462,7 @@ class EMEController extends EventHandler { if (playReadyHeaders.length > 0) { playReadyHeaders.forEach((header) => { - xhr.setRequestHeader(header.name, header.value); + xhr.setRequestHeader(header[0], header[1]); }); } } diff --git a/src/utils/eme-helper.js b/src/utils/eme-helper.js index feb2459fab1..5dfe96453d4 100644 --- a/src/utils/eme-helper.js +++ b/src/utils/eme-helper.js @@ -51,7 +51,7 @@ export function buildPlayReadyPSSHBox (binary) { /* * @param {ArrayBuffer} keyMessage -* @returns {Array} playReadyHeaders +* @returns {Array<[string, string]>} playReadyHeaders */ export function makePlayreadyHeaders (keyMessage) { const xmlContent = String.fromCharCode.apply(null, new Uint16Array(keyMessage)); @@ -64,10 +64,7 @@ export function makePlayreadyHeaders (keyMessage) { for (let i = 0, len = headers.length; i < len; i++) { header = headers[i]; - playReadyHeaders.push({ - 'name': header.querySelector('name').textContent, - 'value': header.querySelector('value').textContent - }); + playReadyHeaders.push([header.querySelector('name').textContent, header.querySelector('value').textContent]); } return playReadyHeaders; From a9d9e8f06f1533fe909b0cf5ede37efa0c777e9b Mon Sep 17 00:00:00 2001 From: Sri Date: Thu, 8 Nov 2018 16:55:38 -0800 Subject: [PATCH 16/23] clarify and update function name to more accurately reflect returned value --- src/controller/eme-controller.js | 6 +++--- src/utils/eme-helper.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 2de93eaa2ba..9df10454ff7 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -7,7 +7,7 @@ import EventHandler from '../event-handler'; import Event from '../events'; import { ErrorTypes, ErrorDetails } from '../errors'; -import { base64ToArrayBuffer, buildPlayReadyPSSHBox, makePlayreadyHeaders } from '../utils/eme-helper'; +import { base64ToUint8Array, buildPlayReadyPSSHBox, makePlayreadyHeaders } from '../utils/eme-helper'; import { logger } from '../utils/logger'; const { XMLHttpRequest, atob, DOMParser } = window; @@ -515,9 +515,9 @@ class EMEController extends EventHandler { if (levelkey.format === DRMIdentifiers[this._selectedDrm]) { if (encoding.includes('base64')) { if (DRMIdentifiers[this._selectedDrm] === 'com.microsoft.playready') { - this._initData = buildPlayReadyPSSHBox(base64ToArrayBuffer(pssh)); // Playready is particular about the pssh box, so it needs to be handcrafted. + this._initData = buildPlayReadyPSSHBox(base64ToUint8Array(pssh)); // Playready is particular about the pssh box, so it needs to be handcrafted. } else if (DRMIdentifiers[this._selectedDrm] === 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') { - this._initData = base64ToArrayBuffer(pssh); // Widevine pssh box + this._initData = base64ToUint8Array(pssh); // Widevine pssh box } else { logger.log('not supported'); } diff --git a/src/utils/eme-helper.js b/src/utils/eme-helper.js index 5dfe96453d4..dbabbb1b21b 100644 --- a/src/utils/eme-helper.js +++ b/src/utils/eme-helper.js @@ -1,8 +1,8 @@ /** * @param {string} base64String base64 encoded string - * @returns {ArrayBuffer} + * @returns {Uint8Array} */ -export function base64ToArrayBuffer (base64String) { +export function base64ToUint8Array (base64String) { let binaryString = window.atob(base64String); let len = binaryString.length; let bytes = new Uint8Array(len); @@ -17,7 +17,7 @@ export function base64ToArrayBuffer (base64String) { /** * https://www.w3.org/TR/eme-initdata-cenc/ * @param {ArrayBuffer} binary data from the URI in the manifest - * @returns {ArrayBuffer} + * @returns {Uint8Array} */ export function buildPlayReadyPSSHBox (binary) { // https://dashif.org/identifiers/protection/ playready uuid: 9a04f079-9840-4286-ab92-e65be0885f95 From 56a357f23f2021bd502b6f03a3109a443bda6156 Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 9 Nov 2018 16:07:38 -0800 Subject: [PATCH 17/23] optimize and improve readability of capturing initData --- src/controller/eme-controller.js | 37 ++++++++++++++------------------ src/loader/m3u8-parser.js | 3 ++- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index 9df10454ff7..b6f97e7a944 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -505,27 +505,22 @@ class EMEController extends EventHandler { */ onLevelLoaded (data) { if (data.details && data.details.drmInfo && data.details.drmInfo.length > 0) { - const drmInfo = data.details.drmInfo; - - drmInfo.forEach((levelkey) => { - const details = levelkey.reluri.split(','); - const encoding = details[0]; - const pssh = details[1]; - - if (levelkey.format === DRMIdentifiers[this._selectedDrm]) { - if (encoding.includes('base64')) { - if (DRMIdentifiers[this._selectedDrm] === 'com.microsoft.playready') { - this._initData = buildPlayReadyPSSHBox(base64ToUint8Array(pssh)); // Playready is particular about the pssh box, so it needs to be handcrafted. - } else if (DRMIdentifiers[this._selectedDrm] === 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') { - this._initData = base64ToUint8Array(pssh); // Widevine pssh box - } else { - logger.log('not supported'); - } - } - - this._initDataType = 'cenc'; - } - }); + const drmIdentifier = DRMIdentifiers[this._selectedDrm]; + + const selectedDrm = data.details.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 = 'cenc'; } } } diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index b2471b786b8..123b6f8c1b5 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -154,7 +154,8 @@ export default class M3U8Parser { let frag = new Fragment(); let result; let i; - let drmInfo = []; + + const drmInfo = []; let firstPdtIndex = null; From ca9bcc3749c76233fc83d31c9b2e1aa3106bb68d Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 9 Nov 2018 17:13:12 -0800 Subject: [PATCH 18/23] fix unit test --- tests/unit/utils/eme-helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/eme-helper.js b/tests/unit/utils/eme-helper.js index 99c253bd7b0..63f4ceab4d4 100644 --- a/tests/unit/utils/eme-helper.js +++ b/tests/unit/utils/eme-helper.js @@ -1,10 +1,10 @@ -import { base64ToArrayBuffer } from '../../../src/utils/eme-helper'; +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 = base64ToArrayBuffer(base64String); + let bytes = base64ToUint8Array(base64String); assert(Object.prototype.toString.call(bytes), '[object Uint8Array]'); assert(bytes.toString(), '0,0,0'); }); From 49e22ca3c3c4b1b2d4d3d4a5312cad78f9b36d67 Mon Sep 17 00:00:00 2001 From: Sri Date: Fri, 9 Nov 2018 17:14:37 -0800 Subject: [PATCH 19/23] remove unnecessary code --- src/controller/eme-controller.js | 1 - tests/unit/controller/eme-controller.js | 43 ------------------------- 2 files changed, 44 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index b6f97e7a944..c01007cf7aa 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -102,7 +102,6 @@ class EMEController extends EventHandler { 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; diff --git a/tests/unit/controller/eme-controller.js b/tests/unit/controller/eme-controller.js index 39bf08cbc00..def7d9a003c 100644 --- a/tests/unit/controller/eme-controller.js +++ b/tests/unit/controller/eme-controller.js @@ -38,47 +38,6 @@ describe('EMEController', () => { it('should be constructable with an unconfigured Hls.js instance', () => {}); - it('should not do anything when `emeEnabled` is false (default)', () => { - let reqMediaKsAccessSpy = sinon.spy(); - - setupEach({ - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy - }); - - emeController.onMediaAttached({ media }); - emeController.onManifestParsed({ media }); - - media.setMediaKeys.callCount.should.be.equal(0); - reqMediaKsAccessSpy.callCount.should.be.equal(0); - }); - - it('should request keys when `emeEnabled` is true (but not set them)', (done) => { - let reqMediaKsAccessSpy = sinon.spy(() => { - return Promise.resolve({ - // Media-keys mock - }); - }); - - setupEach({ - emeEnabled: true, - requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, - drmSystem: 'WIDEVINE' - }); - - emeController.onMediaAttached({ media }); - - media.setMediaKeys.callCount.should.be.equal(0); - reqMediaKsAccessSpy.callCount.should.be.equal(0); - - emeController.onManifestParsed({ levels: fakeLevels }); - - setTimeout(() => { - media.setMediaKeys.callCount.should.be.equal(0); - reqMediaKsAccessSpy.callCount.should.be.equal(1); - done(); - }, 0); - }); - it('should trigger key system error when bad encrypted data is received', (done) => { let reqMediaKsAccessSpy = sinon.spy(() => { return Promise.resolve({ @@ -87,7 +46,6 @@ describe('EMEController', () => { }); setupEach({ - emeEnabled: true, requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, drmSystem: 'WIDEVINE' }); @@ -117,7 +75,6 @@ describe('EMEController', () => { }); setupEach({ - emeEnabled: true, requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, drmSystem: 'WIDEVINE' }); From 897257d0eef693e1ca0216f506ee08eea6c85571 Mon Sep 17 00:00:00 2001 From: Sri Date: Tue, 13 Nov 2018 13:12:45 -0800 Subject: [PATCH 20/23] add documentation, add initDataTypes enum --- src/controller/eme-controller.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index c01007cf7aa..ef0495a29b1 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -27,6 +27,15 @@ const DRMIdentifiers = { PLAYREADY: 'com.microsoft.playready' }; +/* +* https://www.w3.org/TR/eme-initdata-registry/ +*/ +const 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 @@ -114,8 +123,21 @@ class EMEController extends EventHandler { this._requestLicenseFailureCount = 0; + /** + * @private + * https://www.w3.org/TR/encrypted-media/#initialization-data + * Data used by CDN to generate license request + * @member {Uint8Array} _initData + */ this._initData = null; - this._initDataType = ''; + + /** + * @private + * https://www.w3.org/TR/encrypted-media/#initialization-data-type + * a string that indicates the format of the accompanying Initialization Data + * @member {string} _initDataType + */ + this._initDataType = null; } /** @@ -519,7 +541,7 @@ class EMEController extends EventHandler { this._initData = base64ToUint8Array(pssh); // Widevine pssh box } - this._initDataType = 'cenc'; + this._initDataType = InitDataTypes.COMMON_ENCRYPTION; } } } From 177b1e761c49518a16f0748429ec4b1a7be6ee28 Mon Sep 17 00:00:00 2001 From: Sri Date: Tue, 13 Nov 2018 14:13:27 -0800 Subject: [PATCH 21/23] fix functional test --- tests/test-streams.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-streams.js b/tests/test-streams.js index 46cb90c7987..749b79f5d00 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: { From 61494eb07c670eb36b6430a4d0cb6fe3f683b6ff Mon Sep 17 00:00:00 2001 From: Sri Sreedharan Date: Wed, 23 Jan 2019 15:39:58 -0800 Subject: [PATCH 22/23] add key rotation support --- src/controller/eme-controller.js | 138 ++++++++++++++++-------- src/loader/m3u8-parser.js | 8 +- tests/unit/controller/eme-controller.js | 5 +- 3 files changed, 101 insertions(+), 50 deletions(-) diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index ef0495a29b1..cf4eb2587c4 100644 --- a/src/controller/eme-controller.js +++ b/src/controller/eme-controller.js @@ -104,7 +104,6 @@ class EMEController extends EventHandler { super(hls, Event.MEDIA_ATTACHED, Event.MANIFEST_PARSED, - Event.LEVEL_LOADED, Event.FRAG_LOADED ); @@ -126,7 +125,7 @@ class EMEController extends EventHandler { /** * @private * https://www.w3.org/TR/encrypted-media/#initialization-data - * Data used by CDN to generate license request + * Data used by CDM to generate license request * @member {Uint8Array} _initData */ this._initData = null; @@ -138,6 +137,28 @@ class EMEController extends EventHandler { * @member {string} _initDataType */ this._initDataType = null; + + /** + * @private + * https://www.w3.org/TR/encrypted-media/#dom-mediakeys + * the MediaKeySystemAccess object provides access to a Key System + * @member {MediaKeys} _initDataType + */ + this._mediaKeys = null; + + /** + * @private + * List of required audio codecs to support + * @member {Array} audioCodecs + */ + this._audioCodecs = null; + + /** + * @private + * List of required video codecs to support + * @member {Array} videoCodecs + */ + this._videoCodecs = null; } /** @@ -225,18 +246,32 @@ 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._onMediaKeysCreated(); - }) - .catch((err) => { - logger.error('Failed to create media-keys:', err); - }); + this._mediaKeysList.push({ + mediaKeys: this._mediaKeys, + mediaKeysSession: null, + mediaKeysSessionInitialized: false, + mediaKeySystemAccess: mediaKeySystemAccess, + mediaKeySystemDomain: keySystem + }); + + this._onMediaKeysCreated(); } /** @@ -262,12 +297,13 @@ class EMEController extends EventHandler { logger.log(`New key-system session ${keySession.sessionId}`); keySession.addEventListener('message', (event) => { - this._onKeySessionMessage(keySession, event.message); + this._onKeySessionMessage(keySession, event); }, false); } - _onKeySessionMessage (keySession, message) { + _onKeySessionMessage (keySession, session) { logger.log('Got EME message event, creating license request'); + const message = session.message; this._requestLicense(message, (data) => { logger.log('Received license data, updating key-session'); @@ -288,7 +324,8 @@ class EMEController extends EventHandler { _attemptSetMediaKeys () { 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, { @@ -300,15 +337,20 @@ class EMEController extends EventHandler { } logger.log('Setting keys for encrypted media'); - this._media.setMediaKeys(keysListItem.mediaKeys); this._hasSetMediaKeys = true; } } + _getMediaKeys () { + return this._mediaKeysList[this._mediaKeysList.length - 1]; + } + _generateRequestWithPreferredKeySession () { // 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, { @@ -463,7 +505,8 @@ class EMEController extends EventHandler { _requestLicense (keyMessage, callback) { 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, { @@ -502,47 +545,52 @@ class EMEController extends EventHandler { // FIXME: also handle detaching media ! if (!this._hasSetMediaKeys) { media.addEventListener('encrypted', (e) => { - this._onMediaEncrypted(e.initDataType, e.initData); + if (e.initData) { + this._onMediaEncrypted(e.initDataType, e.initData); + } }); } } onManifestParsed (data) { - const audioCodecs = data.levels.map((level) => level.audioCodec); - const videoCodecs = data.levels.map((level) => level.videoCodec); + this._audioCodecs = data.levels.map((level) => level.audioCodec); + this._videoCodecs = data.levels.map((level) => level.videoCodec); - this._attemptKeySystemAccess(KeySystems[this._selectedDrm], audioCodecs, videoCodecs); + this._attemptKeySystemAccess(KeySystems[this._selectedDrm], this._audioCodecs, this._videoCodecs); } - onFragLoaded () { - // add initData and type if they are included in playlist - if (this._initData && !this._hasSetMediaKeys && this._haveKeySession) { + onFragLoaded (data) { + const frag = data.frag; + + // 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(this._initDataType, this._initData); } } - /** - * @param {object} data - */ - onLevelLoaded (data) { - if (data.details && data.details.drmInfo && data.details.drmInfo.length > 0) { - const drmIdentifier = DRMIdentifiers[this._selectedDrm]; - - const selectedDrm = data.details.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 - } + _processInitData (drmInfo) { + const drmIdentifier = DRMIdentifiers[this._selectedDrm]; + + const selectedDrm = drmInfo.filter(levelkey => levelkey.format === drmIdentifier); + const levelkey = selectedDrm.shift(); - this._initDataType = InitDataTypes.COMMON_ENCRYPTION; + 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; } } diff --git a/src/loader/m3u8-parser.js b/src/loader/m3u8-parser.js index 123b6f8c1b5..08139084eec 100644 --- a/src/loader/m3u8-parser.js +++ b/src/loader/m3u8-parser.js @@ -155,7 +155,7 @@ export default class M3U8Parser { let result; let i; - const drmInfo = []; + let drmInfo = []; let firstPdtIndex = null; @@ -180,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); diff --git a/tests/unit/controller/eme-controller.js b/tests/unit/controller/eme-controller.js index def7d9a003c..912e9cab09b 100644 --- a/tests/unit/controller/eme-controller.js +++ b/tests/unit/controller/eme-controller.js @@ -80,7 +80,8 @@ describe('EMEController', () => { }); const data = { - details: { + frag: { + foundKeys: true, drmInfo: [{ format: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', reluri: 'data:text/plain;base64,AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnNoYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=' @@ -90,7 +91,7 @@ describe('EMEController', () => { emeController.onMediaAttached({ media }); emeController.onManifestParsed({ levels: fakeLevels }); - emeController.onLevelLoaded(data); + emeController.onFragLoaded(data); media.emit('encrypted', { 'initDataType': emeController._initDataType, From e840bfba7aa7d0493d1babbd412f2688df9629ee Mon Sep 17 00:00:00 2001 From: Sri Date: Mon, 28 Jan 2019 15:45:38 -0800 Subject: [PATCH 23/23] fix unit test --- src/controller/eme-controller.ts | 2 -- src/utils/eme-helper.js | 2 +- tests/unit/controller/eme-controller.js | 7 +------ 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index 8968d2b4332..57957de0aeb 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -571,8 +571,6 @@ class EMEController extends EventHandler { onManifestParsed (data: any) { this._audioCodecs = data.levels.map((level) => level.audioCodec); this._videoCodecs = data.levels.map((level) => level.videoCodec); - - this._attemptKeySystemAccess(KeySystems[this._selectedDrm], this._audioCodecs, this._videoCodecs); } onFragLoaded (data) { diff --git a/src/utils/eme-helper.js b/src/utils/eme-helper.js index dbabbb1b21b..df712797f73 100644 --- a/src/utils/eme-helper.js +++ b/src/utils/eme-helper.js @@ -16,7 +16,7 @@ export function base64ToUint8Array (base64String) { /** * https://www.w3.org/TR/eme-initdata-cenc/ - * @param {ArrayBuffer} binary data from the URI in the manifest + * @param {ArrayBuffer | ArrayBufferView} binary data from the URI in the manifest * @returns {Uint8Array} */ export function buildPlayReadyPSSHBox (binary) { diff --git a/tests/unit/controller/eme-controller.js b/tests/unit/controller/eme-controller.js index 81fa190c44c..ee4a8c3ec06 100644 --- a/tests/unit/controller/eme-controller.js +++ b/tests/unit/controller/eme-controller.js @@ -64,7 +64,7 @@ describe('EMEController', function () { }, 0); }); - it('should retrieve PSSH data if it exists in manifest', function (done) { + it('should retrieve PSSH data if it exists in manifest', function () { let reqMediaKsAccessSpy = sinon.spy(() => { return Promise.resolve({ // Media-keys mock @@ -97,10 +97,5 @@ describe('EMEController', function () { expect(emeController._initDataType).to.equal('cenc'); expect(62).to.equal(emeController._initData.byteLength); - - setTimeout(() => { - expect(emeController._isMediaEncrypted).to.equal(true); - done(); - }, 0); }); });