diff --git a/index.d.ts b/index.d.ts index 5755b7e15a..40bea3f0d8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1682,7 +1682,8 @@ export class MediaPlayerSettingClass { ], useMediaCapabilitiesApi?: boolean, filterHDRMetadataFormatEssentialProperties?: boolean, - filterVideoColorimetryEssentialProperties?: boolean + filterVideoColorimetryEssentialProperties?: boolean, + filterAudioChannelConfiguration?: boolean }, events?: { eventControllerRefreshDelay?: number, diff --git a/src/core/Settings.js b/src/core/Settings.js index 773ffcbb9a..f2c5b1b52c 100644 --- a/src/core/Settings.js +++ b/src/core/Settings.js @@ -84,7 +84,8 @@ import SwitchRequest from '../streaming/rules/SwitchRequest.js'; * ], * useMediaCapabilitiesApi: true, * filterVideoColorimetryEssentialProperties: false, - * filterHDRMetadataFormatEssentialProperties: false + * filterHDRMetadataFormatEssentialProperties: false, + * filterAudioChannelConfiguration: false * }, * events: { * eventControllerRefreshDelay: 100, @@ -732,6 +733,8 @@ import SwitchRequest from '../streaming/rules/SwitchRequest.js'; * If disabled, registered properties per supportedEssentialProperties will be allowed without any further checking (including 'urn:mpeg:mpegB:cicp:MatrixCoefficients'). * @property {boolean} [filterHDRMetadataFormatEssentialProperties=false] * Enable dash.js to query MediaCapabilities API for signalled HDR-MetadataFormat EssentialProperty (per schemeIdUri:'urn:dvb:dash:hdr-dmi'). + * @property {boolean} [filterAudioChannelConfiguration=false] + * Enable dash.js to query MediaCapabilities API for signalled AudioChannelConfiguration. */ /** @@ -1161,7 +1164,8 @@ function Settings() { ], useMediaCapabilitiesApi: true, filterVideoColorimetryEssentialProperties: false, - filterHDRMetadataFormatEssentialProperties: false + filterHDRMetadataFormatEssentialProperties: false, + filterAudioChannelConfiguration: false }, events: { eventControllerRefreshDelay: 100, diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 1db717ef73..9c677c4c43 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -1053,7 +1053,7 @@ function DashManifestModel() { } const mainAS = getMainAdaptationSetForPreselection(preselection, adaptations); - return mainAS.Representation[0]; + return (mainAS ? mainAS.Representation[0] : undefined); } function getPreselectionsForPeriod(voPeriod) { diff --git a/src/streaming/utils/AudioChannelConfiguration.js b/src/streaming/utils/AudioChannelConfiguration.js index 6c6f59b59d..1db9ab0027 100644 --- a/src/streaming/utils/AudioChannelConfiguration.js +++ b/src/streaming/utils/AudioChannelConfiguration.js @@ -34,26 +34,26 @@ // derived from ISO/IEC 23091-3 const _mapping_CICP = { '0': undefined, - '1': 1, - '2': 2, - '3': 3, - '4': 4, - '5': 5, - '6': 5, - '7': 7, - '8': 2, - '9': 3, - '10': 4, - '11': 6, - '12': 7, - '13': 22, - '14': 7, - '15': 10, - '16': 9, - '17': 11, - '18': 13, - '19': 11, - '20': 13 + '1': { channels: 1, lfe: 0 }, + '2': { channels: 2, lfe: 0 }, + '3': { channels: 3, lfe: 0 }, + '4': { channels: 4, lfe: 0 }, + '5': { channels: 5, lfe: 0 }, + '6': { channels: 5, lfe: 1 }, + '7': { channels: 7, lfe: 1 }, + '8': { channels: 2, lfe: 0 }, + '9': { channels: 3, lfe: 0 }, + '10': { channels: 4, lfe: 0 }, + '11': { channels: 6, lfe: 1 }, + '12': { channels: 7, lfe: 1 }, + '13': { channels: 22, lfe: 2 }, + '14': { channels: 7, lfe: 1 }, + '15': { channels: 10, lfe: 2 }, + '16': { channels: 9, lfe: 1 }, + '17': { channels: 11, lfe: 1 }, + '18': { channels: 13, lfe: 1 }, + '19': { channels: 11, lfe: 1 }, + '20': { channels: 13, lfe: 1 }, }; function _countBits(n) { @@ -71,7 +71,7 @@ function _getNChanFromBitMask(value, masks) { return nChan; } -function _getNChanDolby2011(value) { +function _getNChanDolby2011(value, includeLFE) { if ( value.length !== 4 ) { return undefined; } @@ -79,10 +79,13 @@ function _getNChanDolby2011(value) { // see ETSI TS 103190-1, table F.1: // 0b1111100110001000: single channel flags // 0b0000011001110000: channel pair flags - return _getNChanFromBitMask(value, [0b1111100110001000, 0b0000011001110000]); + // 0b0000000000000110: LFE channels + const single_channel_flags = 0b1111100110001000 + (includeLFE ? 0b0000000000000110 : 0); + const channel_pair_flags = 0b0000011001110000; + return _getNChanFromBitMask(value, [single_channel_flags, channel_pair_flags]); } -function _getNChanDolby2015(value) { +function _getNChanDolby2015(value, includeLFE) { if ( value.length !== 6 ) { return undefined; } @@ -95,21 +98,24 @@ function _getNChanDolby2015(value) { // see ETSI TS 103190-2, table A.27 // 0b001100111000000010: single channel flags // 0b110010000110111101: channel pair flags - // 0b000001000001000000: LFE - excluded - return _getNChanFromBitMask(value, [0b001100111000000010, 0b110010000110111101]); + // 0b000001000001000000: LFE channels + const single_channel_flags = 0b001101111000000010 + (includeLFE ? 0b000001000001000000 : 0); + const channel_pair_flags = 0b110010000110111101; + return _getNChanFromBitMask(value, [single_channel_flags, channel_pair_flags]); } -function _getNChanDTSUHD(value) { +function _getNChanDTSUHD(value, includeLFE) { if ( value.length > 8 ) { return undefined; } // see ETSI TS 103491, table B-5 - // LFE to exclude: 0x00010000 + 0x00000020 - return _getNChanFromBitMask(value, [0xFFFEFFDF, 0x00000000]); + // LFE: 0x00010000 + 0x00000020 + const mask = 0xFFFEFFDF + (includeLFE ? 0x00010020 : 0) + return _getNChanFromBitMask(value, [mask, 0x00000000]); } -function getNChanFromAudioChannelConfig(audioChannelConfiguration) { +function getNChanFromAudioChannelConfig(audioChannelConfiguration, includeLFE = false) { let nChan = undefined; if ( !audioChannelConfiguration || !audioChannelConfiguration.schemeIdUri || !audioChannelConfiguration.value ) { @@ -121,15 +127,15 @@ function getNChanFromAudioChannelConfig(audioChannelConfiguration) { if (scheme === 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011' || scheme === 'urn:mpeg:mpegB:cicp:ChannelConfiguration') { // see ISO/IEC 23091-3 - nChan = _mapping_CICP[value]; + nChan = _mapping_CICP[value] && (_mapping_CICP[value].channels + (includeLFE ? _mapping_CICP[value].lfe : 0)); } else if (scheme === 'tag:dolby.com,2014:dash:audio_channel_configuration:2011') { - nChan = _getNChanDolby2011(value); + nChan = _getNChanDolby2011(value, includeLFE); } else if (scheme === 'tag:dolby.com,2015:dash:audio_channel_configuration:2015') { - nChan = _getNChanDolby2015(value); + nChan = _getNChanDolby2015(value, includeLFE); } else if (scheme === 'tag:dts.com,2014:dash:audio_channel_configuration:2012') { nChan = parseInt(value); // per ETSI TS 102 114,table G.2, this includes LFE } else if (scheme === 'tag:dts.com,2018:uhd:audio_channel_configuration') { - nChan = _getNChanDTSUHD(value); + nChan = _getNChanDTSUHD(value, includeLFE); } return nChan; } diff --git a/src/streaming/utils/Capabilities.js b/src/streaming/utils/Capabilities.js index 19c0c349f8..aae181db2b 100644 --- a/src/streaming/utils/Capabilities.js +++ b/src/streaming/utils/Capabilities.js @@ -443,6 +443,9 @@ function Capabilities() { if (inputConfig.samplerate) { configuration.audio.samplerate = inputConfig.samplerate; } + if (inputConfig.channels) { + configuration.audio.channels = inputConfig.channels; + } return configuration } diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index d072650fb4..65711522c4 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -5,6 +5,8 @@ import EventBus from '../../core/EventBus.js'; import Events from '../../core/events/Events.js'; import DashConstants from '../../dash/constants/DashConstants.js'; +import getNChanFromAudioChannelConfig from './AudioChannelConfiguration.js'; + function CapabilitiesFilter() { const context = this.context; @@ -76,6 +78,7 @@ function CapabilitiesFilter() { } _removeMultiRepresentationPreselections(manifest); + _removePreselectionWithNoAdaptationSet(manifest); return _applyCustomFilters(manifest); }) @@ -130,16 +133,16 @@ function CapabilitiesFilter() { period.Preselection = period.Preselection.filter((prsl) => { if (adapter.getPreselectionIsTypeOf(prsl, period.AdaptationSet, type)) { - const codec = adapter.getCodecForPreselection(prsl, period.AdaptationSet); + const prslCodec = adapter.getCodecForPreselection(prsl, period.AdaptationSet); let isPrslCodecSupported = true; - if (codec) { - let repr = adapter.getCommonRepresentationForPreselection(prsl, period.AdaptationSet); + if (prslCodec) { + let commonRepresentation = adapter.getCommonRepresentationForPreselection(prsl, period.AdaptationSet); - isPrslCodecSupported = _isCodecSupported(type, repr, codec); + isPrslCodecSupported = _isCodecSupported(type, prsl, prslCodec, commonRepresentation); } if (!isPrslCodecSupported) { - logger.warn(`[CapabilitiesFilter] Preselection@codecs ${codec} not supported. Removing Preselection with ID ${prsl.id}`); + logger.warn(`[CapabilitiesFilter] Preselection@codecs ${prslCodec} not supported. Removing Preselection with ID ${prsl.id}`); } return isPrslCodecSupported; @@ -187,8 +190,8 @@ function CapabilitiesFilter() { return isSupplementalCodecSupported } - function _isCodecSupported(type, rep, codec, prslRep) { - const config = _createConfiguration(type, rep, codec, prslRep); + function _isCodecSupported(type, primaryElement, codec, prslCommonRepresentation = undefined) { + const config = _createConfiguration(type, primaryElement, codec, prslCommonRepresentation); return capabilities.isCodecSupportedBasedOnTestedConfigurations(config, type); } @@ -218,10 +221,10 @@ function CapabilitiesFilter() { if (period.Preselection && period.Preselection.length) { period.Preselection.forEach((prsl) => { if (adapter.getPreselectionIsTypeOf(prsl, period.AdaptationSet, type)) { - const codec = adapter.getCodecForPreselection(prsl, period.AdaptationSet); - const prslRep = adapter.getCommonRepresentationForPreselection(prsl, period.AdaptationSet); + const prslCodec = adapter.getCodecForPreselection(prsl, period.AdaptationSet); + const prslCommonRepresentation = adapter.getCommonRepresentationForPreselection(prsl, period.AdaptationSet); - _processCodecToCheck(type, prsl, codec, configurationsSet, configurations, prslRep); + _processCodecToCheck(type, prsl, prslCodec, configurationsSet, configurations, prslCommonRepresentation); } }); } @@ -230,8 +233,9 @@ function CapabilitiesFilter() { return configurations; } - function _processCodecToCheck(type, rep, codec, configurationsSet, configurations, prslRep) { - const config = _createConfiguration(type, rep, codec, prslRep); + function _processCodecToCheck(type, element, codec, configurationsSet, configurations, prslCommonRepresentation = undefined) { + /* el is either a Representation or Preselection element */ + const config = _createConfiguration(type, element, codec, prslCommonRepresentation); const configString = JSON.stringify(config); if (!configurationsSet.has(configString)) { @@ -240,58 +244,93 @@ function CapabilitiesFilter() { } } - function _createConfiguration(type, rep, codec, prslRep) { + /* Build the configuration object for capability requests based on primary element (Representation or Preselection) */ + /* In case Preselection elements are present, attributes of this element override their counterparts from the Representation element */ + function _createConfiguration(type, primaryElement, codec, prslCommonRepresentation) { let config = null; switch (type) { case Constants.VIDEO: - config = _createVideoConfiguration(rep, codec, prslRep); + config = _createVideoConfiguration(primaryElement, codec, prslCommonRepresentation); break; case Constants.AUDIO: - config = _createAudioConfiguration(rep, codec, prslRep); + config = _createAudioConfiguration(primaryElement, codec, prslCommonRepresentation); break; default: return config; } - if (prslRep) { - config = _addGenericAttributesToConfig(prslRep, config); + if (prslCommonRepresentation) { + config = _addGenericAttributesToConfig(prslCommonRepresentation, config); } - return _addGenericAttributesToConfig(rep, config); + return _addGenericAttributesToConfig(primaryElement, config); + } + + function _assignMissing(target, enhancement) { + for (const key in enhancement) { + if (Object.prototype.hasOwnProperty.call(enhancement, key) && !(key in target)) { + target[key] = enhancement[key]; + } + } + return target; } - function _createVideoConfiguration(rep, codec, prslRep) { + function _createVideoConfiguration(primaryElement, codec, prslCommonRep) { let config = { codec: codec, - width: rep ? rep.width || null : null, - height: rep ? rep.height || null : null, - framerate: adapter.getFramerate(rep) || null, - bitrate: rep ? rep.bandwidth || null : null, + width: primaryElement ? primaryElement.width || null : null, + height: primaryElement ? primaryElement.height || null : null, + framerate: adapter.getFramerate(primaryElement) || null, + bitrate: primaryElement ? primaryElement.bandwidth || null : null, isSupported: true } - if (rep.tagName === DashConstants.PRESELECTION && prslRep) { + if (primaryElement.tagName === DashConstants.PRESELECTION && prslCommonRep) { if (!config.width) { - config.width = prslRep.width || null; + config.width = prslCommonRep.width || null; } if (!config.height) { - config.height = prslRep.height || null; + config.height = prslCommonRep.height || null; } if (!config.bitrate) { - config.bitrate = prslRep.bandwidth || null; + config.bitrate = prslCommonRep.bandwidth || null; } if (!config.framerate) { - config.framerate = adapter.getFramerate(prslRep) || null; + config.framerate = adapter.getFramerate(prslCommonRep) || null; } } if (settings.get().streaming.capabilities.filterVideoColorimetryEssentialProperties) { - Object.assign(config, _convertHDRColorimetryToConfig(rep)); + Object.assign(config, _convertHDRColorimetryToConfig(primaryElement)); + + if (primaryElement.tagName === DashConstants.PRESELECTION && prslCommonRep) { + let prslCommonRepresentationHDRColorimetryConfig = _convertHDRColorimetryToConfig(prslCommonRep); + + // if either the properties of the Preselection or the CommonRepresentation is not supported, we can't mark the config as supported. + let isCommonRepCfgSupported = prslCommonRepresentationHDRColorimetryConfig.isSupported; + delete prslCommonRepresentationHDRColorimetryConfig.isSupported; + config.isSupported = config.isSupported && isCommonRepCfgSupported; + + // asign only those attributes that are not present in config + _assignMissing(config, prslCommonRepresentationHDRColorimetryConfig); + } } let colorimetrySupported = config.isSupported; if (settings.get().streaming.capabilities.filterHDRMetadataFormatEssentialProperties) { - Object.assign(config, _convertHDRMetadataFormatToConfig(rep)); + Object.assign(config, _convertHDRMetadataFormatToConfig(primaryElement)); + + if (primaryElement.tagName === DashConstants.PRESELECTION && prslCommonRep) { + let prslCommonRepresentationHDRMetadataFormatConfig = _convertHDRMetadataFormatToConfig(prslCommonRep); + + // if either the properties of the Preselection or the CommonRepresentation is not supported, we can't mark the config as supported. + let isCommonRepCfgSupported = prslCommonRepresentationHDRMetadataFormatConfig.isSupported; + delete prslCommonRepresentationHDRMetadataFormatConfig.isSupported; + config.isSupported = config.isSupported && isCommonRepCfgSupported; + + // asign only those attributes that are not present in config + _assignMissing(config, prslCommonRepresentationHDRMetadataFormatConfig); + } } let metadataFormatSupported = config.isSupported; @@ -365,25 +404,49 @@ function CapabilitiesFilter() { return cfg; } - function _createAudioConfiguration(rep, codec, prslRep) { - var samplerate = rep ? rep.audioSamplingRate || null : null; - var bitrate = rep ? rep.bandwidth || null : null; + function _createAudioConfiguration(primaryElement, codec, prslCommonRep) { + let cfg = { + codec, + samplerate: primaryElement ? primaryElement.audioSamplingRate || null : null, + bitrate: primaryElement ? primaryElement.bandwidth || null : null, + isSupported: true, + }; - if (rep.tagName === DashConstants.PRESELECTION && prslRep) { - if (!samplerate) { - samplerate = prslRep.audioSamplingRate || null; + if (primaryElement.tagName === DashConstants.PRESELECTION && prslCommonRep) { + if (!cfg.samplerate) { + cfg.samplerate = prslCommonRep.audioSamplingRate || null; } - if (!bitrate) { - bitrate = prslRep.bandwidth || null; + if (!cfg.bitrate) { + cfg.bitrate = prslCommonRep.bandwidth || null; } } + if (settings.get().streaming.capabilities.filterAudioChannelConfiguration) { + Object.assign(cfg, _convertAudioChannelConfigurationToConfig(primaryElement, prslCommonRep)) + } + + return cfg; + } + + function _convertAudioChannelConfigurationToConfig(primaryElement, prslCommonRep) { + + let audioChannelConfigs = primaryElement[DashConstants.AUDIO_CHANNEL_CONFIGURATION] || []; + let channels = null; + + if (audioChannelConfigs.length == 0 && prslCommonRep) { + audioChannelConfigs = prslCommonRep[DashConstants.AUDIO_CHANNEL_CONFIGURATION] || [] + } + + const channelCounts = audioChannelConfigs.map(channelConfig => getNChanFromAudioChannelConfig(channelConfig, true)); + + // ensure that all AudioChannelConfiguration elements are the same value, otherwise ignore + if (channelCounts.every(e => e == channelCounts[0])) { + channels = channelCounts[0]; + } + return { - codec, - bitrate, - samplerate, - isSupported: true - }; + channels + } } function _addGenericAttributesToConfig(rep, config) { @@ -472,6 +535,22 @@ function CapabilitiesFilter() { }); } + function _removePreselectionWithNoAdaptationSet(manifest) { + if (!manifest || !manifest.Period || manifest.Period.length === 0) { + return; + } + + manifest.Period.forEach((period) => { + if (period.Preselection) { + period.Preselection = period.Preselection.filter((prsl) => { + const prslComponents = String(prsl.preselectionComponents).split(' '); + const adaptationSetIds = period.AdaptationSet.map(as => {return as.id}); + return prslComponents.every(c => {return adaptationSetIds.includes(c)}); + }); + } + }); + } + function _applyCustomFilters(manifest) { if (!manifest || !manifest.Period || manifest.Period.length === 0) { return Promise.resolve(); diff --git a/test/unit/mocks/AdapterMock.js b/test/unit/mocks/AdapterMock.js index b038868f4f..2fa1706382 100644 --- a/test/unit/mocks/AdapterMock.js +++ b/test/unit/mocks/AdapterMock.js @@ -225,6 +225,32 @@ function AdapterMock() { return true } + this.getPreselectionIsTypeOf = function (preselection, adaptations, type) { + if (preselection.mimeType) { + return preselection.mimeType.startsWith(type) + } + if (adaptations[0].mimeType) { + return adaptations[0].mimeType.startsWith(type) + } + return adaptations[0].Representation[0].mimeType.startsWith(type) + } + + this.getCodecForPreselection = function (preselection, adaptations) { + if (preselection.codecs) { + return 'audio/mp4;codecs="' + preselection.codecs + '\"' + } + if (adaptations[0].codec) { + return 'audio/mp4;codecs="' + adaptations.codecs + '"' + } + return 'audio/mp4;codecs="' + adaptations[0].Representation[0].codecs + '"' + } + + this.getCommonRepresentationForPreselection = function (preselection, adaptations) { + const id = preselection.preselectionComponents.split(' ')[0]; + const as = adaptations.find((as) => as.id == id); + return (as ? as.Representation[0] : undefined); + } + } export default AdapterMock; diff --git a/test/unit/mocks/CapabilitiesMock.js b/test/unit/mocks/CapabilitiesMock.js index e7e50e3e8b..2a1ec200cc 100644 --- a/test/unit/mocks/CapabilitiesMock.js +++ b/test/unit/mocks/CapabilitiesMock.js @@ -45,6 +45,10 @@ class CapabilitiesMock { isCodecSupportedBasedOnTestedConfigurations() { return true; } + + supportsEssentialProperty() { + return true; + } } export default CapabilitiesMock; diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index cdd6bc9e24..bf94b8a83c 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -266,6 +266,580 @@ describe('CapabilitiesFilter', function () { done(e); }); }); + + it('should filter AdaptationSets with preselection override', function (done) { + const manifest = { + Period: [{ + Preselection: [ + { + id: '10', + codecs: 'iamf.000.000.mp4a.40.2', + preselectionComponents: '1', + tagName: 'Preselection', + }, + { + id: '11', + codecs: 'iamf.000.001.mp4a.40.2', + preselectionComponents: '2', + tagName: 'Preselection', + }, + ], + AdaptationSet: [ + { + id: '1', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 6 + } + ], + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + }, + { + id: '2', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 6 + } + ], + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + } + ] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + const supportedConfig = ['audio/mp4;codecs="iamf.000.000.mp4a.40.2"' , 'audio/mp4;codecs="mp4a.40.2"']; + return supportedConfig.includes(config.codec); + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].Preselection).to.have.lengthOf(1); + expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(2); + expect(manifest.Period[0].AdaptationSet[0].id).to.be.equal('1'); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + }); + + describe('filter codecs using codec properties', function () { + + it('should filter AdaptationSets, ignoring channels', function (done) { + const manifest = { + Period: [{ + AdaptationSet: [{ + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', + value: '2' + } + ] + } + ] + }, { + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', + value: '6' + } + ] + } + ] + }] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: false } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + return config.channels === undefined; + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(2); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[1].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + it('should filter AdaptationSets, using channels', function (done) { + const manifest = { + Period: [{ + AdaptationSet: [{ + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', + value: 2 + } + ] + } + ] + }, { + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', + value: 6 + } + ] + } + ] + }] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + return config.channels === 2; + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + it('should filter AdaptationSets, using channels but missing AudioChannelConfiguration', function (done) { + const manifest = { + Period: [{ + AdaptationSet: [{ + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + } + ] + }] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + return config.channel === undefined; + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + it('should filter AdaptationSets, using consistent channel configurations', function (done) { + const manifest = { + Period: [{ + AdaptationSet: [{ + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', + value: 2 + }, + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 2 + } + ] + } + ] + }] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + expect(config.channels).to.be.equal(2) + return config.channels === 2; + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + it('should filter AdaptationSets, ignoring inconsistent channel configurations', function (done) { + const manifest = { + Period: [{ + AdaptationSet: [{ + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mhm1.0x0C', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 2 + }, + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 0 + } + ] + } + ] + }] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + expect(config.channels).to.be.equal(undefined) + return config.channels === undefined; + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + it('should filter AdaptationSets, channels with preselections', function (done) { + const manifest = { + Period: [{ + Preselection: [ + { + id: '10', + preselectionComponents: '1', + tagName: 'Preselection', + AudioChannelConfiguration: [{ + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 6 + }], + }, + { + id: '11', + preselectionComponents: '2', + AudioChannelConfiguration: [{ + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 7 + }], + tagName: 'Preselection', + }, + ], + AdaptationSet: [ + { + id: '1', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + }, { + id: '2', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + } + ] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + // only accept the preselection track with 6 channels + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + return config.channels === 6 || config.channels === undefined; + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].Preselection).to.have.lengthOf(1); + expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(2); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + it('should filter AdaptationSets, channels with preselection override', function (done) { + const manifest = { + Period: [{ + Preselection: [ + { + id: '10', + preselectionComponents: '1', + tagName: 'Preselection', + }, + { + id: '11', + preselectionComponents: '2', + AudioChannelConfiguration: [{ + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 7 + }], + tagName: 'Preselection', + }, + ], + AdaptationSet: [ + { + id: '1', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 6 + } + ], + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + }, { + id: '2', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 2 + } + ], + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + } + ] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + // the Each AdapationSet and Preselection is checked + return [2, 6].includes(config.channels); + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].Preselection).to.have.lengthOf(1); + expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(2); + expect(manifest.Period[0].AdaptationSet[0].id).to.be.equal('1'); + expect(manifest.Period[0].AdaptationSet[1].id).to.be.equal('2'); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + + it('should filter AdaptationSets and Preselections, channels with preselection override', function (done) { + const manifest = { + Period: [{ + Preselection: [ + { + id: '10', + preselectionComponents: '1', + AudioChannelConfiguration: [{ + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 6 + }], + tagName: 'Preselection', + }, + { + id: '11', + preselectionComponents: '2', + AudioChannelConfiguration: [{ + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 6 + }], + tagName: 'Preselection', + }, + ], + AdaptationSet: [ + { + id: '1', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 7 + } + ], + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + }, { + id: '2', + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + audioSamplingRate: '48000', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', + value: 2 + } + ], + } + ], + EssentialProperty: [ { schemeIdUri: 'urn:mpeg:dash:preselection:2016' } ], + } + ] + }] + }; + + settings.update({ streaming: { capabilities: { filterAudioChannelConfiguration: true } } }); + + prepareCapabilitiesMock({ + name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { + // the Each AdapationSet and Preselection is checked + return [2, 6].includes(config.channels); + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].Preselection).to.have.lengthOf(1); + expect(manifest.Period[0].Preselection[0].id).to.be.equal('11'); + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSet[0].id).to.be.equal('2'); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + }); describe('filter codecs using essentialProperties', function () {