diff --git a/src/controller/eme-controller.ts b/src/controller/eme-controller.ts index bcff60dc65d..a46e3ce618e 100644 --- a/src/controller/eme-controller.ts +++ b/src/controller/eme-controller.ts @@ -303,6 +303,7 @@ class EMEController extends Logger implements ComponentAPI { ); }); return keySystemAccess.then((mediaKeySystemAccess) => { + this.throwIfDestroyed(); this.log( `Access for key-system "${mediaKeySystemAccess.keySystem}" obtained`, ); @@ -1703,6 +1704,9 @@ class EMEController extends Logger implements ComponentAPI { `Selecting key-system from session-keys ${keyFormats.join(', ')}`, ); this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); + // Until onMediaEncrypted is called this promise is unhandled, add a catch to prevent unhandled rejection + // when result is not used. + this.keyFormatPromise.catch(() => {}); } } diff --git a/tests/unit/controller/eme-controller.ts b/tests/unit/controller/eme-controller.ts index 5d49cb667cb..26b594c0c71 100644 --- a/tests/unit/controller/eme-controller.ts +++ b/tests/unit/controller/eme-controller.ts @@ -33,7 +33,12 @@ type EMEControllerTestable = Omit & { data: MediaAttachedData, ) => void; onMediaDetached: () => void; + onManifestLoaded: ( + event: Events.MANIFEST_LOADED, + data: { sessionKeys: LevelKey[] }, + ) => void; media: HTMLMediaElement | null; + keyFormatPromise: Promise | null; getKeyStatuses: (mediaKeySessionContext: MediaKeySessionContext) => { [keyId: string]: MediaKeyStatus; }; @@ -582,6 +587,112 @@ describe('EMEController', function () { }); }); + it('should reject pending session-key key-system selection with invalid state when destroyed', function () { + let resolveKeySystemAccess!: (value: MediaKeySystemAccess) => void; + const keySystemAccessPromise = new Promise( + (resolve) => { + resolveKeySystemAccess = resolve; + }, + ); + const createMediaKeysSpy = sinon.spy(() => + Promise.resolve(new MediaKeysMock()), + ); + const reqMediaKsAccessSpy = sinon.spy(() => keySystemAccessPromise); + + setupEach({ + emeEnabled: true, + requestMediaKeySystemAccessFunc: reqMediaKsAccessSpy, + drmSystems: { + 'com.apple.fps': {}, + }, + }); + + const levelKey = getParsedLevelKey(); + + emeController.onManifestLoaded(Events.MANIFEST_LOADED, { + sessionKeys: [levelKey], + }); + + const keyFormatPromise = emeController.keyFormatPromise; + if (!keyFormatPromise) { + throw new Error('Expected pending key-system selection'); + } + + emeController.destroy(); + const mediaKeySystemAccess: MediaKeySystemAccess = { + keySystem: KeySystems.FAIRPLAY, + createMediaKeys: createMediaKeysSpy, + getConfiguration: () => ({}), + }; + resolveKeySystemAccess(mediaKeySystemAccess); + + return keyFormatPromise.then( + () => { + throw new Error('Expected key-system selection to reject'); + }, + (error) => { + expect(error).to.be.instanceOf(Error); + expect(error.message).to.equal('invalid state'); + expect(createMediaKeysSpy).not.to.have.been.called; + }, + ); + }); + + it('should handle unused session-key key-system selection rejections', function () { + let rejectKeySystemAccess!: (error: Error) => void; + const keySystemAccessPromise = new Promise( + (resolve, reject) => { + rejectKeySystemAccess = reject; + }, + ); + let unhandledRejection: PromiseRejectionEvent | null = null; + const onUnhandledRejection = (event: PromiseRejectionEvent) => { + unhandledRejection = event; + event.preventDefault(); + }; + + self.addEventListener('unhandledrejection', onUnhandledRejection); + + setupEach({ + emeEnabled: true, + requestMediaKeySystemAccessFunc: () => keySystemAccessPromise, + drmSystems: { + 'com.apple.fps': {}, + }, + }); + + emeController.onManifestLoaded(Events.MANIFEST_LOADED, { + sessionKeys: [getParsedLevelKey()], + }); + + const keyFormatPromise = emeController.keyFormatPromise; + if (!keyFormatPromise) { + throw new Error('Expected key-system selection promise'); + } + + rejectKeySystemAccess(new Error('key-system access failed')); + + return new Promise((resolve) => { + self.setTimeout(resolve, 0); + }) + .then(() => { + expect(unhandledRejection).to.equal(null); + expect(emeController.hls.trigger).not.to.have.been.called; + return keyFormatPromise.then( + () => { + throw new Error('Expected key-system selection to reject'); + }, + (error) => { + expect(error).to.be.instanceOf(Error); + expect(error.message).to.equal('key-system access failed'); + }, + ); + }) + .finally(() => { + self.removeEventListener('unhandledrejection', onUnhandledRejection); + }); + }); + it('should remove media property when media is detached', function () { const reqMediaKsAccessSpy = sinon.spy(function () { return Promise.resolve({