From 4fb5fdb61d1833b034ac39a6cd1be65d33804d42 Mon Sep 17 00:00:00 2001 From: Sri Date: Wed, 12 Sep 2018 21:35:13 -0700 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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'); + }); +});