From 6fa577f54554e3ba788218dee9f9439f45445fa5 Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Fri, 19 Sep 2025 17:54:21 +1000 Subject: [PATCH 01/20] Allow the channels to be query by MediaCapabilities. --- src/core/Settings.js | 8 +++- src/streaming/utils/Capabilities.js | 3 ++ src/streaming/utils/CapabilitiesFilter.js | 45 ++++++++++++++++++----- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/core/Settings.js b/src/core/Settings.js index e870dc9200..0284c97407 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, @@ -727,6 +728,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. */ /** @@ -1156,7 +1159,8 @@ function Settings() { ], useMediaCapabilitiesApi: true, filterVideoColorimetryEssentialProperties: false, - filterHDRMetadataFormatEssentialProperties: false + filterHDRMetadataFormatEssentialProperties: false, + filterAudioChannelConfiguration: false }, events: { eventControllerRefreshDelay: 100, diff --git a/src/streaming/utils/Capabilities.js b/src/streaming/utils/Capabilities.js index a7bd5e2c8f..089e691179 100644 --- a/src/streaming/utils/Capabilities.js +++ b/src/streaming/utils/Capabilities.js @@ -428,6 +428,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..cefb7e7cba 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; @@ -366,23 +368,46 @@ function CapabilitiesFilter() { } function _createAudioConfiguration(rep, codec, prslRep) { - var samplerate = rep ? rep.audioSamplingRate || null : null; - var bitrate = rep ? rep.bandwidth || null : null; + let cfg = { + codec, + samplerate: rep ? rep.audioSamplingRate || null : null, + bitrate: rep ? rep.bandwidth || null : null, + isSupported: true, + }; if (rep.tagName === DashConstants.PRESELECTION && prslRep) { - if (!samplerate) { - samplerate = prslRep.audioSamplingRate || null; + if (!cfg.samplerate) { + cfg.samplerate = prslRep.audioSamplingRate || null; } - if (!bitrate) { - bitrate = prslRep.bandwidth || null; + if (!cfg.bitrate) { + cfg.bitrate = prslRep.bandwidth || null; } } + if (settings.get().streaming.capabilities.filterAudioChannelConfiguration) { + Object.assign(cfg, _convertAudioChannelConfigurationToConfig(rep)) + } + } + + return cfg; + } + + function _convertAudioChannelConfigurationToConfig(representation) { + + let channels = null; + + const channelCounts = representation[DashConstants.AUDIO_CHANNEL_CONFIGURATION].map(channelConfig => getNChanFromAudioChannelConfig(channelConfig)) + + // 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 + } + } + }; } From b5d6fe722e1bcd8e75529f5a7934276aaeb1a0be Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Fri, 26 Sep 2025 11:48:55 +1000 Subject: [PATCH 02/20] From feedback to preserve the existing behaviour to ignore the LFE channels., specify whether lfe should be considered in channel count. # Conflicts: # src/streaming/utils/AudioChannelConfiguration.js --- .../utils/AudioChannelConfiguration.js | 72 ++++++++++--------- src/streaming/utils/CapabilitiesFilter.js | 2 +- 2 files changed, 40 insertions(+), 34 deletions(-) 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/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index cefb7e7cba..2945c9133e 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -396,7 +396,7 @@ function CapabilitiesFilter() { let channels = null; - const channelCounts = representation[DashConstants.AUDIO_CHANNEL_CONFIGURATION].map(channelConfig => getNChanFromAudioChannelConfig(channelConfig)) + const channelCounts = representation[DashConstants.AUDIO_CHANNEL_CONFIGURATION].map(channelConfig => getNChanFromAudioChannelConfig(channelConfig, true)) // ensure that all AudioChannelConfiguration elements are the same value, otherwise ignore if (channelCounts.every(e => e == channelCounts[0])) { From ce4e2b56dc68b3f36cc53fa8483ddc4c1b74d0bf Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Tue, 11 Nov 2025 18:01:18 +1100 Subject: [PATCH 03/20] Fixed botched merge --- src/streaming/utils/CapabilitiesFilter.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 2945c9133e..39be3b53a4 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -387,7 +387,6 @@ function CapabilitiesFilter() { if (settings.get().streaming.capabilities.filterAudioChannelConfiguration) { Object.assign(cfg, _convertAudioChannelConfigurationToConfig(rep)) } - } return cfg; } @@ -408,9 +407,6 @@ function CapabilitiesFilter() { } } - }; - } - function _addGenericAttributesToConfig(rep, config) { if (protectionController && rep && rep[DashConstants.CONTENT_PROTECTION] && rep[DashConstants.CONTENT_PROTECTION].length > 0) { config.keySystemsMetadata = protectionController.getSupportedKeySystemMetadataFromContentProtection(rep[DashConstants.CONTENT_PROTECTION]) From f29ba6e97c7b0ce0bff414759579ca4ba7e1f7a9 Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Tue, 11 Nov 2025 18:01:35 +1100 Subject: [PATCH 04/20] Add tests for filtering on number of channels. --- .../streaming.utils.CapabilitiesFilter.js | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index cdd6bc9e24..1906ca63d4 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -266,6 +266,122 @@ describe('CapabilitiesFilter', function () { 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', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', + value: '2' + } + ] + } + ] + }, { + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + 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', + AudioChannelConfiguration: [ + { + schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', + value: '2' + } + ] + } + ] + }, { + mimeType: 'audio/mp4', + Representation: [ + { + mimeType: 'audio/mp4', + codecs: 'mp4a.40.2', + 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); + }); + + }) + }); describe('filter codecs using essentialProperties', function () { From d72102fe9063c7d6ddae0630d6d124034e186c4f Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Tue, 11 Nov 2025 18:05:15 +1100 Subject: [PATCH 05/20] Add debug symbols. --- index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index bc4e795bdc..91086952ae 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, From c290b759deef978ba17d06d68ea0b9916b1be7f6 Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Tue, 11 Nov 2025 18:23:42 +1100 Subject: [PATCH 06/20] add sampling rate to test query --- .../unit/test/streaming/streaming.utils.CapabilitiesFilter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index 1906ca63d4..b3cce5efa9 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -280,6 +280,7 @@ describe('CapabilitiesFilter', function () { { mimeType: 'audio/mp4', codecs: 'mp4a.40.2', + audioSamplingRate: '48000', AudioChannelConfiguration: [ { schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', @@ -294,6 +295,7 @@ describe('CapabilitiesFilter', function () { { mimeType: 'audio/mp4', codecs: 'mp4a.40.2', + audioSamplingRate: '48000', AudioChannelConfiguration: [ { schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', @@ -336,6 +338,7 @@ describe('CapabilitiesFilter', function () { { mimeType: 'audio/mp4', codecs: 'mp4a.40.2', + audioSamplingRate: '48000', AudioChannelConfiguration: [ { schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', @@ -350,6 +353,7 @@ describe('CapabilitiesFilter', function () { { mimeType: 'audio/mp4', codecs: 'mp4a.40.2', + audioSamplingRate: '48000', AudioChannelConfiguration: [ { schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', From 7f762f4c66673fb3b13c24bef0b80222d887baca Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Wed, 12 Nov 2025 13:56:03 +1100 Subject: [PATCH 07/20] Fix filter of Preselections. --- src/streaming/utils/CapabilitiesFilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 39be3b53a4..665e429ed1 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -137,7 +137,7 @@ function CapabilitiesFilter() { if (codec) { let repr = adapter.getCommonRepresentationForPreselection(prsl, period.AdaptationSet); - isPrslCodecSupported = _isCodecSupported(type, repr, codec); + isPrslCodecSupported = _isCodecSupported(type, prsl, codec, repr); } if (!isPrslCodecSupported) { From 6190c6e8a2fdc3483fe70721d45dad94c57b1d1b Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Wed, 12 Nov 2025 13:58:06 +1100 Subject: [PATCH 08/20] Support AudioChannelConfiguration on Preselections. Add tests. --- src/streaming/utils/CapabilitiesFilter.js | 13 +- test/unit/mocks/AdapterMock.js | 21 ++ .../streaming.utils.CapabilitiesFilter.js | 257 +++++++++++++++++- 3 files changed, 283 insertions(+), 8 deletions(-) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 665e429ed1..42af21793f 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -385,21 +385,26 @@ function CapabilitiesFilter() { } if (settings.get().streaming.capabilities.filterAudioChannelConfiguration) { - Object.assign(cfg, _convertAudioChannelConfigurationToConfig(rep)) + Object.assign(cfg, _convertAudioChannelConfigurationToConfig(rep, prslRep)) } return cfg; } - function _convertAudioChannelConfigurationToConfig(representation) { + function _convertAudioChannelConfigurationToConfig(representation, prsl) { + let audioChannelConfigs = representation[DashConstants.AUDIO_CHANNEL_CONFIGURATION] || []; let channels = null; - const channelCounts = representation[DashConstants.AUDIO_CHANNEL_CONFIGURATION].map(channelConfig => getNChanFromAudioChannelConfig(channelConfig, true)) + if (!audioChannelConfigs && prsl) { + audioChannelConfigs = prsl[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] + channels = channelCounts[0]; } return { diff --git a/test/unit/mocks/AdapterMock.js b/test/unit/mocks/AdapterMock.js index b038868f4f..1e9257dc84 100644 --- a/test/unit/mocks/AdapterMock.js +++ b/test/unit/mocks/AdapterMock.js @@ -225,6 +225,27 @@ 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] + return adaptations.find((as) => as.id == id).Representation[0] + } + } export default AdapterMock; diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index b3cce5efa9..a261187f8d 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -327,7 +327,7 @@ describe('CapabilitiesFilter', function () { done(e); }); - }) + }); it('should filter AdaptationSets, using channels', function (done) { const manifest = { @@ -342,7 +342,7 @@ describe('CapabilitiesFilter', function () { AudioChannelConfiguration: [ { schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', - value: '2' + value: 2 } ] } @@ -357,7 +357,7 @@ describe('CapabilitiesFilter', function () { AudioChannelConfiguration: [ { schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011', - value: '6' + value: 6 } ] } @@ -384,7 +384,256 @@ describe('CapabilitiesFilter', function () { 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: '1 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: '1 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) { + return [2,6,8].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(2); + expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); }); From 01021da958c819076df037f707ff91ce155f0b7e Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Wed, 12 Nov 2025 15:31:17 +1100 Subject: [PATCH 09/20] Fix lint failures. --- test/unit/mocks/AdapterMock.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/unit/mocks/AdapterMock.js b/test/unit/mocks/AdapterMock.js index 1e9257dc84..82457c65e6 100644 --- a/test/unit/mocks/AdapterMock.js +++ b/test/unit/mocks/AdapterMock.js @@ -226,19 +226,23 @@ function AdapterMock() { } this.getPreselectionIsTypeOf = function (preselection, adaptations, type) { - if (preselection.mimeType) + if (preselection.mimeType) { return preselection.mimeType.startsWith(type) - if (adaptations[0].mimeType) + } + 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 + "\"" + 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) { From b27fc707ab3cd93e2a19b1201a99d173fef310bc Mon Sep 17 00:00:00 2001 From: "Schreiner, Stephan" Date: Thu, 27 Nov 2025 16:27:28 +0100 Subject: [PATCH 10/20] rename variable for better readability --- src/streaming/utils/CapabilitiesFilter.js | 75 ++++++++++++----------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index d072650fb4..0555ea47eb 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -130,16 +130,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 +187,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 +218,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 +230,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 +241,60 @@ 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 _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)); } let colorimetrySupported = config.isSupported; if (settings.get().streaming.capabilities.filterHDRMetadataFormatEssentialProperties) { - Object.assign(config, _convertHDRMetadataFormatToConfig(rep)); + Object.assign(config, _convertHDRMetadataFormatToConfig(primaryElement)); } let metadataFormatSupported = config.isSupported; @@ -365,16 +368,16 @@ 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) { + var samplerate = primaryElement ? primaryElement.audioSamplingRate || null : null; + var bitrate = primaryElement ? primaryElement.bandwidth || null : null; - if (rep.tagName === DashConstants.PRESELECTION && prslRep) { + if (primaryElement.tagName === DashConstants.PRESELECTION && prslCommonRep) { if (!samplerate) { - samplerate = prslRep.audioSamplingRate || null; + samplerate = prslCommonRep.audioSamplingRate || null; } if (!bitrate) { - bitrate = prslRep.bandwidth || null; + bitrate = prslCommonRep.bandwidth || null; } } From fb58b419399a9fbb5dbc8a240be163f33a17be23 Mon Sep 17 00:00:00 2001 From: "Schreiner, Stephan" Date: Thu, 27 Nov 2025 18:08:44 +0100 Subject: [PATCH 11/20] process HDR properties from CommonRepresentation with Preselections --- src/streaming/utils/CapabilitiesFilter.js | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 0555ea47eb..8d50d615b2 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -263,6 +263,15 @@ function CapabilitiesFilter() { 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(primaryElement, codec, prslCommonRep) { let config = { codec: codec, @@ -290,11 +299,35 @@ function CapabilitiesFilter() { if (settings.get().streaming.capabilities.filterVideoColorimetryEssentialProperties) { 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(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; From 518882d5a0db86f29912637380015f733e79d349 Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Fri, 28 Nov 2025 18:08:39 +1100 Subject: [PATCH 12/20] Fix merge conflicts. --- src/streaming/utils/CapabilitiesFilter.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 934a880e18..237f43fe23 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -404,37 +404,36 @@ function CapabilitiesFilter() { } function _createAudioConfiguration(primaryElement, codec, prslCommonRep) { - function _createAudioConfiguration(rep, codec, prslRep) { let cfg = { codec, - samplerate: rep ? rep.audioSamplingRate || null : null, - bitrate: rep ? rep.bandwidth || null : null, + samplerate: primaryElement ? primaryElement.audioSamplingRate || null : null, + bitrate: primaryElement ? primaryElement.bandwidth || null : null, isSupported: true, }; - if (rep.tagName === DashConstants.PRESELECTION && prslRep) { + if (primaryElement.tagName === DashConstants.PRESELECTION && prslCommonRep) { if (!cfg.samplerate) { - cfg.samplerate = prslRep.audioSamplingRate || null; + cfg.samplerate = prslCommonRep.audioSamplingRate || null; } if (!cfg.bitrate) { - cfg.bitrate = prslRep.bandwidth || null; + cfg.bitrate = prslCommonRep.bandwidth || null; } } if (settings.get().streaming.capabilities.filterAudioChannelConfiguration) { - Object.assign(cfg, _convertAudioChannelConfigurationToConfig(rep, prslRep)) + Object.assign(cfg, _convertAudioChannelConfigurationToConfig(primaryElement, prslCommonRep)) } return cfg; } - function _convertAudioChannelConfigurationToConfig(representation, prsl) { + function _convertAudioChannelConfigurationToConfig(primaryElement, prslCommonRep) { - let audioChannelConfigs = representation[DashConstants.AUDIO_CHANNEL_CONFIGURATION] || []; + let audioChannelConfigs = primaryElement[DashConstants.AUDIO_CHANNEL_CONFIGURATION] || []; let channels = null; - if (!audioChannelConfigs && prsl) { - audioChannelConfigs = prsl[DashConstants.AUDIO_CHANNEL_CONFIGURATION]; + if (!audioChannelConfigs && prslCommonRep) { + audioChannelConfigs = prslCommonRep[DashConstants.AUDIO_CHANNEL_CONFIGURATION]; } const channelCounts = audioChannelConfigs.map(channelConfig => getNChanFromAudioChannelConfig(channelConfig, true)); From f0af8d121b2d011cf433575985ba78adf1df7325 Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Fri, 28 Nov 2025 19:28:37 +1100 Subject: [PATCH 13/20] Fix incorrect test for missing AudioChannelConfiguration on Preselection --- src/streaming/utils/CapabilitiesFilter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 237f43fe23..671084bf00 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -432,8 +432,8 @@ function CapabilitiesFilter() { let audioChannelConfigs = primaryElement[DashConstants.AUDIO_CHANNEL_CONFIGURATION] || []; let channels = null; - if (!audioChannelConfigs && prslCommonRep) { - audioChannelConfigs = prslCommonRep[DashConstants.AUDIO_CHANNEL_CONFIGURATION]; + if (audioChannelConfigs.length == 0 && prslCommonRep) { + audioChannelConfigs = prslCommonRep[DashConstants.AUDIO_CHANNEL_CONFIGURATION] || [] } const channelCounts = audioChannelConfigs.map(channelConfig => getNChanFromAudioChannelConfig(channelConfig, true)); From 3aa0415b9779f252be83c9a7813946ab734b83af Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Fri, 28 Nov 2025 19:29:53 +1100 Subject: [PATCH 14/20] Remove test for component-based preselections --- .../streaming.utils.CapabilitiesFilter.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index a261187f8d..f17f840f35 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -495,7 +495,7 @@ describe('CapabilitiesFilter', function () { }, { id: '11', - preselectionComponents: '1 2', + preselectionComponents: '2', AudioChannelConfiguration: [{ schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', value: 7 @@ -565,7 +565,7 @@ describe('CapabilitiesFilter', function () { }, { id: '11', - preselectionComponents: '1 2', + preselectionComponents: '2', AudioChannelConfiguration: [{ schemeIdUri: 'urn:mpeg:mpegB:cicp:ChannelConfiguration', value: 7 @@ -617,16 +617,19 @@ describe('CapabilitiesFilter', function () { prepareCapabilitiesMock({ name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { - return [2,6,8].includes(config.channels); + // the Each AdapationSet and Preselection is checked + return [8].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(2); - expect(manifest.Period[0].AdaptationSet[0].Representation).to.have.lengthOf(1); + console.log('after manifest', JSON.stringify(manifest)) + expect(manifest.Period[0].Preselection).to.have.lengthOf(2); + expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); + expect(manifest.Period[0].Preselection[1].id).to.be.equal('11'); + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSetp[0].id).to.be.equal(2); done(); }) .catch((e) => { From 82c41ebed7df89eca553e611da5874471b3e925d Mon Sep 17 00:00:00 2001 From: Gregory McGarry Date: Fri, 28 Nov 2025 19:30:46 +1100 Subject: [PATCH 15/20] Add generic preselection test. Add additional test if AudioChannelConfiguration is missing from both Preselection and AdapatationSet. --- .../streaming.utils.CapabilitiesFilter.js | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index f17f840f35..d646b36708 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -267,6 +267,88 @@ describe('CapabilitiesFilter', function () { }); }); + 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) { + console.log('CONFIG', JSON.stringify(config)) + return config.contentType == 'audio/mp4;codecs="iamf.000.000.mp4a.40.2"'; + } + }); + + capabilitiesFilter.filterUnsupportedFeatures(manifest) + .then(() => { + expect(manifest.Period[0].Preselection).to.have.lengthOf(2); + expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); + expect(manifest.Period[0].Preselection[1].id).to.be.equal('11'); + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); + expect(manifest.Period[0].AdaptationSetp[0].id).to.be.equal(1); + done(); + }) + .catch((e) => { + done(e); + }); + + }); + }); describe('filter codecs using codec properties', function () { @@ -386,6 +468,42 @@ describe('CapabilitiesFilter', function () { }); + 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: [{ From 04e8a17804b6dcd133d9118c2e39734e48473daf Mon Sep 17 00:00:00 2001 From: "Schreiner, Stephan" Date: Fri, 28 Nov 2025 12:19:54 +0100 Subject: [PATCH 16/20] proposed fixes to unit test #1 --- .../streaming/streaming.utils.CapabilitiesFilter.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index d646b36708..f62113fa8b 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -329,18 +329,17 @@ describe('CapabilitiesFilter', function () { prepareCapabilitiesMock({ name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { - console.log('CONFIG', JSON.stringify(config)) - return config.contentType == 'audio/mp4;codecs="iamf.000.000.mp4a.40.2"'; + 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(2); + expect(manifest.Period[0].Preselection).to.have.lengthOf(1); expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); - expect(manifest.Period[0].Preselection[1].id).to.be.equal('11'); - expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); - expect(manifest.Period[0].AdaptationSetp[0].id).to.be.equal(1); + expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(2); + expect(manifest.Period[0].AdaptationSet[0].id).to.be.equal('1'); done(); }) .catch((e) => { From 3c4016d5fa2292c0efa558fff4413f9459857e67 Mon Sep 17 00:00:00 2001 From: "Schreiner, Stephan" Date: Fri, 28 Nov 2025 13:11:49 +0100 Subject: [PATCH 17/20] proposed fixes to unit test #2 --- .../streaming/streaming.utils.CapabilitiesFilter.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index f62113fa8b..0dc972a353 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -671,7 +671,7 @@ describe('CapabilitiesFilter', function () { }); - it('should filter AdaptationSets, channels with preselection override', function (done) { + it.only('should filter AdaptationSets, channels with preselection override', function (done) { const manifest = { Period: [{ Preselection: [ @@ -735,18 +735,17 @@ describe('CapabilitiesFilter', function () { prepareCapabilitiesMock({ name: 'isCodecSupportedBasedOnTestedConfigurations', definition: function (config) { // the Each AdapationSet and Preselection is checked - return [8].includes(config.channels) + return [2, 6].includes(config.channels); } }); capabilitiesFilter.filterUnsupportedFeatures(manifest) .then(() => { - console.log('after manifest', JSON.stringify(manifest)) - expect(manifest.Period[0].Preselection).to.have.lengthOf(2); + expect(manifest.Period[0].Preselection).to.have.lengthOf(1); expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); - expect(manifest.Period[0].Preselection[1].id).to.be.equal('11'); - expect(manifest.Period[0].AdaptationSet).to.have.lengthOf(1); - expect(manifest.Period[0].AdaptationSetp[0].id).to.be.equal(2); + 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) => { From 8ab7004cd6931ffe39c8abec9351d41cfac898b6 Mon Sep 17 00:00:00 2001 From: "Schreiner, Stephan" Date: Fri, 28 Nov 2025 13:26:42 +0100 Subject: [PATCH 18/20] proposed new unit test #3, where a supported Preselectionrefers to an unsupported AdaptationSet --- .../streaming.utils.CapabilitiesFilter.js | 89 ++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index 0dc972a353..bbc6385858 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -671,7 +671,7 @@ describe('CapabilitiesFilter', function () { }); - it.only('should filter AdaptationSets, channels with preselection override', function (done) { + it.only('should filter AdaptationSets, channels with preselection override - 1', function (done) { const manifest = { Period: [{ Preselection: [ @@ -754,6 +754,93 @@ describe('CapabilitiesFilter', function () { }); + it.only('should filter AdaptationSets, channels with preselection override - 2', 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(2); + expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); + expect(manifest.Period[0].Preselection[1].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 () { From 1751297a99ecd5166891c9879c869e18d97d0281 Mon Sep 17 00:00:00 2001 From: "Schreiner, Stephan" Date: Fri, 28 Nov 2025 18:12:53 +0100 Subject: [PATCH 19/20] enhancing new unit test #3, Preselection now gets removed if it refers to non-existing AdaptationSet --- src/streaming/utils/CapabilitiesFilter.js | 17 +++++++++++++++++ .../streaming.utils.CapabilitiesFilter.js | 9 ++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js index 671084bf00..65711522c4 100644 --- a/src/streaming/utils/CapabilitiesFilter.js +++ b/src/streaming/utils/CapabilitiesFilter.js @@ -78,6 +78,7 @@ function CapabilitiesFilter() { } _removeMultiRepresentationPreselections(manifest); + _removePreselectionWithNoAdaptationSet(manifest); return _applyCustomFilters(manifest); }) @@ -534,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/test/streaming/streaming.utils.CapabilitiesFilter.js b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js index bbc6385858..bf94b8a83c 100644 --- a/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js +++ b/test/unit/test/streaming/streaming.utils.CapabilitiesFilter.js @@ -671,7 +671,7 @@ describe('CapabilitiesFilter', function () { }); - it.only('should filter AdaptationSets, channels with preselection override - 1', function (done) { + it('should filter AdaptationSets, channels with preselection override', function (done) { const manifest = { Period: [{ Preselection: [ @@ -754,7 +754,7 @@ describe('CapabilitiesFilter', function () { }); - it.only('should filter AdaptationSets, channels with preselection override - 2', function (done) { + it('should filter AdaptationSets and Preselections, channels with preselection override', function (done) { const manifest = { Period: [{ Preselection: [ @@ -828,9 +828,8 @@ describe('CapabilitiesFilter', function () { capabilitiesFilter.filterUnsupportedFeatures(manifest) .then(() => { - expect(manifest.Period[0].Preselection).to.have.lengthOf(2); - expect(manifest.Period[0].Preselection[0].id).to.be.equal('10'); - expect(manifest.Period[0].Preselection[1].id).to.be.equal('11'); + 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(); From 18a43106c9ba547d552d1d1b64296a8288dc20eb Mon Sep 17 00:00:00 2001 From: "Schreiner, Stephan" Date: Fri, 28 Nov 2025 18:17:55 +0100 Subject: [PATCH 20/20] bugfixes as identified by new test case #3 --- src/dash/models/DashManifestModel.js | 2 +- test/unit/mocks/AdapterMock.js | 5 +++-- test/unit/mocks/CapabilitiesMock.js | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) 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/test/unit/mocks/AdapterMock.js b/test/unit/mocks/AdapterMock.js index 82457c65e6..2fa1706382 100644 --- a/test/unit/mocks/AdapterMock.js +++ b/test/unit/mocks/AdapterMock.js @@ -246,8 +246,9 @@ function AdapterMock() { } this.getCommonRepresentationForPreselection = function (preselection, adaptations) { - const id = preselection.preselectionComponents.split(' ')[0] - return adaptations.find((as) => as.id == id).Representation[0] + const id = preselection.preselectionComponents.split(' ')[0]; + const as = adaptations.find((as) => as.id == id); + return (as ? as.Representation[0] : undefined); } } 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;