diff --git a/AUTHORS b/AUTHORS index ce01202bc2..d2a77ceb39 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,7 @@ Charter Communications Inc <*@charter.com> Code It <*@code-it.fr> Damien Deis Dany L'Hébreux +Dave Nicholas Dl Dador Edgeware AB <*@edgeware.tv> Enson Choy diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2cb97be516..ad4022bc07 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -43,6 +43,7 @@ Chris Fillmore Costel Madalin Grecu Damien Deis Dany L'Hébreux +Dave Nicholas Dl Dador Donato Borrello Duc Pham diff --git a/lib/polyfill/media_capabilities.js b/lib/polyfill/media_capabilities.js index 038bb72f19..87cba19126 100644 --- a/lib/polyfill/media_capabilities.js +++ b/lib/polyfill/media_capabilities.js @@ -185,10 +185,25 @@ shaka.polyfill.MediaCapabilities = class { mediaKeySystemConfig.videoCapabilities = videoCapabilities; } + const cacheKey = shaka.polyfill.MediaCapabilities + .generateKeySystemCacheKey_( + mediaDecodingConfig.video.contentType, + mediaDecodingConfig.audio.contentType, + mediaDecodingConfig.keySystemConfiguration.keySystem); + let keySystemAccess; try { - keySystemAccess = await navigator.requestMediaKeySystemAccess( - mediaCapkeySystemConfig.keySystem, [mediaKeySystemConfig]); + if (cacheKey in shaka.polyfill.MediaCapabilities + .memoizedMediaKeySystemAccessRequests_) { + keySystemAccess = shaka.polyfill.MediaCapabilities + .memoizedMediaKeySystemAccessRequests_[cacheKey]; + } else { + keySystemAccess = await navigator.requestMediaKeySystemAccess( + mediaCapkeySystemConfig.keySystem, [mediaKeySystemConfig]); + shaka.polyfill.MediaCapabilities + .memoizedMediaKeySystemAccessRequests_[cacheKey] = + keySystemAccess; + } } catch (e) { shaka.log.info('navigator.requestMediaKeySystemAccess failed.'); } @@ -201,11 +216,24 @@ shaka.polyfill.MediaCapabilities = class { return res; } + + /** + * A method for generating a key for the MediaKeySystemAccessRequests cache. + * + * @param {!string} videoCodec + * @param {!string} audioCodec + * @param {!string} keySystem + * @return {!string} + * @private + */ + static generateKeySystemCacheKey_(videoCodec, audioCodec, keySystem) { + return `${videoCodec}#${audioCodec}#${keySystem}`; + } }; /** * A copy of the MediaCapabilities instance, to prevent Safari from - * garbage-collecting the polyfilled method on it. We make it public and export + * garbage-collecting the polyfilled method on it. We make it public and export * it to ensure that it is not stripped out by the compiler. * * @type {MediaCapabilities} @@ -213,6 +241,16 @@ shaka.polyfill.MediaCapabilities = class { */ shaka.polyfill.MediaCapabilities.originalMcap = null; +/** + * A cache that stores the MediaKeySystemAccess result of calling + * `navigator.requestMediaKeySystemAccess` by a key combination of + * video/audio codec and key system string. + * + * @type {(Object<(!string), (!MediaKeySystemAccess)>)} + * @export + */ +shaka.polyfill.MediaCapabilities.memoizedMediaKeySystemAccessRequests_ = {}; + // Install at a lower priority than MediaSource polyfill, so that we have // MediaSource available first. shaka.polyfill.register(shaka.polyfill.MediaCapabilities.install, -1); diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 6b305d9757..236eff0daf 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -515,7 +515,6 @@ shaka.util.StreamUtils = class { const mediaCapabilities = navigator.mediaCapabilities; - const operations = []; const getVariantDecodingInfos = (async (variant, decodingConfig) => { try { const result = await mediaCapabilities.decodingInfo(decodingConfig); @@ -531,11 +530,14 @@ shaka.util.StreamUtils = class { const decodingConfigs = shaka.util.StreamUtils.getDecodingConfigs_( variant, usePersistentLicenses, srcEquals); + // The reason we are performing this await in a loop rather than + // batching into a `promise.all` is performance related. + // https://github.com/shaka-project/shaka-player/pull/4708#discussion_r1022581178 for (const config of decodingConfigs) { - operations.push(getVariantDecodingInfos(variant, config)); + // eslint-disable-next-line no-await-in-loop + await getVariantDecodingInfos(variant, config); } } - await Promise.all(operations); } diff --git a/test/polyfill/media_capabilities_unit.js b/test/polyfill/media_capabilities_unit.js new file mode 100644 index 0000000000..b709c93fd7 --- /dev/null +++ b/test/polyfill/media_capabilities_unit.js @@ -0,0 +1,176 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('MediaCapabilities', () => { + const originalVendor = navigator.vendor; + const originalUserAgent = navigator.userAgent; + const originalRequestMediaKeySystemAccess = + navigator.requestMediaKeySystemAccess; + const originalMediaCapabilities = navigator.mediaCapabilities; + /** @type {MediaDecodingConfiguration} */ + let mockDecodingConfig; + + beforeAll(() => { + Object.defineProperty(window['navigator'], + 'userAgent', { + value: 'unknown', configurable: true, + writable: true, + }); + Object.defineProperty(window['navigator'], + 'vendor', { + value: 'unknown', configurable: true, + writable: true, + }); + Object.defineProperty(window['navigator'], + 'requestMediaKeySystemAccess', { + value: 'unknown', configurable: true, + writable: true, + }); + Object.defineProperty(window['navigator'], + 'mediaCapabilities', { + value: undefined, configurable: true, + writable: true, + }); + }); + + beforeEach(() => { + mockDecodingConfig = { + audio: { + bitrate: 100891, + channels: 2, + contentType: 'audio/mp4; codecs="mp4a.40.2"', + samplerate: 48000, + spatialRendering: false, + }, + keySystemConfiguration: { + audio: {robustness: 'SW_SECURE_CRYPTO'}, + distinctiveIdentifier: 'optional', + initDataType: 'cenc', + keySystem: 'com.widevine.alpha', + persistentState: 'optional', + sessionTypes: ['temporary'], + video: {robustness: 'SW_SECURE_CRYPTO'}, + }, + type: 'media-source', + video: { + bitrate: 349265, + contentType: 'video/mp4; codecs="avc1.4D4015"', + framerate: 23.976023976023978, + height: 288, + width: 512, + }, + }; + shaka.polyfill.MediaCapabilities.memoizedMediaKeySystemAccessRequests_ = {}; + }); + + afterAll(() => { + Object.defineProperty(window['navigator'], + 'userAgent', {value: originalUserAgent}); + Object.defineProperty(window['navigator'], + 'vendor', {value: originalVendor}); + Object.defineProperty(window['navigator'], + 'requestMediaKeySystemAccess', + {value: originalRequestMediaKeySystemAccess}); + Object.defineProperty(window['navigator'], + 'mediaCapabilities', {value: originalMediaCapabilities}); + }); + + describe('install', () => { + it('should define decoding info method', () => { + shaka.polyfill.MediaCapabilities.install(); + + expect(navigator.mediaCapabilities.decodingInfo).toBeDefined(); + }); + }); + + describe('decodingInfo', () => { + it('should check codec support when MediaDecodingConfiguration.type ' + + 'is "media-source"', () => { + const isTypeSupportedSpy = + spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true); + shaka.polyfill.MediaCapabilities.install(); + navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); + + expect(isTypeSupportedSpy).toHaveBeenCalledTimes(2); + expect(isTypeSupportedSpy).toHaveBeenCalledWith( + mockDecodingConfig.video.contentType, + ); + expect(isTypeSupportedSpy).toHaveBeenCalledWith( + mockDecodingConfig.audio.contentType, + ); + }); + + it('should check codec support when MediaDecodingConfiguration.type ' + + 'is "file"', () => { + const supportsMediaTypeSpy = + spyOn(shaka['util']['Platform'], + 'supportsMediaType').and.returnValue(true); + mockDecodingConfig.type = 'file'; + shaka.polyfill.MediaCapabilities.install(); + navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); + + expect(supportsMediaTypeSpy).toHaveBeenCalledTimes(2); + expect(supportsMediaTypeSpy).toHaveBeenCalledWith( + mockDecodingConfig.video.contentType, + ); + expect(supportsMediaTypeSpy).toHaveBeenCalledWith( + mockDecodingConfig.audio.contentType, + ); + }); + + it('should check MediaKeySystem when keySystemConfiguration is present', + async () => { + const mockResult = {mockKeySystemAccess: 'mockKeySystemAccess'}; + spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true); + const requestKeySystemAccessSpy = + spyOn(window['navigator'], + 'requestMediaKeySystemAccess').and.returnValue(mockResult); + + shaka.polyfill.MediaCapabilities.install(); + const result = await navigator.mediaCapabilities + .decodingInfo(mockDecodingConfig); + + expect(requestKeySystemAccessSpy).toHaveBeenCalledWith( + 'com.widevine.alpha', + [{ + audioCapabilities: [ + { + robustness: 'SW_SECURE_CRYPTO', + contentType: 'audio/mp4; codecs="mp4a.40.2"', + }, + ], + distinctiveIdentifier: 'optional', + initDataTypes: ['cenc'], + persistentState: 'optional', + sessionTypes: ['temporary'], + videoCapabilities: [{ + robustness: 'SW_SECURE_CRYPTO', + contentType: 'video/mp4; codecs="avc1.4D4015"', + }], + }], + ); + expect(result.keySystemAccess).toEqual(mockResult); + }); + + it('should read previously requested codec/key system'+ + 'combinations from cache', async () => { + const mockResult = {mockKeySystemAccess: 'mockKeySystemAccess'}; + spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true); + const requestKeySystemAccessSpy = + spyOn(window['navigator'], + 'requestMediaKeySystemAccess').and.returnValue(mockResult); + + shaka.polyfill.MediaCapabilities.install(); + await navigator.mediaCapabilities + .decodingInfo(mockDecodingConfig); + await navigator.mediaCapabilities + .decodingInfo(mockDecodingConfig); + + expect(requestKeySystemAccessSpy) + .toHaveBeenCalledTimes(1); + }); + }); +});