diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js index 1db717ef73..1551950049 100644 --- a/src/dash/models/DashManifestModel.js +++ b/src/dash/models/DashManifestModel.js @@ -676,7 +676,7 @@ function DashManifestModel() { } // now, only return properties present on all Representations - // repr.legth is always >= 2 + // repr.length is always >= 2 return propertiesOfFirstRepresentation.filter(prop => { return repr.slice(1).every(currRep => { return currRep.hasOwnProperty(propertyType) && currRep[propertyType].some(e => { @@ -692,10 +692,18 @@ function DashManifestModel() { } let allProperties = _getPropertiesCommonToAllRepresentations(propertyType, adaptation[DashConstants.REPRESENTATION]); + + // now, only take those Properties from AdaptationSet which we didn't already get from Representations if (adaptation.hasOwnProperty(propertyType) && adaptation[propertyType].length) { - allProperties.push(...adaptation[propertyType]) + adaptation[propertyType].forEach( adaptationProp => { + const alreadyPresent = allProperties.some(d => { + return d.schemeIdUri === adaptationProp.schemeIdUri && d.value === adaptationProp.value + }); + if (!alreadyPresent) { + allProperties.push(adaptationProp); + } + }) } - // we don't check whether there are duplicates on AdaptationSets and Representations return allProperties.map(essentialProperty => { const s = new DescriptorType(); diff --git a/src/dash/parser/maps/MapNode.js b/src/dash/parser/maps/MapNode.js index 83e83391ec..2200234a89 100644 --- a/src/dash/parser/maps/MapNode.js +++ b/src/dash/parser/maps/MapNode.js @@ -34,9 +34,10 @@ import CommonProperty from './CommonProperty.js'; class MapNode { - constructor(name, properties, children) { + constructor(name, properties, exceptions, children) { this._name = name || ''; this._properties = []; + this._exceptions = exceptions || {}; this._children = children || []; if (Array.isArray(properties)) { @@ -57,6 +58,10 @@ class MapNode { get properties() { return this._properties; } + + get exceptions() { + return this._exceptions; + } } export default MapNode; diff --git a/src/dash/parser/maps/RepresentationBaseValuesMap.js b/src/dash/parser/maps/RepresentationBaseValuesMap.js index 889bdec679..cf03fc7c1d 100644 --- a/src/dash/parser/maps/RepresentationBaseValuesMap.js +++ b/src/dash/parser/maps/RepresentationBaseValuesMap.js @@ -33,6 +33,7 @@ */ import MapNode from './MapNode.js'; import DashConstants from '../../constants/DashConstants.js'; +import Constants from '../../../streaming/constants/Constants.js'; class RepresentationBaseValuesMap extends MapNode { constructor() { @@ -42,6 +43,7 @@ class RepresentationBaseValuesMap extends MapNode { DashConstants.CODECS, DashConstants.CODING_DEPENDENCY, DashConstants.CONTENT_PROTECTION, + DashConstants.ESSENTIAL_PROPERTY, DashConstants.FRAMERATE, DashConstants.FRAME_PACKING, DashConstants.HEIGHT, @@ -55,12 +57,24 @@ class RepresentationBaseValuesMap extends MapNode { DashConstants.SEGMENT_PROFILES, DashConstants.SEGMENT_SEQUENCE_PROPERTIES, DashConstants.START_WITH_SAP, + DashConstants.SUPPLEMENTAL_CODECS, + DashConstants.SUPPLEMENTAL_PROPERTY, DashConstants.WIDTH, ]; - super(DashConstants.ADAPTATION_SET, commonProperties, [ - new MapNode(DashConstants.REPRESENTATION, commonProperties, [ - new MapNode(DashConstants.SUB_REPRESENTATION, commonProperties) + // RegEx are supported + const exceptions = { + [DashConstants.SUPPLEMENTAL_PROPERTY]: { + schemeIdUri: [ Constants.URL_QUERY_INFO_SCHEME, Constants.EXT_URL_QUERY_INFO_SCHEME, Constants.ADV_URL_QUERY_INFO_SCHEME, /urn:mpeg:dash:state:/ ] + }, + [DashConstants.ESSENTIAL_PROPERTY]: { + schemeIdUri: [ Constants.URL_QUERY_INFO_SCHEME, Constants.EXT_URL_QUERY_INFO_SCHEME, Constants.ADV_URL_QUERY_INFO_SCHEME, /urn:mpeg:dash:state:/ ] + } + }; + + super(DashConstants.ADAPTATION_SET, commonProperties, exceptions, [ + new MapNode(DashConstants.REPRESENTATION, commonProperties, exceptions, [ + new MapNode(DashConstants.SUB_REPRESENTATION, commonProperties, exceptions) ]) ]); } diff --git a/src/dash/parser/objectiron.js b/src/dash/parser/objectiron.js index cb0d56d722..20c2164959 100644 --- a/src/dash/parser/objectiron.js +++ b/src/dash/parser/objectiron.js @@ -32,37 +32,54 @@ import FactoryMaker from '../../core/FactoryMaker.js'; function ObjectIron(mappers) { - function mergeValues(parentItem, childItem) { - for (let name in parentItem) { - if (!childItem.hasOwnProperty(name)) { - childItem[name] = parentItem[name]; + function _conditionallyMapProperty(exception, parentName, parentIsArray, parentEl, child, mergeFlag) { + let allowMapping = true; + + // check, if element matches an exception + if (exception) { + for (const [key, values] of Object.entries(exception)) { + let attr = parentEl[key]; + if (values.some(v => attr.match(v))) { + allowMapping = false; + } + } + } + + // apply mapping + if (allowMapping) { + if (child[parentName]) { + // property already exists + // check to see if we should merge + if (mergeFlag) { + if (parentIsArray) { + child[parentName].push(parentEl); + } + } + } else { + // just add the property + if (parentIsArray) { + child[parentName] = [parentEl]; + } else { + child[parentName] = parentEl; + } } } + } - function mapProperties(properties, parent, child) { + function mapProperties(properties, exceptions, parent, child) { for (let i = 0, len = properties.length; i < len; ++i) { const property = properties[i]; if (parent[property.name]) { - if (child[property.name]) { - // check to see if we should merge - if (property.merge) { - const parentValue = parent[property.name]; - const childValue = child[property.name]; - - // complex objects; merge properties - if (typeof parentValue === 'object' && typeof childValue === 'object') { - mergeValues(parentValue, childValue); - } - // simple objects; merge them together - else { - child[property.name] = parentValue + childValue; - } - } + const propertyParentElementArray = parent[property.name]; + + if (Array.isArray(propertyParentElementArray)) { + propertyParentElementArray.forEach(propParentEl => { + _conditionallyMapProperty(exceptions[property.name], property.name, true, propParentEl, child, property.merge); + }); } else { - // just add the property - child[property.name] = parent[property.name]; + _conditionallyMapProperty(exceptions[property.name], property.name, false, propertyParentElementArray, child, property.merge); } } } @@ -76,7 +93,7 @@ function ObjectIron(mappers) { if (array) { for (let v = 0, len2 = array.length; v < len2; ++v) { const childNode = array[v]; - mapProperties(item.properties, node, childNode); + mapProperties(item.properties, item.exceptions, node, childNode); mapItem(childItem, childNode); } } diff --git a/src/streaming/constants/Constants.js b/src/streaming/constants/Constants.js index 6922b7bb83..ae6d09debe 100644 --- a/src/streaming/constants/Constants.js +++ b/src/streaming/constants/Constants.js @@ -268,6 +268,7 @@ export default { COLOUR_PRIMARIES_SCHEME_ID_URI: 'urn:mpeg:mpegB:cicp:ColourPrimaries', URL_QUERY_INFO_SCHEME: 'urn:mpeg:dash:urlparam:2014', EXT_URL_QUERY_INFO_SCHEME: 'urn:mpeg:dash:urlparam:2016', + ADV_URL_QUERY_INFO_SCHEME: 'urn:mpeg:dash:urlparam:2025', MATRIX_COEFFICIENTS_SCHEME_ID_URI: 'urn:mpeg:mpegB:cicp:MatrixCoefficients', TRANSFER_CHARACTERISTICS_SCHEME_ID_URI: 'urn:mpeg:mpegB:cicp:TransferCharacteristics', SEGMENT_SEQUENCE_REPRESENTATION_SCHEME_ID_URI: 'urn:mpeg:dash:ssr:2023', diff --git a/test/unit/data/dash/manifest_properties.xml b/test/unit/data/dash/manifest_properties.xml new file mode 100644 index 0000000000..d4e77e6fb0 --- /dev/null +++ b/test/unit/data/dash/manifest_properties.xml @@ -0,0 +1,21 @@ + + ./ + + + + + + + + + + + + + + + + + + + diff --git a/test/unit/test/dash/dash.DashParser.js b/test/unit/test/dash/dash.DashParser.js index 9b26446c3e..934aecf811 100644 --- a/test/unit/test/dash/dash.DashParser.js +++ b/test/unit/test/dash/dash.DashParser.js @@ -56,6 +56,41 @@ describe('DashParser', function () { expect(labelArray[1].lang).to.equal('fr'); }); }); + + describe('DashParser - ObjectIron', async () => { + beforeEach(function () { + dashManifestModel.setConfig({ + errHandler: errorHandlerMock + }); + }); + + let manifest_prop = await FileLoader.loadTextFile('/data/dash/manifest_properties.xml'); + + it('should map AudioChannelConfig even if another instance is present on Representation', async () => { + let parsedMpd = dashParser.parse(manifest_prop); + let audioAdaptationsArray = dashManifestModel.getAdaptationsForType(parsedMpd, 0, 'audio'); + let audiorepresentation = dashManifestModel.getRepresentationFor(0, audioAdaptationsArray[0]); + + let acc = dashManifestModel.getAudioChannelConfigurationForRepresentation(audiorepresentation); + + expect(acc).to.be.instanceOf(Array); + expect(acc.length).to.equal(2); + }); + + it('should map allowed SupplementalProperties from AdaptationSet to Representation', async () => { + let parsedMpd = dashParser.parse(manifest_prop); + let rawAdaptationSet = parsedMpd.Period[0].AdaptationSet[0]; + + expect(rawAdaptationSet.SupplementalProperty).to.be.instanceOf(Array); + expect(rawAdaptationSet.SupplementalProperty.length).to.equal(3); + + let rawRepresentation = rawAdaptationSet.Representation[0]; + + expect(rawRepresentation.SupplementalProperty).to.be.instanceOf(Array); + expect(rawRepresentation.SupplementalProperty.length).to.equal(4); + }); + + }); }) diff --git a/test/unit/test/dash/dash.models.DashManifestModel.js b/test/unit/test/dash/dash.models.DashManifestModel.js index 5e0a628dd7..c9f502105f 100644 --- a/test/unit/test/dash/dash.models.DashManifestModel.js +++ b/test/unit/test/dash/dash.models.DashManifestModel.js @@ -102,104 +102,143 @@ describe('DashManifestModel', function () { expect(language).to.equal(EMPTY_STRING); }); - it('should return an empty array when getViewpointForAdaptation is called and adaptation is undefined', () => { - const viewPoint = dashManifestModel.getViewpointForAdaptation(); - - expect(viewPoint).to.be.instanceOf(Array); - expect(viewPoint).to.be.empty; - }); - - it('should return an empty array when getAudioChannelConfigurationForAdaptation is called and adaptation is undefined', () => { - const AudioChannelConfigurationArray = dashManifestModel.getAudioChannelConfigurationForAdaptation(); - - expect(AudioChannelConfigurationArray).to.be.instanceOf(Array); - expect(AudioChannelConfigurationArray).to.be.empty; - }); - - it('should return an empty array when getAccessibilityForAdaptation is called and adaptation is undefined', () => { - const accessibilityArray = dashManifestModel.getAccessibilityForAdaptation(); - - expect(accessibilityArray).to.be.instanceOf(Array); - expect(accessibilityArray).to.be.empty; - }); - - it('should return an empty array when getRolesForAdaptation is called and adaptation is undefined', () => { - const rolesArray = dashManifestModel.getRolesForAdaptation(); - - expect(rolesArray).to.be.instanceOf(Array); - expect(rolesArray).to.be.empty; - }); - - it('should return DescriptorTypes with sanitized value for Role-value set to Main only for MPEG-Role scheme', () => { - const rolesArray = dashManifestModel.getRolesForAdaptation({ - Role: [ - { schemeIdUri: Constants.DASH_ROLE_SCHEME_ID, value: 'Main' }, - { schemeIdUri: 'my.own.scheme', value: 'Main' }] + describe('handling of descriptors', function () { + it('should return an empty array when getViewpointForAdaptation is called and adaptation is undefined', () => { + const viewPoint = dashManifestModel.getViewpointForAdaptation(); + + expect(viewPoint).to.be.instanceOf(Array); + expect(viewPoint).to.be.empty; }); - - expect(rolesArray).to.be.instanceOf(Array); - expect(rolesArray.length).to.equal(2); - expect(rolesArray[0]).to.be.instanceOf(DescriptorType); - expect(rolesArray[0].value).equals(DashConstants.MAIN); - expect(rolesArray[1].value).equals('Main'); - }); - - it('should return an empty array when getEssentialProperties', () => { - const suppPropArray = dashManifestModel.getEssentialProperties(); - - expect(suppPropArray).to.be.instanceOf(Object); - expect(suppPropArray).to.be.empty; - }); - - it('should return an empty array when getEssentialProperties', () => { - const suppPropArray = dashManifestModel.getEssentialProperties(); - - expect(suppPropArray).to.be.instanceOf(Array); - expect(suppPropArray).to.be.empty; - }); - - it('should return correct array of DescriptorType when getEssentialProperties is called', () => { - const essPropArray = dashManifestModel.getEssentialProperties({ - EssentialProperty: [{ schemeIdUri: 'test.scheme', value: 'testVal' }, { - schemeIdUri: 'test.scheme', - value: 'test2Val' - }] + + it('should return an empty array when getAudioChannelConfigurationForAdaptation is called and adaptation is undefined', () => { + const AudioChannelConfigurationArray = dashManifestModel.getAudioChannelConfigurationForAdaptation(); + + expect(AudioChannelConfigurationArray).to.be.instanceOf(Array); + expect(AudioChannelConfigurationArray).to.be.empty; + }); + + it('should return an empty array when getAccessibilityForAdaptation is called and adaptation is undefined', () => { + const accessibilityArray = dashManifestModel.getAccessibilityForAdaptation(); + + expect(accessibilityArray).to.be.instanceOf(Array); + expect(accessibilityArray).to.be.empty; + }); + + it('should return an empty array when getRolesForAdaptation is called and adaptation is undefined', () => { + const rolesArray = dashManifestModel.getRolesForAdaptation(); + + expect(rolesArray).to.be.instanceOf(Array); + expect(rolesArray).to.be.empty; + }); + + it('should return DescriptorTypes with sanitized value for Role-value set to Main only for MPEG-Role scheme', () => { + const rolesArray = dashManifestModel.getRolesForAdaptation({ + Role: [ + { schemeIdUri: Constants.DASH_ROLE_SCHEME_ID, value: 'Main' }, + { schemeIdUri: 'my.own.scheme', value: 'Main' }] + }); + + expect(rolesArray).to.be.instanceOf(Array); + expect(rolesArray.length).to.equal(2); + expect(rolesArray[0]).to.be.instanceOf(DescriptorType); + expect(rolesArray[0].value).equals(DashConstants.MAIN); + expect(rolesArray[1].value).equals('Main'); }); - - expect(essPropArray).to.be.instanceOf(Array); - expect(essPropArray.length).to.equal(2); - expect(essPropArray[0]).to.be.instanceOf(DescriptorType); - expect(essPropArray[0].schemeIdUri).equals('test.scheme'); - expect(essPropArray[0].value).equals('testVal'); - expect(essPropArray[1].schemeIdUri).equals('test.scheme'); - expect(essPropArray[1].value).equals('test2Val'); - }); - - it('should return an empty array when getEssentialProperties', () => { - const essPropArray = dashManifestModel.getEssentialProperties(); - - expect(essPropArray).to.be.instanceOf(Object); - expect(essPropArray).to.be.empty; }); - it('should return an empty array when getEssentialProperties', () => { - const essPropArray = dashManifestModel.getEssentialProperties(); + describe('handling of Property descriptors', function () { + + it('should return an empty array when getEssentialProperties', () => { + const suppPropArray = dashManifestModel.getEssentialProperties(); + + expect(suppPropArray).to.be.instanceOf(Object); + expect(suppPropArray).to.be.empty; + }); + + it('should return an empty array when getEssentialProperties', () => { + const suppPropArray = dashManifestModel.getEssentialProperties(); + + expect(suppPropArray).to.be.instanceOf(Array); + expect(suppPropArray).to.be.empty; + }); + + it('should return correct array of DescriptorType when getEssentialProperties is called', () => { + const essPropArray = dashManifestModel.getEssentialProperties({ + EssentialProperty: [{ schemeIdUri: 'test.scheme', value: 'testVal' }, { + schemeIdUri: 'test.scheme', + value: 'test2Val' + }] + }); + + expect(essPropArray).to.be.instanceOf(Array); + expect(essPropArray.length).to.equal(2); + expect(essPropArray[0]).to.be.instanceOf(DescriptorType); + expect(essPropArray[0].schemeIdUri).equals('test.scheme'); + expect(essPropArray[0].value).equals('testVal'); + expect(essPropArray[1].schemeIdUri).equals('test.scheme'); + expect(essPropArray[1].value).equals('test2Val'); + }); + + it('should return an empty array when getEssentialProperties', () => { + const essPropArray = dashManifestModel.getEssentialProperties(); + + expect(essPropArray).to.be.instanceOf(Object); + expect(essPropArray).to.be.empty; + }); + + it('should return an empty array when getEssentialProperties', () => { + const essPropArray = dashManifestModel.getEssentialProperties(); + + expect(essPropArray).to.be.instanceOf(Array); + expect(essPropArray).to.be.empty; + }); + + it('should return correct array of DescriptorType when getEssentialProperties is called', () => { + const essPropArray = dashManifestModel.getEssentialProperties({ + EssentialProperty: [{ schemeIdUri: 'test.scheme', value: 'testVal' }] + }); + + expect(essPropArray).to.be.instanceOf(Array); + expect(essPropArray[0]).to.be.instanceOf(DescriptorType); + expect(essPropArray[0].schemeIdUri).equals('test.scheme'); + expect(essPropArray[0].value).equals('testVal'); + }); - expect(essPropArray).to.be.instanceOf(Array); - expect(essPropArray).to.be.empty; - }); + it('should return correct array of DescriptorType when getCombinedEssentialPropertiesForAdaptationSet is called', () => { + const essPropArray = dashManifestModel.getCombinedEssentialPropertiesForAdaptationSet({ + Representation: [ + { + EssentialProperty: [ + {schemeIdUri: 'test.scheme.A', value: '1'}, + {schemeIdUri: 'test.scheme.A', value: '2'}, + {schemeIdUri: 'test.scheme.B', value: 'XYZ'} + ] + }, + { + EssentialProperty: [ + {schemeIdUri: 'test.scheme.A', value: '1'}, + {schemeIdUri: 'test.scheme.A', value: '2'}, + {schemeIdUri: 'test.scheme.B', value: 'ABC'} + ] + } + ], + EssentialProperty: [ + {schemeIdUri: 'test.scheme.A', value: '1'}, + {schemeIdUri: 'test.scheme.B'} + ] + }); - it('should return correct array of DescriptorType when getEssentialProperties is called', () => { - const essPropArray = dashManifestModel.getEssentialProperties({ - EssentialProperty: [{ schemeIdUri: 'test.scheme', value: 'testVal' }] + expect(essPropArray).to.be.instanceOf(Array); + expect(essPropArray.length).to.equal(3); + expect(essPropArray[0].schemeIdUri).equals('test.scheme.A'); + expect(essPropArray[0].value).equals('1'); + expect(essPropArray[1].schemeIdUri).equals('test.scheme.A'); + expect(essPropArray[1].value).equals('2'); + expect(essPropArray[2].schemeIdUri).equals('test.scheme.B'); + expect(essPropArray[2].value).equals(null); }); - - expect(essPropArray).to.be.instanceOf(Array); - expect(essPropArray[0]).to.be.instanceOf(DescriptorType); - expect(essPropArray[0].schemeIdUri).equals('test.scheme'); - expect(essPropArray[0].value).equals('testVal'); }); - + it('should return null when getAdaptationForId is called and id, manifest and periodIndex are undefined', () => { const adaptation = dashManifestModel.getAdaptationForId(undefined, undefined, undefined);