diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f6f106a5e..0754fed4d5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,7 @@ orbs: executors: dashjs-executor: working_directory: ~/repo + resource_class: large docker: - image: cimg/node:20.11.1 diff --git a/.eslintrc b/.eslintrc index c5548d7797..8fa6dbec61 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,6 +7,7 @@ }, "globals": { "dashjs": true, + "ManagedMediaSource": true, "WebKitMediaSource": true, "MediaSource": true, "WebKitMediaKeys": true, diff --git a/index.d.ts b/index.d.ts index f7b842ceab..51cd6b52ae 100644 --- a/index.d.ts +++ b/index.d.ts @@ -246,7 +246,7 @@ declare namespace dashjs { getAdaptationsForType(manifest: object, periodIndex: number, type: string): any[]; - getCodec(adaptation: object, representationId: number, addResolutionInfo: boolean): string; + getCodec(adaptation: object, representationIndex: number, addResolutionInfo: boolean): string; getMimeType(adaptation: object): object; @@ -754,7 +754,7 @@ declare namespace dashjs { getRepresentationSortFunction(): (a: object, b: object) => number; - getCodec(adaptation: object, representationId: number, addResolutionInfo: boolean): string; + getCodec(adaptation: object, representationIndex: number, addResolutionInfo: boolean): string; getBandwidthForRepresentation(representationId: string, periodIdx: number): number; @@ -3998,7 +3998,9 @@ declare namespace dashjs { supportsEncryptedMedia(): boolean; - supportsCodec(config: object, type: string): Promise; + isCodecSupportedBasedOnTestedConfigurations(basicConfiguration: object, type: string): boolean; + + runCodecSupportCheck(basicConfiguration: object, type: string): Promise; setEncryptedMediaSupported(value: boolean): void; diff --git a/samples/dash-if-reference-player/app/sources.json b/samples/dash-if-reference-player/app/sources.json index 34c7ec75aa..a27411e478 100644 --- a/samples/dash-if-reference-player/app/sources.json +++ b/samples/dash-if-reference-player/app/sources.json @@ -88,6 +88,11 @@ "acronym": "Dolby", "name": "Dolby Laboratories Inc.", "url": "https://www.dolby.com/" + }, + "arte": { + "acronym": "ARTE", + "name": "ARTE", + "url": "https://www.arte.tv/en/" } }, "items": [ @@ -473,6 +478,11 @@ "url": "https://dash.akamaized.net/dash264/CTA/imsc1/IT1-20171027_dash.mpd", "name": "IMSC1 Text Subtitles via sidecar file", "provider": "cta" + }, + { + "name": "Arte forced-subtitles", + "url": "https://arteamd1.akamaized.net/GPU/034000/034700/034755-230-A/221125154117/034755-230-A_8_DA_v20221125.mpd", + "provider": "arte" } ] }, diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js index b71b85ecd1..df3433d980 100644 --- a/src/dash/DashAdapter.js +++ b/src/dash/DashAdapter.js @@ -734,8 +734,8 @@ function DashAdapter() { * @memberOf module:DashAdapter * @instance */ - function getCodec(adaptation, representationId, addResolutionInfo) { - return dashManifestModel.getCodec(adaptation, representationId, addResolutionInfo); + function getCodec(adaptation, representationIndex, addResolutionInfo) { + return dashManifestModel.getCodec(adaptation, representationIndex, addResolutionInfo); } /** diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 7207eb6920..6c1bc09155 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -373,12 +373,12 @@ function DashManifestModel() { return adaptations; } - function getCodec(adaptation, representationId, addResolutionInfo) { + function getCodec(adaptation, representationIndex, addResolutionInfo) { let codec = null; if (adaptation && adaptation.Representation && adaptation.Representation.length > 0) { - const representation = isInteger(representationId) && representationId >= 0 && representationId < adaptation.Representation.length ? - adaptation.Representation[representationId] : adaptation.Representation[0]; + const representation = isInteger(representationIndex) && representationIndex >= 0 && representationIndex < adaptation.Representation.length ? + adaptation.Representation[representationIndex] : adaptation.Representation[0]; if (representation) { codec = representation.mimeType + ';codecs="' + representation.codecs + '"'; if (addResolutionInfo && representation.width !== undefined) { diff --git a/src/streaming/utils/Capabilities.js b/src/streaming/utils/Capabilities.js index b3b2e6f9f7..086bc11ecb 100644 --- a/src/streaming/utils/Capabilities.js +++ b/src/streaming/utils/Capabilities.js @@ -32,6 +32,7 @@ import FactoryMaker from '../../core/FactoryMaker.js'; import Constants from '../constants/Constants.js'; import ProtectionConstants from '../constants/ProtectionConstants.js'; import ObjectUtils from './ObjectUtils.js'; +import Debug from '../../core/Debug.js'; export function supportsMediaSource() { let hasManagedMediaSource = ('ManagedMediaSource' in window) @@ -46,7 +47,8 @@ function Capabilities() { let instance, settings, testedCodecConfigurations, - encryptedMediaSupported; + encryptedMediaSupported, + logger; const context = this.context; const objectUtils = ObjectUtils(context).getInstance(); @@ -54,6 +56,7 @@ function Capabilities() { function setup() { encryptedMediaSupported = false; testedCodecConfigurations = []; + logger = Debug(context).getInstance().getLogger(instance); } function setConfig(config) { @@ -84,7 +87,6 @@ function Capabilities() { } /** - * * @param {boolean} value */ function setEncryptedMediaSupported(value) { @@ -93,108 +95,183 @@ function Capabilities() { /** * Check if a codec is supported by the MediaSource. We use the MediaCapabilities API or the MSE to check. - * @param {object} config + * @param {object} basicConfiguration * @param {string} type - * @return {Promise} + * @return {Promise<>} */ - function supportsCodec(config, type) { + function runCodecSupportCheck(basicConfiguration, type) { if (type !== Constants.AUDIO && type !== Constants.VIDEO) { - return Promise.resolve(true); + return Promise.resolve(); + } + + const configurationsToTest = _getEnhancedConfigurations(basicConfiguration, type); + + if (_canUseMediaCapabilitiesApi(basicConfiguration, type)) { + return _checkCodecWithMediaCapabilities(configurationsToTest); } - if (_canUseMediaCapabilitiesApi(config, type)) { - return _checkCodecWithMediaCapabilities(config, type); + _checkCodecWithMse(configurationsToTest); + return Promise.resolve(); + } + + /** + * Checks whether a codec is supported according to the previously tested configurations. + * Note that you need to call runCodecSupportCheck() first to populate the testedCodecConfigurations array. + * This function only validates codec support based on previously tested configurations. + * @param basicConfiguration + * @param type + * @returns {*|boolean} + */ + function isCodecSupportedBasedOnTestedConfigurations(basicConfiguration, type) { + if (!basicConfiguration || !basicConfiguration.codec || (basicConfiguration.isSupported === false)) { + return false; + } + + const configurationsToTest = _getEnhancedConfigurations(basicConfiguration, type); + + const testedConfigurations = configurationsToTest + .map((config) => { + return _getTestedCodecConfiguration(config, type); + }) + .filter((config) => { + return config !== null && config !== undefined; + }) + + if (testedConfigurations && testedConfigurations.length > 0) { + return _isConfigSupported(testedConfigurations) } - return _checkCodecWithMse(config); + return true } /** * MediaCapabilitiesAPI throws an error if one of the attribute is missing. We only use it if we have all required information. - * @param {object} config - * @param {string} type - * @return {*|boolean|boolean} + * @return {boolean} * @private */ - function _canUseMediaCapabilitiesApi(config, type) { - return settings.get().streaming.capabilities.useMediaCapabilitiesApi && navigator.mediaCapabilities && navigator.mediaCapabilities.decodingInfo && ((config.codec && type === Constants.AUDIO) || (type === Constants.VIDEO && config.codec && config.width && config.height && config.bitrate && config.framerate)); + function _canUseMediaCapabilitiesApi(basicConfiguration, type) { + return _isMediaCapabilitiesApiSupported() && ((basicConfiguration.codec && type === Constants.AUDIO) || (type === Constants.VIDEO && basicConfiguration.codec && basicConfiguration.width && basicConfiguration.height && basicConfiguration.bitrate && basicConfiguration.framerate)); + } + + function _isMediaCapabilitiesApiSupported() { + return settings.get().streaming.capabilities.useMediaCapabilitiesApi && navigator.mediaCapabilities && navigator.mediaCapabilities.decodingInfo } + /** * Check codec support using the MSE - * @param {object} config - * @return {Promise | Promise} + * @param {object} configurationsToTest * @private */ - function _checkCodecWithMse(config) { - return new Promise((resolve) => { - if (!config || !config.codec) { - resolve(false); - return; - } + function _checkCodecWithMse(configurationsToTest) { + if (!configurationsToTest || !configurationsToTest.length) { + return; + } - let codec = config.codec; - if (config.width && config.height) { - codec += ';width="' + config.width + '";height="' + config.height + '"'; - } + // We only need one config here as we can not add any DRM configuration to the test + const configurationToTest = configurationsToTest[0]; - // eslint-disable-next-line no-undef - if ('ManagedMediaSource' in window && ManagedMediaSource.isTypeSupported(codec)) { - resolve(true); - return; - } else if ('MediaSource' in window && MediaSource.isTypeSupported(codec)) { - resolve(true); - return; - } else if ('WebKitMediaSource' in window && WebKitMediaSource.isTypeSupported(codec)) { - resolve(true); - return; - } + const alreadyTestedConfiguration = _getTestedCodecConfiguration(configurationToTest); + if (alreadyTestedConfiguration) { + return + } - resolve(false); - }); + let decodingInfo = { + supported: false + } + // eslint-disable-next-line no-undef + if ('ManagedMediaSource' in window && ManagedMediaSource.isTypeSupported(configurationToTest.mediaSourceCodecString)) { + decodingInfo.supported = true; + } else if ('MediaSource' in window && MediaSource.isTypeSupported(configurationToTest.mediaSourceCodecString)) { + decodingInfo.supported = true; + } else if ('WebKitMediaSource' in window && WebKitMediaSource.isTypeSupported(configurationToTest.mediaSourceCodecString)) { + decodingInfo.supported = true; + } + + configurationToTest.decodingInfo = decodingInfo; + testedCodecConfigurations.push(configurationToTest); } + /** * Check codec support using the MediaCapabilities API - * @param {object} inputConfig - * @param {string} type + * @param {object} configurationsToTest * @return {Promise} * @private */ - function _checkCodecWithMediaCapabilities(inputConfig, type) { + function _checkCodecWithMediaCapabilities(configurationsToTest) { return new Promise((resolve) => { - if (!inputConfig || !inputConfig.codec || (inputConfig.isSupported === false)) { - resolve(false); + if (!configurationsToTest || configurationsToTest.length === 0) { + resolve(); return; } - const genericMediaCapabilitiesConfiguration = _getGenericMediaCapabilitiesConfig(inputConfig, type); - const configurationsToTest = _enhanceGenericConfigurationWithKeySystemConfiguration(genericMediaCapabilitiesConfiguration, inputConfig, type) - const promises = configurationsToTest.map((configuration) => { return _checkSingleConfigurationWithMediaCapabilities(configuration); }) Promise.allSettled(promises) - .then((results) => { - const isSupported = results.some((result) => { - return !!result.value - }) - resolve(isSupported); + .then(() => { + resolve(); + }) + .catch((e) => { + logger.error(e); + resolve(); }) }); } + function _getEnhancedConfigurations(inputConfig, type) { + let configuration + + if (type === Constants.VIDEO) { + configuration = _getGenericMediaCapabilitiesVideoConfig(inputConfig) + } else if (type === Constants.AUDIO) { + configuration = _getGenericMediaCapabilitiesAudioConfig(inputConfig) + } + + configuration[type].contentType = inputConfig.codec; + configuration[type].bitrate = parseInt(inputConfig.bitrate); + configuration.type = 'media-source'; + + let mediaSourceCodecString = inputConfig.codec; + if (inputConfig.width && inputConfig.height) { + mediaSourceCodecString += ';width="' + inputConfig.width + '";height="' + inputConfig.height + '"'; + } + configuration.mediaSourceCodecString = mediaSourceCodecString; + + return _enhanceGenericConfigurationWithKeySystemConfiguration(configuration, inputConfig, type) + } + + function _enhanceGenericConfigurationWithKeySystemConfiguration(genericConfiguration, inputConfig, type) { + if (!inputConfig || !inputConfig.keySystemsMetadata || inputConfig.keySystemsMetadata.length === 0) { + return [genericConfiguration]; + } + + return inputConfig.keySystemsMetadata.map((keySystemMetadata) => { + const curr = { ...genericConfiguration }; + if (keySystemMetadata.ks) { + curr.keySystemConfiguration = {}; + if (keySystemMetadata.ks.systemString) { + curr.keySystemConfiguration.keySystem = keySystemMetadata.ks.systemString; + } + if (keySystemMetadata.ks.systemString === ProtectionConstants.WIDEVINE_KEYSTEM_STRING) { + curr.keySystemConfiguration[type] = { robustness: ProtectionConstants.ROBUSTNESS_STRINGS.WIDEVINE.SW_SECURE_CRYPTO }; + + } + } + return curr + }) + } + function _checkSingleConfigurationWithMediaCapabilities(configuration) { return new Promise((resolve) => { const alreadyTestedConfiguration = _getTestedCodecConfiguration(configuration); - if (alreadyTestedConfiguration) { - const isSupported = _isConfigSupported(alreadyTestedConfiguration.decodingInfo); - resolve(isSupported); + resolve(); return } @@ -202,19 +279,26 @@ function Capabilities() { .then((decodingInfo) => { configuration.decodingInfo = decodingInfo; testedCodecConfigurations.push(configuration); - const isSupported = _isConfigSupported(decodingInfo); - resolve(isSupported); + resolve(); + }) + .catch((e) => { + configuration.decodingInfo = { supported: false }; + testedCodecConfigurations.push(configuration); + logger.error(e); + resolve(); }) }) } - function _isConfigSupported(decodingInfo) { - return decodingInfo.supported + function _isConfigSupported(testedConfigurations) { + return testedConfigurations.some((testedConfiguration) => { + return testedConfiguration && testedConfiguration.decodingInfo && testedConfiguration.decodingInfo.supported + }); } function _getTestedCodecConfiguration(configuration) { if (!testedCodecConfigurations || testedCodecConfigurations.length === 0 || !configuration) { - return null + return } return testedCodecConfigurations.find((current) => { @@ -227,30 +311,23 @@ function Capabilities() { } - function _getGenericMediaCapabilitiesConfig(inputConfig, type) { - let configuration - - if (type === Constants.VIDEO) { - configuration = _getGenericMediaCapabilitiesVideoConfig(inputConfig) - } else if (type === Constants.AUDIO) { - configuration = _getGenericMediaCapabilitiesAudioConfig(inputConfig) - } - - configuration[type].contentType = inputConfig.codec; - configuration[type].bitrate = parseInt(inputConfig.bitrate); - configuration.type = 'media-source'; - - return configuration - } - function _getGenericMediaCapabilitiesVideoConfig(inputConfig) { const configuration = { video: {} }; - configuration.video.width = inputConfig.width; - configuration.video.height = inputConfig.height; - configuration.video.framerate = parseFloat(inputConfig.framerate); + if (!inputConfig) { + return configuration; + } + if (inputConfig.width) { + configuration.video.width = inputConfig.width; + } + if (inputConfig.height) { + configuration.video.height = inputConfig.height; + } + if (inputConfig.framerate) { + configuration.video.framerate = parseFloat(inputConfig.framerate) + } if (inputConfig.hdrMetadataType) { configuration.video.hdrMetadataType = inputConfig.hdrMetadataType; } @@ -276,27 +353,6 @@ function Capabilities() { return configuration } - function _enhanceGenericConfigurationWithKeySystemConfiguration(genericMediaCapabilitiesConfiguration, inputConfig, type) { - if (!inputConfig || !inputConfig.keySystemsMetadata || inputConfig.keySystemsMetadata.length === 0) { - return [genericMediaCapabilitiesConfiguration]; - } - - return inputConfig.keySystemsMetadata.map((keySystemMetadata) => { - const curr = { ...genericMediaCapabilitiesConfiguration }; - if (keySystemMetadata.ks) { - curr.keySystemConfiguration = {}; - if (keySystemMetadata.ks.systemString) { - curr.keySystemConfiguration.keySystem = keySystemMetadata.ks.systemString; - } - if (keySystemMetadata.ks.systemString === ProtectionConstants.WIDEVINE_KEYSTEM_STRING) { - curr.keySystemConfiguration[type] = { robustness: ProtectionConstants.ROBUSTNESS_STRINGS.WIDEVINE.SW_SECURE_CRYPTO }; - - } - } - return curr - }) - } - /** * Add additional descriptors to list of descriptors, * avoid duplicated entries @@ -345,10 +401,11 @@ function Capabilities() { } instance = { + isCodecSupportedBasedOnTestedConfigurations, isProtectionCompatible, + runCodecSupportCheck, setConfig, setEncryptedMediaSupported, - supportsCodec, supportsEncryptedMedia, supportsEssentialProperty, supportsMediaSource, diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index bc89dd2830..0e846ded42 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -52,110 +52,121 @@ function CapabilitiesFilter() { function filterUnsupportedFeatures(manifest) { return new Promise((resolve) => { + const mediaTypesToCheck = [Constants.VIDEO, Constants.AUDIO]; const promises = []; - promises.push(_filterUnsupportedCodecs(Constants.VIDEO, manifest)); - promises.push(_filterUnsupportedCodecs(Constants.AUDIO, manifest)); + // We determine all the configurations we need to check. Each unique configuration should only be checked once. + // This is important especially for large multiperiod MPDs. A redundant configuration check can lead to increased processing time. + mediaTypesToCheck.forEach(mediaType => { + const configurationsToCheck = _getConfigurationsToCheck(manifest, mediaType); + configurationsToCheck.forEach(basicConfiguration => { + promises.push(capabilities.runCodecSupportCheck(basicConfiguration, mediaType)); + }) + }) - Promise.all(promises) + + Promise.allSettled(promises) .then(() => { + mediaTypesToCheck.forEach((mediaType) => { + _filterUnsupportedCodecs(mediaType, manifest) + }) + if (settings.get().streaming.capabilities.filterUnsupportedEssentialProperties) { _filterUnsupportedEssentialProperties(manifest); } + return _applyCustomFilters(manifest); }) - .then(() => _applyCustomFilters(manifest)) - .then(() => resolve()) - .catch(() => { + .then(() => { + resolve(); + }) + .catch((e) => { + logger.error(e); resolve(); }); }); } + function _filterUnsupportedCodecs(type, manifest) { if (!manifest || !manifest.Period || manifest.Period.length === 0) { - return Promise.resolve(); + return } - const promises = []; - manifest.Period.forEach((period) => { - promises.push(_filterUnsupportedAdaptationSetsOfPeriod(period, type)); - }); - - return Promise.all(promises); + manifest.Period + .forEach((period) => { + _filterUnsupportedAdaptationSetsOfPeriod(period, type); + }) } function _filterUnsupportedAdaptationSetsOfPeriod(period, type) { - return new Promise((resolve) => { + if (!period || !period.AdaptationSet || period.AdaptationSet.length === 0) { + return; + } - if (!period || !period.AdaptationSet || period.AdaptationSet.length === 0) { - resolve(); - return; + period.AdaptationSet = period.AdaptationSet.filter((as) => { + if (adapter.getIsTypeOf(as, type)) { + _filterUnsupportedRepresentationsOfAdaptation(as, type); } - - const promises = []; - period.AdaptationSet.forEach((as) => { - if (adapter.getIsTypeOf(as, type)) { - promises.push(_filterUnsupportedRepresentationsOfAdaptation(as, type)); - } - }); - - Promise.all(promises) - .then(() => { - period.AdaptationSet = period.AdaptationSet.filter((as) => { - const supported = as.Representation && as.Representation.length > 0; - if (!supported) { - eventBus.trigger(Events.ADAPTATION_SET_REMOVED_NO_CAPABILITIES, { - adaptationSet: as - }); - logger.warn(`AdaptationSet with ID ${as.id ? as.id : 'unknown'} and codec ${as.codecs ? as.codecs : 'unknown'} has been removed because of no supported Representation`); - } - - return supported; - }); - resolve(); - }) - .catch(() => { - resolve(); + const supported = as.Representation && as.Representation.length > 0; + if (!supported) { + eventBus.trigger(Events.ADAPTATION_SET_REMOVED_NO_CAPABILITIES, { + adaptationSet: as }); - }); + logger.warn(`[CapabilitiesFilter] AdaptationSet with ID ${as.id ? as.id : 'undefined'} and codec ${as.codecs ? as.codecs : 'undefined'} has been removed because of no supported Representation`); + } + return supported; + }) } function _filterUnsupportedRepresentationsOfAdaptation(as, type) { - return new Promise((resolve) => { + if (!as.Representation || as.Representation.length === 0) { + return; + } + const configurations = []; - if (!as.Representation || as.Representation.length === 0) { - resolve(); - return; + as.Representation = as.Representation.filter((rep, i) => { + const codec = adapter.getCodec(as, i, false); + const config = _createConfiguration(type, rep, codec); + + configurations.push(config); + const supported = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, type); + if (!supported) { + logger.debug(`[CapabilitiesFilter] Codec ${configurations[i].codec} not supported. Removing Representation with ID ${rep.id}`); } + return supported + }); + } - const promises = []; - const configurations = []; + function _getConfigurationsToCheck(manifest, type) { + if (!manifest || !manifest.Period || manifest.Period.length === 0) { + return []; + } - as.Representation.forEach((rep, i) => { - const codec = adapter.getCodec(as, i, false); - const config = _createConfiguration(type, rep, codec); + const configurationsSet = new Set(); + const configurations = []; - configurations.push(config); - promises.push(capabilities.supportsCodec(config, type)); - }); - - Promise.all(promises) - .then((supported) => { - as.Representation = as.Representation.filter((_, i) => { - if (!supported[i]) { - logger.debug(`[Stream] Codec ${configurations[i].codec} not supported `); + manifest.Period.forEach((period) => { + period.AdaptationSet.forEach((as) => { + if (adapter.getIsTypeOf(as, type)) { + as.Representation.forEach((rep, i) => { + const codec = adapter.getCodec(as, i, false); + const config = _createConfiguration(type, rep, codec); + const configString = JSON.stringify(config); + + if (!configurationsSet.has(configString)) { + configurationsSet.add(configString); + configurations.push(config); } - return supported[i]; }); - resolve(); - }) - .catch(() => { - resolve(); - }); + } + }); }); + + return configurations; } + function _createConfiguration(type, rep, codec) { let config = null; switch (type) { @@ -172,6 +183,32 @@ function CapabilitiesFilter() { return _addGenericAttributesToConfig(rep, config); } + function _createVideoConfiguration(rep, codec) { + let config = { + codec: codec, + width: rep.width || null, + height: rep.height || null, + framerate: rep.frameRate || null, + bitrate: rep.bandwidth || null, + isSupported: true + } + if (settings.get().streaming.capabilities.filterVideoColorimetryEssentialProperties) { + Object.assign(config, _convertHDRColorimetryToConfig(rep)); + } + let colorimetrySupported = config.isSupported; + + if (settings.get().streaming.capabilities.filterHDRMetadataFormatEssentialProperties) { + Object.assign(config, _convertHDRMetadataFormatToConfig(rep)); + } + let metadataFormatSupported = config.isSupported; + + if (!colorimetrySupported || !metadataFormatSupported) { + config.isSupported = false; // restore this flag as it may got overridden by 2nd Object.assign + } + + return config; + } + function _convertHDRColorimetryToConfig(representation) { let cfg = { colorGamut: null, @@ -235,32 +272,6 @@ function CapabilitiesFilter() { return cfg; } - function _createVideoConfiguration(rep, codec) { - let config = { - codec: codec, - width: rep.width || null, - height: rep.height || null, - framerate: rep.frameRate || null, - bitrate: rep.bandwidth || null, - isSupported: true - } - if (settings.get().streaming.capabilities.filterVideoColorimetryEssentialProperties) { - Object.assign(config, _convertHDRColorimetryToConfig(rep)); - } - let colorimetrySupported = config.isSupported; - - if (settings.get().streaming.capabilities.filterHDRMetadataFormatEssentialProperties) { - Object.assign(config, _convertHDRMetadataFormatToConfig(rep)); - } - let metadataFormatSupported = config.isSupported; - - if (!colorimetrySupported || !metadataFormatSupported) { - config.isSupported = false; // restore this flag as it may got overridden by 2nd Object.assign - } - - return config; - } - function _createAudioConfiguration(rep, codec) { const samplerate = rep.audioSamplingRate || null; const bitrate = rep.bandwidth || null; diff --git a/test/functional/adapter/DashJsAdapter.js b/test/functional/adapter/DashJsAdapter.js index a65c25313b..aafa489055 100644 --- a/test/functional/adapter/DashJsAdapter.js +++ b/test/functional/adapter/DashJsAdapter.js @@ -1,5 +1,7 @@ import Constants from '../src/Constants.js'; import {getRandomNumber} from '../test/common/common.js'; +import {MediaPlayer, Debug} from '../../../dist/esm/dash.all.min.esm.js'; +import '../../../dist/dash.mss.min.js'; class DashJsAdapter { @@ -38,16 +40,16 @@ class DashJsAdapter { } _initLogEvents() { - this.logEvents[dashjs.Debug.LOG_LEVEL_NONE] = []; - this.logEvents[dashjs.Debug.LOG_LEVEL_FATAL] = []; - this.logEvents[dashjs.Debug.LOG_LEVEL_ERROR] = []; - this.logEvents[dashjs.Debug.LOG_LEVEL_WARNING] = []; - this.logEvents[dashjs.Debug.LOG_LEVEL_INFO] = []; - this.logEvents[dashjs.Debug.LOG_LEVEL_DEBUG] = []; + this.logEvents[Debug.LOG_LEVEL_NONE] = []; + this.logEvents[Debug.LOG_LEVEL_FATAL] = []; + this.logEvents[Debug.LOG_LEVEL_ERROR] = []; + this.logEvents[Debug.LOG_LEVEL_WARNING] = []; + this.logEvents[Debug.LOG_LEVEL_INFO] = []; + this.logEvents[Debug.LOG_LEVEL_DEBUG] = []; } _createPlayerInstance() { - this.player = dashjs.MediaPlayer().create(); + this.player = MediaPlayer().create(); this.player.updateSettings({ debug: { logLevel: 3, @@ -100,8 +102,8 @@ class DashJsAdapter { * @private */ _registerInternalEvents() { - this.player.on(dashjs.MediaPlayer.events.FRAGMENT_LOADING_STARTED, this._onFragmentLoadedHandler) - this.player.on(dashjs.MediaPlayer.events.LOG, this._onLogEvent) + this.player.on(MediaPlayer.events.FRAGMENT_LOADING_STARTED, this._onFragmentLoadedHandler) + this.player.on(MediaPlayer.events.LOG, this._onLogEvent) } /** @@ -109,8 +111,8 @@ class DashJsAdapter { * @private */ _unregisterInternalEvents() { - this.player.off(dashjs.MediaPlayer.events.FRAGMENT_LOADING_STARTED, this._onFragmentLoadedHandler) - this.player.off(dashjs.MediaPlayer.events.LOG, this._onLogEvent) + this.player.off(MediaPlayer.events.FRAGMENT_LOADING_STARTED, this._onFragmentLoadedHandler) + this.player.off(MediaPlayer.events.LOG, this._onLogEvent) } /** @@ -332,7 +334,7 @@ class DashJsAdapter { const _onComplete = (res) => { clearTimeout(timeout); timeout = null; - this.player.off(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, _onPlaying); + this.player.off(MediaPlayer.events.PLAYBACK_PLAYING, _onPlaying); resolve(res); } const _onTimeout = () => { @@ -342,7 +344,7 @@ class DashJsAdapter { _onComplete(true); } timeout = setTimeout(_onTimeout, timeoutValue); - this.player.on(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, _onPlaying); + this.player.on(MediaPlayer.events.PLAYBACK_PLAYING, _onPlaying); } }) } @@ -361,7 +363,7 @@ class DashJsAdapter { const _onComplete = (res) => { clearTimeout(timeout); timeout = null; - this.player.off(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); + this.player.off(MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); resolve(res); } const _onTimeout = () => { @@ -377,7 +379,7 @@ class DashJsAdapter { } } timeout = setTimeout(_onTimeout, timeoutValue); - this.player.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); + this.player.on(MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); }) } @@ -388,7 +390,7 @@ class DashJsAdapter { const _onComplete = (res) => { clearTimeout(timeout); timeout = null; - this.player.off(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); + this.player.off(MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); resolve(res); } const _onTimeout = () => { @@ -400,7 +402,7 @@ class DashJsAdapter { } } timeout = setTimeout(_onTimeout, timeoutValue); - this.player.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); + this.player.on(MediaPlayer.events.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated); }) } @@ -412,7 +414,7 @@ class DashJsAdapter { const _onComplete = () => { clearTimeout(timeout); timeout = null; - this.player.off(dashjs.MediaPlayer.events.PERIOD_SWITCH_COMPLETED, _onPeriodSwitched); + this.player.off(MediaPlayer.events.PERIOD_SWITCH_COMPLETED, _onPeriodSwitched); resolve(periodSwitches); } const _onTimeout = () => { @@ -422,7 +424,7 @@ class DashJsAdapter { periodSwitches += 1; } timeout = setTimeout(_onTimeout, timeoutValue); - this.player.on(dashjs.MediaPlayer.events.PERIOD_SWITCH_COMPLETED, _onPeriodSwitched); + this.player.on(MediaPlayer.events.PERIOD_SWITCH_COMPLETED, _onPeriodSwitched); }) } @@ -433,7 +435,7 @@ class DashJsAdapter { const _onComplete = (res) => { clearTimeout(timeout); timeout = null; - this.player.off(dashjs.MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated); + this.player.off(MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated); resolve(res); } const _onTimeout = () => { @@ -459,7 +461,7 @@ class DashJsAdapter { } } timeout = setTimeout(_onTimeout, timeoutValue); - this.player.on(dashjs.MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated); + this.player.on(MediaPlayer.events.BUFFER_LEVEL_UPDATED, _onBufferLevelUpdated); this.player.on('bufferingCompleted', _onBufferingCompleted); }) } @@ -498,7 +500,7 @@ class DashJsAdapter { } timeout = setTimeout(_onTimeout, timeoutValue); this.player.on(schemeIdUri, _onStartEvent, null); /* Default mode is onStart, no need to specify a mode */ - this.player.on(schemeIdUri, _onReceiveEvent, null, { mode: dashjs.MediaPlayer.events.EVENT_MODE_ON_RECEIVE }); + this.player.on(schemeIdUri, _onReceiveEvent, null, { mode: MediaPlayer.events.EVENT_MODE_ON_RECEIVE }); }) } @@ -563,7 +565,7 @@ class DashJsAdapter { } } timeout = setTimeout(_onTimeout, timeoutValue); - this.player.on(dashjs.MediaPlayer.events.FRAGMENT_LOADING_COMPLETED, _onEvent); + this.player.on(MediaPlayer.events.FRAGMENT_LOADING_COMPLETED, _onEvent); }) } diff --git a/test/functional/config/karma.functional.conf.cjs b/test/functional/config/karma.functional.conf.cjs index 5b98223022..e922f4088d 100644 --- a/test/functional/config/karma.functional.conf.cjs +++ b/test/functional/config/karma.functional.conf.cjs @@ -50,8 +50,6 @@ module.exports = function (config) { // https://github.com/webpack-contrib/karma-webpack#alternative-usage files: [ { pattern: 'test/functional/lib/ima3_dai.js', watched: false, nocache: true }, - { pattern: 'dist/dash.all.debug.js', watched: false, nocache: true }, - { pattern: 'dist/dash.mss.min.js', watched: false, nocache: true }, { pattern: 'test/functional/content/**/*.mpd', watched: false, included: false, served: true } ].concat(includedTestfiles), diff --git a/test/unit/mocks/CapabilitiesMock.js b/test/unit/mocks/CapabilitiesMock.js index 390cafe2e6..b37e0975d7 100644 --- a/test/unit/mocks/CapabilitiesMock.js +++ b/test/unit/mocks/CapabilitiesMock.js @@ -28,6 +28,14 @@ class CapabilitiesMock { supportsCodec() { return 'probably'; } + + runCodecSupportCheck() { + return Promise.resolve(); + } + + isCodecSupportedBasedOnTestedConfigurations() { + return true; + } } export default CapabilitiesMock; diff --git a/test/unit/test/streaming/streaming.utils.Capabilities.js b/test/unit/test/streaming/streaming.utils.Capabilities.js index 54880a07ec..0f76e66bba 100644 --- a/test/unit/test/streaming/streaming.utils.Capabilities.js +++ b/test/unit/test/streaming/streaming.utils.Capabilities.js @@ -3,10 +3,18 @@ import Settings from '../../../../src/core/Settings.js'; import DescriptorType from '../../../../src/dash/vo/DescriptorType.js'; import {expect} from 'chai'; +import {UAParser} from 'ua-parser-js'; let settings; let capabilities; +const uaString = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''; +const ua = UAParser(uaString); + +// The Media Capabilities API seems to return wrong values on Linux with Firefox. Deactivate some tests for now +const isLinuxFirefox = ua.browser.name.toLowerCase() === 'firefox' && ua.os.name.toLowerCase().includes('linux'); + + let EssentialPropertyThumbNail = new DescriptorType; EssentialPropertyThumbNail.init({ schemeIdUri: 'http://dashif.org/thumbnail_tile', @@ -258,91 +266,283 @@ describe('Capabilities', function () { }); describe('supportsCodec', function () { - it('should return true for supported codec using MediaSource.isTypeSupported', function (done) { - const config = { - codec: 'video/mp4;codecs="avc1.64001f"', - width: 320, - height: 180 - } - - capabilities.supportsCodec(config, 'video') - .then((result) => { - expect(result).to.be.true - done() - }) - .catch((e) => { - done(e) - }) - }) - it('should filter unsupported codec using MediaSource.isTypeSupported', function (done) { - const config = { - codec: 'video/mp4;codecs="vvvvc1.64001f"', - width: 320, - height: 180 - } - - capabilities.supportsCodec(config, 'video') - .then((result) => { - expect(result).to.be.false - done() - }) - .catch((e) => { - done(e) - }) + describe('MediaSourceExtensions.isTypeSupported', function () { + it('should return true for supported codec using MediaSource.isTypeSupported', function (done) { + const config = { + codec: 'video/mp4;codecs="avc1.64001f"', + width: 320, + height: 180 + } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: false + } + } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.true + done() + }) + .catch((e) => { + done(e) + }) + }) + + it('should filter unsupported codec using MediaSource.isTypeSupported', function (done) { + const config = { + codec: 'video/mp4;codecs="vvvvc1.64001f"', + width: 320, + height: 180 + } + + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: false + } + } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.false + done() + }) + .catch((e) => { + done(e) + }) + }) }) - /* - it('should return true for supported codec using the MediaCapabilitiesAPI', function (done) { - const config = { - codec: 'video/mp4;codecs="avc1.64001f"', - width: 320, - height: 180, - bitrate: 5000, - framerate: 25 - } - settings.update({ - streaming: { - capabilities: { - useMediaCapabilitiesApi: true + describe('MediaCapabilitiesAPI.decodingInfo()', function () { + + before(function () { + if (isLinuxFirefox) { + this.skip(); + } + }); + + it('should return true for supported codec using Media Capabilities API', function (done) { + const config = { + codec: 'video/mp4;codecs="avc1.4D4028"', + width: 320, + height: 180, + bitrate: 5000000, + framerate: '25/1', + isSupported: true + } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.true + done() + }) + .catch((e) => { + done(e) + }) + }) + + it('should return true when a valid codec string is provided but no other parameters. In this case isTypeSupported shall be used instead of the MediaCapabilitiesAPI', function (done) { + const config = { + codec: 'video/mp4;codecs="avc1.4D4028"', } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } + } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.true + done() + }) + .catch((e) => { + done(e) + }) }) - capabilities.supportsCodec(config, 'video') - .then((result) => { - expect(result).to.be.true - done() - }) - .catch((e) => { - done(e) - }) - }) - */ - it('should filter unsupported codec using MediaSource.isTypeSupported', function (done) { - const config = { - codec: 'video/mp4;codecs="vvvvc1.64001f"', - width: 320, - height: 180, - bitrate: 5000, - framerate: 25 - } + it('should return true when a valid codec string is provided but no width. In this case isTypeSupported shall be used instead of the MediaCapabilitiesAPI', function (done) { + const config = { + codec: 'video/mp4;codecs="avc1.4D4028"', + height: 180, + bitrate: 5000000, + framerate: '25/1', + isSupported: true + } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } + } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.true + done() + }) + .catch((e) => { + done(e) + }) + }) - settings.update({ - streaming: { - capabilities: { - useMediaCapabilitiesApi: true + it('should return true when a valid codec string is provided but no height. In this case isTypeSupported shall be used instead of the MediaCapabilitiesAPI', function (done) { + const config = { + codec: 'video/mp4;codecs="avc1.4D4028"', + width: 180, + bitrate: 5000000, + framerate: '25/1', + isSupported: true + } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.true + done() + }) + .catch((e) => { + done(e) + }) + }) + + it('should return true when a valid codec string is provided but no bitrate. In this case isTypeSupported shall be used instead of the MediaCapabilitiesAPI', function (done) { + const config = { + codec: 'video/mp4;codecs="avc1.4D4028"', + height: 180, + width: 320, + framerate: '25/1', + isSupported: true } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } + } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.true + done() + }) + .catch((e) => { + done(e) + }) }) - capabilities.supportsCodec(config, 'video') - .then((result) => { - expect(result).to.be.false - done() - }) - .catch((e) => { - done(e) + + it('should return true when a valid codec string is provided but no framerate. In this case isTypeSupported shall be used instead of the MediaCapabilitiesAPI', function (done) { + const config = { + codec: 'video/mp4;codecs="avc1.4D4028"', + height: 180, + bitrate: 5000000, + width: 320, + isSupported: true + } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } + } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.true + done() + }) + .catch((e) => { + done(e) + }) + }) + + it('should return false when no valid codec string is provided', function (done) { + const config = { + height: 180, + bitrate: 5000000, + framerate: '25/1', + isSupported: true + } + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } + } + }); + + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.false + done() + }) + .catch((e) => { + done(e) + }) + }) + + it('should filter unsupported codec using Media Capabilities API', function (done) { + const config = { + codec: 'video/mp4;codecs="vvvvc1.64001f"', + width: 320, + height: 180, + bitrate: 5000000, + framerate: '25/1', + isSupported: true + } + + settings.update({ + streaming: { + capabilities: { + useMediaCapabilitiesApi: true + } + } }) + capabilities.runCodecSupportCheck(config, 'video') + .then(() => { + const result = capabilities.isCodecSupportedBasedOnTestedConfigurations(config, 'video'); + expect(result).to.be.false + done() + }) + .catch((e) => { + done(e) + }) + }) }) + + + }) }); diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index 7bbaa0e11a..181adc6a9a 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -96,7 +96,7 @@ describe('CapabilitiesFilter', function () { }; prepareCapabilitiesMock({ - name: 'supportsCodec', definition: function () { + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function () { return false; } }); @@ -133,7 +133,7 @@ describe('CapabilitiesFilter', function () { }; prepareCapabilitiesMock({ - name: 'supportsCodec', definition: function (config) { + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { return config.codec === 'audio/mp4;codecs="mp4a.40.2"'; } }); @@ -181,7 +181,7 @@ describe('CapabilitiesFilter', function () { }; prepareCapabilitiesMock({ - name: 'supportsCodec', definition: function (config) { + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { return config.colorGamut === 'srgb' && config.transferFunction === 'srgb'; } }); @@ -221,7 +221,7 @@ describe('CapabilitiesFilter', function () { }; prepareCapabilitiesMock({ - name: 'supportsCodec', definition: function (config) { + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { return config.colorGamut === 'rec2020' && config.transferFunction === 'pq'; } }); @@ -293,7 +293,7 @@ describe('CapabilitiesFilter', function () { }; prepareCapabilitiesMock({ - name: 'supportsCodec', definition: function (config) { + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { return config.isSupported; } }); @@ -376,7 +376,7 @@ describe('CapabilitiesFilter', function () { settings.update({ streaming: { capabilities: { filterHDRMetadataFormatEssentialProperties: true } } }); prepareCapabilitiesMock({ - name: 'supportsCodec', definition: function (config) { + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { return config.colorGamut === 'srgb' && config.hdrMetadataType === 'smpteSt2094-10'; } });