diff --git a/src/controller/eme-controller.js b/src/controller/eme-controller.js index d387ff9c487..157b7e5aad2 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; @@ -88,7 +88,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; @@ -104,6 +106,9 @@ class EMEController extends EventHandler { this._isMediaEncrypted = false; this._requestLicenseFailureCount = 0; + + this._initData = null; + this._initDataType = ''; } /** @@ -195,7 +200,6 @@ class EMEController extends EventHandler { mediaKeysListItem.mediaKeys = mediaKeys; logger.log(`Media-keys created for key-system "${keySystem}"`); - this._onMediaKeysCreated(); }) .catch((err) => { @@ -240,7 +244,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 +359,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 +472,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 +489,38 @@ class EMEController extends EventHandler { this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs); } + + onFragLoaded () { + if (!this._emeEnabled) { + return; + } + + // add initData and type if they are included in playlist + if (this._initData && !this._hasSetMediaKeys) { + this._onMediaEncrypted(this._initDataType, this._initData); + } + } + + /** + * @param {object} data + */ + 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 diff --git a/src/utils/base64toArrayBuffer.js b/src/utils/base64toArrayBuffer.js new file mode 100644 index 00000000000..aa8f80df50e --- /dev/null +++ b/src/utils/base64toArrayBuffer.js @@ -0,0 +1,15 @@ +/** + * @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/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'); + }); +});