diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index 3ef8c86e4ce..b3f2ebcc3ad 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -83,7 +83,7 @@ class EMEController extends EventHandler { private _widevineLicenseUrl?: string; private _licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void; private _emeEnabled: boolean; - private _requestMediaKeySystemAccess: MediaKeyFunc | null + private _requestMediaKeySystemAccess: MediaKeyFunc | null; private _config: EMEControllerConfig; private _mediaKeysList: MediaKeysListItem[] = []; @@ -91,6 +91,8 @@ class EMEController extends EventHandler { private _hasSetMediaKeys: boolean = false; private _requestLicenseFailureCount: number = 0; + private mediaKeysPromise: Promise | null = null; + /** * @constructs * @param {Hls} hls Our Hls.js instance @@ -143,13 +145,14 @@ class EMEController extends EventHandler { logger.log('Requesting encrypted media key-system access'); // expecting interface like window.navigator.requestMediaKeySystemAccess - this.requestMediaKeySystemAccess(keySystem, mediaKeySystemConfigs) - .then((mediaKeySystemAccess) => { - this._onMediaKeySystemAccessObtained(keySystem, mediaKeySystemAccess); - }) - .catch((err) => { - logger.error(`Failed to obtain key-system "${keySystem}" access:`, err); - }); + const keySystemAccessPromise = this.requestMediaKeySystemAccess(keySystem, mediaKeySystemConfigs); + + this.mediaKeysPromise = keySystemAccessPromise.then((mediaKeySystemAccess) => + this._onMediaKeySystemAccessObtained(keySystem, mediaKeySystemAccess)); + + keySystemAccessPromise.catch((err) => { + logger.error(`Failed to obtain key-system "${keySystem}" access:`, err); + }); } get requestMediaKeySystemAccess () { @@ -166,7 +169,7 @@ class EMEController extends EventHandler { * @param {string} keySystem * @param {MediaKeySystemAccess} mediaKeySystemAccess https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemAccess */ - private _onMediaKeySystemAccessObtained (keySystem: KeySystems, mediaKeySystemAccess: MediaKeySystemAccess) { + private _onMediaKeySystemAccessObtained (keySystem: KeySystems, mediaKeySystemAccess: MediaKeySystemAccess): Promise { logger.log(`Access for key-system "${keySystem}" obtained`); const mediaKeysListItem: MediaKeysListItem = { @@ -177,17 +180,22 @@ class EMEController extends EventHandler { this._mediaKeysList.push(mediaKeysListItem); - mediaKeySystemAccess.createMediaKeys() + const mediaKeysPromise = Promise.resolve().then(() => mediaKeySystemAccess.createMediaKeys()) .then((mediaKeys) => { mediaKeysListItem.mediaKeys = mediaKeys; logger.log(`Media-keys created for key-system "${keySystem}"`); this._onMediaKeysCreated(); - }) - .catch((err) => { - logger.error('Failed to create media-keys:', err); + + return mediaKeys; }); + + mediaKeysPromise.catch((err) => { + logger.error('Failed to create media-keys:', err); + }); + + return mediaKeysPromise; } /** @@ -235,20 +243,37 @@ class EMEController extends EventHandler { /** * @private - * @param {string} initDataType - * @param {ArrayBuffer|null} initData + * @param e {MediaEncryptedEvent} */ private _onMediaEncrypted = (e: MediaEncryptedEvent) => { logger.log(`Media is encrypted using "${e.initDataType}" init data type`); - this._attemptSetMediaKeys(); - this._generateRequestWithPreferredKeySession(e.initDataType, e.initData); + if (!this.mediaKeysPromise) { + logger.error('Fatal: Media is encrypted but no CDM access or no keys have been requested'); + this.hls.trigger(Event.ERROR, { + type: ErrorTypes.KEY_SYSTEM_ERROR, + details: ErrorDetails.KEY_SYSTEM_NO_KEYS, + fatal: true + }); + return; + } + + const finallySetKeyAndStartSession = (mediaKeys) => { + if (!this._media) { + return; + } + this._attemptSetMediaKeys(mediaKeys); + this._generateRequestWithPreferredKeySession(e.initDataType, e.initData); + }; + + // Could use `Promise.finally` but some Promise polyfills are missing it + this.mediaKeysPromise.then(finallySetKeyAndStartSession).catch(finallySetKeyAndStartSession); } /** * @private */ - private _attemptSetMediaKeys () { + private _attemptSetMediaKeys (mediaKeys?: MediaKeys) { if (!this._media) { throw new Error('Attempted to set mediaKeys without first attaching a media element'); } diff --git a/tests/unit/controller/eme-controller.js b/tests/unit/controller/eme-controller.js index 95d41d2a38e..cad40ea2deb 100644 --- a/tests/unit/controller/eme-controller.js +++ b/tests/unit/controller/eme-controller.js @@ -75,7 +75,7 @@ describe('EMEController', function () { }, 0); }); - it('should trigger key system error when bad encrypted data is received', function (done) { + it('should trigger key system error(s) when bad encrypted data is received', function (done) { let reqMediaKsAccessSpy = sinon.spy(function () { return Promise.resolve({ // Media-keys mock @@ -98,8 +98,9 @@ describe('EMEController', function () { media.emit('encrypted', badData); setTimeout(function () { + expect(emeController.hls.trigger).to.have.been.calledTwice; 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); + expect(emeController.hls.trigger.args[1][1].details).to.equal(ErrorDetails.KEY_SYSTEM_NO_SESSION); done(); }, 0); });