diff --git a/AUTHORS b/AUTHORS index 1db255c8bc..701a828f33 100644 --- a/AUTHORS +++ b/AUTHORS @@ -94,3 +94,4 @@ Raymond Cheng Blue Billywig <*@bluebillywig.com> João Nabais Koen Romers +Zhenghang Chen diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 4488e1ffbc..7e0989a110 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -134,3 +134,4 @@ Raymond Cheng Janroel Koppen João Nabais Koen Romers +Zhenghang Chen diff --git a/build/types/core b/build/types/core index b3e9ae7546..4cb896d72f 100644 --- a/build/types/core +++ b/build/types/core @@ -24,6 +24,7 @@ +../../lib/media/drm_engine.js +../../lib/media/gap_jumping_controller.js +../../lib/media/manifest_parser.js ++../../lib/media/media_source_capabilities.js +../../lib/media/media_source_engine.js +../../lib/media/mp4_segment_index_parser.js +../../lib/media/play_rate_controller.js diff --git a/lib/media/media_source_capabilities.js b/lib/media/media_source_capabilities.js new file mode 100644 index 0000000000..8c0e0f2f64 --- /dev/null +++ b/lib/media/media_source_capabilities.js @@ -0,0 +1,36 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.media.Capabilities'); + +/** + * @summary + * This is for capturing all media source capabilities on current platform. + * And this is for static check and can not be constructed. + */ +shaka.media.Capabilities = class { + /** + * Cache browser engine call to improve performance on some poor platforms + * + * @param {string} type + * @return {boolean} + */ + static isTypeSupported(type) { + const supportMap = shaka.media.Capabilities.MediaSourceTypeSupportMap; + if (supportMap.has(type)) { + return supportMap.get(type); + } + const currentSupport = MediaSource.isTypeSupported(type); + supportMap.set(type, currentSupport); + return currentSupport; + } +}; + +/** + * Public it for unit test, and developer could also check the support map. + * @type {!Map.} + */ +shaka.media.Capabilities.MediaSourceTypeSupportMap = new Map(); diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 285632964a..d5ff5af8cb 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -8,6 +8,7 @@ goog.provide('shaka.media.MediaSourceEngine'); goog.require('goog.asserts'); goog.require('shaka.log'); +goog.require('shaka.media.Capabilities'); goog.require('shaka.media.ContentWorkarounds'); goog.require('shaka.media.ClosedCaptionParser'); goog.require('shaka.media.IClosedCaptionParser'); @@ -175,7 +176,7 @@ shaka.media.MediaSourceEngine = class { stream.mimeType, stream.codecs); const extendedMimeType = shaka.util.MimeUtils.getExtendedType(stream); return shaka.text.TextEngine.isTypeSupported(fullMimeType) || - MediaSource.isTypeSupported(extendedMimeType) || + shaka.media.Capabilities.isTypeSupported(extendedMimeType) || shaka.media.Transmuxer.isSupported(fullMimeType, stream.type); } @@ -231,7 +232,7 @@ shaka.media.MediaSourceEngine = class { if (shaka.text.TextEngine.isTypeSupported(type)) { support[type] = true; } else { - support[type] = MediaSource.isTypeSupported(type) || + support[type] = shaka.media.Capabilities.isTypeSupported(type) || shaka.media.Transmuxer.isSupported(type); } } else { @@ -365,7 +366,8 @@ shaka.media.MediaSourceEngine = class { if (contentType == ContentType.TEXT) { this.reinitText(mimeType, sequenceMode); } else { - if ((forceTransmux || !MediaSource.isTypeSupported(mimeType)) && + if ((forceTransmux || + !shaka.media.Capabilities.isTypeSupported(mimeType)) && shaka.media.Transmuxer.isSupported(mimeType, contentType)) { this.transmuxers_[contentType] = new shaka.media.Transmuxer(mimeType); diff --git a/lib/media/transmuxer.js b/lib/media/transmuxer.js index 126a37f2e7..18b07ca853 100644 --- a/lib/media/transmuxer.js +++ b/lib/media/transmuxer.js @@ -7,6 +7,7 @@ goog.provide('shaka.media.Transmuxer'); goog.require('goog.asserts'); +goog.require('shaka.media.Capabilities'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.IDestroyable'); @@ -71,6 +72,7 @@ shaka.media.Transmuxer = class { */ static isSupported(mimeType, contentType) { const Transmuxer = shaka.media.Transmuxer; + const Capabilities = shaka.media.Capabilities; const isTs = Transmuxer.isTsContainer_(mimeType); const isAac = Transmuxer.isAacContainer_(mimeType); @@ -80,11 +82,11 @@ shaka.media.Transmuxer = class { } if (isAac) { - return MediaSource.isTypeSupported(Transmuxer.convertAacCodecs_()); + return Capabilities.isTypeSupported(Transmuxer.convertAacCodecs_()); } if (contentType) { - return MediaSource.isTypeSupported( + return Capabilities.isTypeSupported( Transmuxer.convertTsCodecs_(contentType, mimeType)); } @@ -92,8 +94,8 @@ shaka.media.Transmuxer = class { const audioMime = Transmuxer.convertTsCodecs_(ContentType.AUDIO, mimeType); const videoMime = Transmuxer.convertTsCodecs_(ContentType.VIDEO, mimeType); - return MediaSource.isTypeSupported(audioMime) || - MediaSource.isTypeSupported(videoMime); + return Capabilities.isTypeSupported(audioMime) || + Capabilities.isTypeSupported(videoMime); } diff --git a/lib/polyfill/media_capabilities.js b/lib/polyfill/media_capabilities.js index 38bdceff70..2b758b5f28 100644 --- a/lib/polyfill/media_capabilities.js +++ b/lib/polyfill/media_capabilities.js @@ -7,6 +7,7 @@ goog.provide('shaka.polyfill.MediaCapabilities'); goog.require('shaka.log'); +goog.require('shaka.media.Capabilities'); goog.require('shaka.polyfill'); goog.require('shaka.util.Error'); goog.require('shaka.util.Platform'); @@ -100,11 +101,14 @@ shaka.polyfill.MediaCapabilities = class { const videoConfig = mediaDecodingConfig['video']; const audioConfig = mediaDecodingConfig['audio']; + const Capabilities = shaka.media.Capabilities; + if (mediaDecodingConfig.type == 'media-source') { if (!shaka.util.Platform.supportsMediaSource()) { return res; } - // Use 'MediaSource.isTypeSupported' to check if the stream is supported. + // Use 'shaka.media.Capabilities.isTypeSupported'to check if + // the stream is supported. // Cast platforms will additionally check canDisplayType(), which // accepts extended MIME type parameters. // See: https://github.com/shaka-project/shaka-player/issues/4726 @@ -114,7 +118,7 @@ shaka.polyfill.MediaCapabilities = class { isSupported = shaka.polyfill.MediaCapabilities.canCastDisplayType_(videoConfig); } else { - isSupported = MediaSource.isTypeSupported(videoConfig.contentType); + isSupported = Capabilities.isTypeSupported(videoConfig.contentType); } if (!isSupported) { return res; @@ -123,7 +127,7 @@ shaka.polyfill.MediaCapabilities = class { if (audioConfig) { const contentType = audioConfig.contentType; - const isSupported = MediaSource.isTypeSupported(contentType); + const isSupported = Capabilities.isTypeSupported(contentType); if (!isSupported) { return res; } @@ -246,8 +250,8 @@ shaka.polyfill.MediaCapabilities = class { shaka.util.Error.Code.CAST_API_UNAVAILABLE); } else if (!(cast.__platform__ && cast.__platform__.canDisplayType)) { shaka.log.warning('Expected cast APIs to be available! Falling back to ' + - 'MediaSource.isTypeSupported() for type support.'); - return MediaSource.isTypeSupported(videoConfig.contentType); + 'shaka.media.Capabilities.isTypeSupported() for type support.'); + return shaka.media.Capabilities.isTypeSupported(videoConfig.contentType); } let displayType = videoConfig.contentType; diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index b3fb4c0a92..9cdc80f06a 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -8,6 +8,7 @@ goog.provide('shaka.util.StreamUtils'); goog.require('goog.asserts'); goog.require('shaka.log'); +goog.require('shaka.media.Capabilities'); goog.require('shaka.text.TextEngine'); goog.require('shaka.util.Functional'); goog.require('shaka.util.LanguageUtils'); @@ -437,6 +438,7 @@ shaka.util.StreamUtils = class { // See: https://github.com/shaka-project/shaka-player/issues/3860 const video = variant.video; const ContentType = shaka.util.ManifestParserUtils.ContentType; + const Capabilities = shaka.media.Capabilities; if (video) { let videoCodecs = shaka.util.StreamUtils.getCorrectVideoCodecs_(video.codecs); @@ -454,13 +456,13 @@ shaka.util.StreamUtils = class { shaka.util.StreamUtils.getCorrectAudioCodecs_(audioCodecs); const audioFullType = shaka.util.MimeUtils.getFullOrConvertedType( video.mimeType, audioCodecs, ContentType.AUDIO); - if (!MediaSource.isTypeSupported(audioFullType)) { + if (!Capabilities.isTypeSupported(audioFullType)) { return false; } } const fullType = shaka.util.MimeUtils.getFullOrConvertedType( video.mimeType, videoCodecs, ContentType.VIDEO); - if (!MediaSource.isTypeSupported(fullType)) { + if (!Capabilities.isTypeSupported(fullType)) { return false; } } @@ -470,7 +472,7 @@ shaka.util.StreamUtils = class { shaka.util.StreamUtils.getCorrectAudioCodecs_(audio.codecs); const fullType = shaka.util.MimeUtils.getFullOrConvertedType( audio.mimeType, codecs, ContentType.AUDIO); - if (!MediaSource.isTypeSupported(fullType)) { + if (!Capabilities.isTypeSupported(fullType)) { return false; } } diff --git a/test/polyfill/media_capabilities_unit.js b/test/polyfill/media_capabilities_unit.js index cda21813eb..716ba78e49 100644 --- a/test/polyfill/media_capabilities_unit.js +++ b/test/polyfill/media_capabilities_unit.js @@ -12,6 +12,7 @@ describe('MediaCapabilities', () => { const originalRequestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess; const originalMediaCapabilities = navigator.mediaCapabilities; + const supportMap = shaka.media.Capabilities.MediaSourceTypeSupportMap; /** @type {MediaDecodingConfiguration} */ let mockDecodingConfig; @@ -102,18 +103,12 @@ describe('MediaCapabilities', () => { describe('decodingInfo', () => { it('should check codec support when MediaDecodingConfiguration.type ' + 'is "media-source"', () => { - const isTypeSupportedSpy = - spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true); + expect(window['MediaSource']['isTypeSupported']).toBeDefined(); shaka.polyfill.MediaCapabilities.install(); navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); - expect(isTypeSupportedSpy).toHaveBeenCalledTimes(2); - expect(isTypeSupportedSpy).toHaveBeenCalledWith( - mockDecodingConfig.video.contentType, - ); - expect(isTypeSupportedSpy).toHaveBeenCalledWith( - mockDecodingConfig.audio.contentType, - ); + expect(supportMap.has(mockDecodingConfig.video.contentType)).toBe(true); + expect(supportMap.has(mockDecodingConfig.audio.contentType)).toBe(true); }); it('should check codec support when MediaDecodingConfiguration.type ' + @@ -218,9 +213,7 @@ describe('MediaCapabilities', () => { const isChromecastSpy = spyOn(shaka['util']['Platform'], 'isChromecast').and.returnValue(true); - const isTypeSupportedSpy = - spyOn(window['MediaSource'], 'isTypeSupported') - .and.returnValue(true); + expect(window['MediaSource']['isTypeSupported']).toBeDefined(); shaka.polyfill.MediaCapabilities.install(); await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig); @@ -230,7 +223,10 @@ describe('MediaCapabilities', () => { expect(isChromecastSpy).toHaveBeenCalledTimes(2); // 1 (fallback in canCastDisplayType()) + // 1 (mockDecodingConfig.audio). - expect(isTypeSupportedSpy).toHaveBeenCalledTimes(2); + expect(supportMap.has(mockDecodingConfig.video.contentType)) + .toBe(true); + expect(supportMap.has(mockDecodingConfig.audio.contentType)) + .toBe(true); }); it('should use cast.__platform__.canDisplayType for "supported" field ' + @@ -244,8 +240,7 @@ describe('MediaCapabilities', () => { const isChromecastSpy = spyOn(shaka['util']['Platform'], 'isChromecast').and.returnValue(true); - const isTypeSupportedSpy = - spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true); + expect(window['MediaSource']['isTypeSupported']).toBeDefined(); // Tests an HDR stream's extended MIME type is correctly provided. mockDecodingConfig.video.transferFunction = 'pq'; @@ -254,14 +249,17 @@ describe('MediaCapabilities', () => { // Round to a whole number since we can't rely on number => string // conversion precision on all devices. mockDecodingConfig.video.framerate = 24; + + const chromecastType = + 'video/mp4; ' + + 'codecs="hev1.2.4.L153.B0"; ' + + 'width=512; ' + + 'height=288; ' + + 'framerate=24; ' + + 'eotf=smpte2084'; mockCanDisplayType.and.callFake((type) => { - expect(type).toBe( - 'video/mp4; ' + - 'codecs="hev1.2.4.L153.B0"; ' + - 'width=512; ' + - 'height=288; ' + - 'framerate=24; ' + - 'eotf=smpte2084'); + expect(type).toBe(chromecastType); + supportMap.set(type, true); return true; }); @@ -271,7 +269,7 @@ describe('MediaCapabilities', () => { // 1 (during install()) + 1 (for video config check). expect(isChromecastSpy).toHaveBeenCalledTimes(2); // 1 (mockDecodingConfig.audio). - expect(isTypeSupportedSpy).toHaveBeenCalledTimes(1); + expect(supportMap.has(chromecastType)).toBe(true); // Called once in canCastDisplayType. expect(mockCanDisplayType).toHaveBeenCalledTimes(1); });