Skip to content

Commit d5fe149

Browse files
authored
Add SMPTE ST 2067-201:2026 IAB support (#417)
Add SMPTE ST 2067-201:2026 IAB support Update the IAB IMF plug-in for the 2026 revision of SMPTE ST 2067-201, covering the changes observable in IMF/MXF metadata. Parsing: - Add IABMaxObjectCount item to IABEssenceDescriptor (Table 3). - Add IABChannelSubDescriptor (Annex E): new class + BO, set-key and item ULs registered in StructuralMetadata, wired into HeaderPartition. Validation (MXF track file, IABTrackFileConstraints): - Adopt MCA Content / MCA Use Class: warn when absent (now recommended), and when present validate values and their combination against SMPTE ST 377-41:2023 Tables 2, 3 and 4, reusing the existing st0377_41 MCAContent / MCAUseClass enums. Stop flagging the deprecated MCA Audio Content/Element Kind as missing. - Enforce the SMPTE ST 377-4:2021 pairing rule: neither MCA Content nor MCA Use Class may be present without the other. - Validate IABChannelSubDescriptor required items and the IABAudioDescriptionText / IABAudioDescription MSB rule (Annex E.2.5). Validation (CPL-only, IMFIABConstraintsChecker): - Apply the same MCA Content / Use Class value, combination, pairing and absence checks so a CPL-only validation (no track files) catches them, matching the MXF path. Tooling: - IMFTrackFileCPLBuilder now emits an IABSequence for IAB track files instead of failing with "only Audio/Image supported". Misc: - Report the constraints specification as SMPTE ST 2067-201:2026. Tests: - New CompleteIMP2026 IMP exercising IABMaxObjectCount and IABChannelSubDescriptor end-to-end. - CPL fixtures + cases for invalid MCA Content/Use Class values, invalid and missing-pair combinations, and valid MCA metadata. - IABChannelSubDescriptor visibility reduced (private mask, package-private helper). - IAB tests assert the specific expected errors rather than error counts, via new TestHelper.assertHasError / assertNoErrorAtOrAbove helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 54b7ef7 commit d5fe149

28 files changed

Lines changed: 3637 additions & 38 deletions

src/main/java/com/netflix/imflibrary/app/IMFTrackFileCPLBuilder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
import javax.annotation.concurrent.Immutable;
5353
import jakarta.xml.bind.JAXBElement;
54+
import javax.xml.namespace.QName;
5455
import javax.xml.parsers.DocumentBuilder;
5556
import javax.xml.parsers.DocumentBuilderFactory;
5657
import javax.xml.parsers.ParserConfigurationException;
@@ -297,8 +298,15 @@ private void buildSegmentList(List<String> uuidList, IMFErrorLogger imfErrorLogg
297298
else if(this.imfTrackFileReader.getEssenceType(imfErrorLogger).equals(HeaderPartition.EssenceTypeEnum.MainAudioEssence)){
298299
element = objectFactory.createMainAudioSequence(sequenceType);
299300
}
301+
else if(this.imfTrackFileReader.getEssenceType(imfErrorLogger).equals(HeaderPartition.EssenceTypeEnum.IABEssence)){
302+
/* The IABSequence element (SMPTE ST 2067-201) lives in a foreign namespace relative to the 2067-3:2013
303+
* CPL produced here. The SequenceList accepts it via xs:any (##other, lax), so it is wrapped directly
304+
* as a JAXBElement carrying the 2067-201 QName rather than through the 2067-3:2013 ObjectFactory. */
305+
element = new JAXBElement<>(new QName("http://www.smpte-ra.org/ns/2067-201/2019", "IABSequence"),
306+
SequenceType.class, sequenceType);
307+
}
300308
else{
301-
String message = String.format("Currently only Audio/Image sequence types are supported");
309+
String message = String.format("Currently only Audio/Image/IAB sequence types are supported");
302310
imfErrorLogger.addError(IMFErrorLogger.IMFErrors.ErrorCodes.IMF_CPL_ERROR, IMFErrorLogger.IMFErrors
303311
.ErrorLevels.FATAL,
304312
message);

src/main/java/com/netflix/imflibrary/st0377/HeaderPartition.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.netflix.imflibrary.st0377.header.*;
3030
import com.netflix.imflibrary.st2067_2.AudioContentKind;
3131
import com.netflix.imflibrary.st2067_2.Composition;
32+
import com.netflix.imflibrary.st2067_201.IABChannelSubDescriptor;
3233
import com.netflix.imflibrary.st2067_201.IABEssenceDescriptor;
3334
import com.netflix.imflibrary.st2067_201.IABSoundfieldLabelSubDescriptor;
3435
import com.netflix.imflibrary.st2067_202.ISXDDataEssenceDescriptor;
@@ -938,6 +939,15 @@ public List<InterchangeObject> getIABSoundFieldLabelSubDescriptors()
938939
return this.getInterchangeObjects(IABSoundfieldLabelSubDescriptor.class);
939940
}
940941

942+
/**
943+
* Gets all the IAB Channel SubDescriptors associated with this HeaderPartition object
944+
* @return list of IAB Channel SubDescriptors contained in this header partition
945+
*/
946+
public List<InterchangeObject> getIABChannelSubDescriptors()
947+
{
948+
return this.getInterchangeObjects(IABChannelSubDescriptor.class);
949+
}
950+
941951
/**
942952
* Gets all the MGA Soundfield Group Label SubDescriptors associated with this HeaderPartition object
943953
* @return list MGA Soundfield Group Label SubDescriptors contained in this header partition

src/main/java/com/netflix/imflibrary/st0377/header/StructuralMetadata.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121

2222
import com.netflix.imflibrary.MXFUID;
23+
import com.netflix.imflibrary.st2067_201.IABChannelSubDescriptor;
2324
import com.netflix.imflibrary.st2067_201.IABEssenceDescriptor;
2425
import com.netflix.imflibrary.st2067_201.IABSoundfieldLabelSubDescriptor;
2526
import com.netflix.imflibrary.st2067_202.ISXDDataEssenceDescriptor;
@@ -419,6 +420,33 @@ private StructuralMetadata()
419420
MXFUID mxfUL = new MXFUID(byteArray);
420421
map.put(mxfUL, "soundfield_group_link_id");
421422
}
423+
//IABEssenceDescriptor (ST 2067-201:2026, Table 3)
424+
{
425+
byte[] byteArray = {0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x0e, 0x04, 0x02, 0x03, 0x0c, 0x05, 0x00, 0x00, 0x00};
426+
MXFUID mxfUL = new MXFUID(byteArray);
427+
map.put(mxfUL, "iab_max_object_count");
428+
}
429+
//IABChannelSubDescriptor (ST 2067-201:2026, Annex E.1)
430+
{
431+
byte[] byteArray = {0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x0e, 0x04, 0x02, 0x03, 0x0c, 0x01, 0x00, 0x00, 0x00};
432+
MXFUID mxfUL = new MXFUID(byteArray);
433+
map.put(mxfUL, "iab_bed_meta_id");
434+
}
435+
{
436+
byte[] byteArray = {0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x0e, 0x04, 0x02, 0x03, 0x0c, 0x02, 0x00, 0x00, 0x00};
437+
MXFUID mxfUL = new MXFUID(byteArray);
438+
map.put(mxfUL, "iab_channel_id");
439+
}
440+
{
441+
byte[] byteArray = {0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x0e, 0x04, 0x02, 0x03, 0x0c, 0x03, 0x00, 0x00, 0x00};
442+
MXFUID mxfUL = new MXFUID(byteArray);
443+
map.put(mxfUL, "iab_audio_description");
444+
}
445+
{
446+
byte[] byteArray = {0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x0e, 0x04, 0x02, 0x03, 0x0c, 0x04, 0x00, 0x00, 0x00};
447+
MXFUID mxfUL = new MXFUID(byteArray);
448+
map.put(mxfUL, "iab_audio_description_text");
449+
}
422450

423451
/*{
424452
byte[] byteArray = {0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x0e, 0x01, 0x03, 0x07, 0x01, 0x04, 0x00, 0x00, 0x00};
@@ -947,6 +975,8 @@ else if (isStructuralMetadata(key) && (key[13] == 0x01))
947975
return MGASoundfieldGroupLabelSubDescriptor.MGASoundfieldGroupLabelSubDescriptorBO.class;
948976
case 0x09:
949977
return SADMAudioMetadataSubDescriptor.SADMAudioMetadataSubDescriptorBO.class;
978+
case 0x15:
979+
return IABChannelSubDescriptor.IABChannelSubDescriptorBO.class;
950980
default :
951981
return Object.class;
952982
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
*
3+
* Copyright 2026 Netflix, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package com.netflix.imflibrary.st2067_201;
20+
21+
import com.netflix.imflibrary.IMFErrorLogger;
22+
import com.netflix.imflibrary.KLVPacket;
23+
import com.netflix.imflibrary.MXFUID;
24+
import com.netflix.imflibrary.annotations.MXFProperty;
25+
import com.netflix.imflibrary.st0377.header.StructuralMetadata;
26+
import com.netflix.imflibrary.st0377.header.SubDescriptor;
27+
import com.netflix.imflibrary.utils.ByteProvider;
28+
29+
import javax.annotation.concurrent.Immutable;
30+
import java.io.IOException;
31+
import java.util.Map;
32+
33+
/**
34+
* Object model corresponding to the IABChannelSubDescriptor structural metadata set defined in
35+
* Annex E of SMPTE ST 2067-201:2026. The sub-descriptor replicates, at the MXF level, the per-channel
36+
* metadata of the BedDefinition structures carried inside the Immersive Audio Bitstream, so that MXF
37+
* parsers can expose it without decoding the bitstream.
38+
*/
39+
@Immutable
40+
public final class IABChannelSubDescriptor extends SubDescriptor {
41+
42+
/** Most significant bit of IABAudioDescription. When set, IABAudioDescriptionText is present (Annex E.2.5). */
43+
private static final short IAB_AUDIO_DESCRIPTION_TEXT_PRESENT_MASK = (short) 0x80;
44+
45+
private final IABChannelSubDescriptorBO iabChannelSubDescriptorBO;
46+
47+
/**
48+
* Constructor for an IABChannelSubDescriptor object
49+
* @param iabChannelSubDescriptorBO the parsed IAB Channel sub-descriptor object
50+
*/
51+
public IABChannelSubDescriptor(IABChannelSubDescriptorBO iabChannelSubDescriptorBO)
52+
{
53+
super();
54+
this.iabChannelSubDescriptorBO = iabChannelSubDescriptorBO;
55+
}
56+
57+
/**
58+
* A method that returns a string representation of an IABChannelSubDescriptor object.
59+
*
60+
* @return string representing the object
61+
*/
62+
public String toString()
63+
{
64+
return this.iabChannelSubDescriptorBO.toString();
65+
}
66+
67+
/**
68+
* Object corresponding to a parsed IABChannelSubDescriptor structural metadata set defined in
69+
* Annex E of SMPTE ST 2067-201:2026.
70+
*/
71+
@Immutable
72+
@SuppressWarnings({"PMD.FinalFieldCouldBeStatic"})
73+
public static final class IABChannelSubDescriptorBO extends SubDescriptor.SubDescriptorBO
74+
{
75+
@MXFProperty(size=4) private final Long iab_bed_meta_id = null; //Uint32, IAB MetaID of the associated BedDefinition
76+
@MXFProperty(size=4) private final Long iab_channel_id = null; //Uint32, channel within a bed and its routing destination
77+
@MXFProperty(size=1) private final Short iab_audio_description = null; //Uint8, top-level audio description
78+
@MXFProperty(size=0, charset="UTF-16") private final String iab_audio_description_text = null; //UTF-16 String, optional
79+
80+
private static final String ERROR_DESCRIPTION_PREFIX = "MXF Header Partition: " + IABChannelSubDescriptor.class.getSimpleName() + " : ";
81+
82+
/**
83+
* Instantiates a new parsed IABChannelSubDescriptor object by virtue of parsing the MXF file bitstream
84+
*
85+
* @param header the parsed header (K and L fields from the KLV packet)
86+
* @param byteProvider the input stream corresponding to the MXF file
87+
* @param localTagToUIDMap mapping from local tag to element UID as provided by the Primer Pack defined in st377-1:2011
88+
* @param imfErrorLogger logger for recording any parsing errors
89+
* @throws IOException - any I/O related error will be exposed through an IOException
90+
*/
91+
public IABChannelSubDescriptorBO(KLVPacket.Header header, ByteProvider byteProvider, Map<Integer, MXFUID> localTagToUIDMap, IMFErrorLogger imfErrorLogger)
92+
throws IOException
93+
{
94+
super(header);
95+
long numBytesToRead = this.header.getVSize();
96+
97+
StructuralMetadata.populate(this, byteProvider, numBytesToRead, localTagToUIDMap);
98+
99+
if (this.instance_uid == null)
100+
{
101+
imfErrorLogger.addError(IMFErrorLogger.IMFErrors.ErrorCodes.IMF_ESSENCE_METADATA_ERROR, IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
102+
IABChannelSubDescriptorBO.ERROR_DESCRIPTION_PREFIX + "instance_uid is null");
103+
}
104+
105+
if (this.iab_bed_meta_id == null)
106+
{
107+
imfErrorLogger.addError(IMFErrorLogger.IMFErrors.ErrorCodes.IMF_ESSENCE_METADATA_ERROR, IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
108+
IABChannelSubDescriptorBO.ERROR_DESCRIPTION_PREFIX + "iab_bed_meta_id is null");
109+
}
110+
111+
if (this.iab_channel_id == null)
112+
{
113+
imfErrorLogger.addError(IMFErrorLogger.IMFErrors.ErrorCodes.IMF_ESSENCE_METADATA_ERROR, IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
114+
IABChannelSubDescriptorBO.ERROR_DESCRIPTION_PREFIX + "iab_channel_id is null");
115+
}
116+
117+
if (this.iab_audio_description == null)
118+
{
119+
imfErrorLogger.addError(IMFErrorLogger.IMFErrors.ErrorCodes.IMF_ESSENCE_METADATA_ERROR, IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
120+
IABChannelSubDescriptorBO.ERROR_DESCRIPTION_PREFIX + "iab_audio_description is null");
121+
}
122+
}
123+
124+
/**
125+
* Accessor for the IABBedMetaID of this IABChannelSubDescriptor
126+
* @return the IAB MetaID of the associated BedDefinition, or null if absent
127+
*/
128+
public Long getIABBedMetaID(){
129+
return this.iab_bed_meta_id;
130+
}
131+
132+
/**
133+
* Accessor for the IABChannelID of this IABChannelSubDescriptor
134+
* @return the channel id within the bed, or null if absent
135+
*/
136+
public Long getIABChannelID(){
137+
return this.iab_channel_id;
138+
}
139+
140+
/**
141+
* Accessor for the IABAudioDescription of this IABChannelSubDescriptor
142+
* @return the top-level audio description, or null if absent
143+
*/
144+
public Short getIABAudioDescription(){
145+
return this.iab_audio_description;
146+
}
147+
148+
/**
149+
* Accessor for the IABAudioDescriptionText of this IABChannelSubDescriptor
150+
* @return the custom audio description text, or null if absent
151+
*/
152+
public String getIABAudioDescriptionText(){
153+
return this.iab_audio_description_text;
154+
}
155+
156+
/**
157+
* Indicates whether IABAudioDescriptionText is expected to be present, i.e. whether the most
158+
* significant bit of IABAudioDescription is set (Annex E.2.5 of SMPTE ST 2067-201:2026).
159+
* @return true if the IABAudioDescriptionText item is required to be present
160+
*/
161+
boolean isAudioDescriptionTextExpected(){
162+
return this.iab_audio_description != null
163+
&& (this.iab_audio_description & IAB_AUDIO_DESCRIPTION_TEXT_PRESENT_MASK) != 0;
164+
}
165+
166+
/**
167+
* A method that returns a string representation of the object.
168+
*
169+
* @return string representing the object
170+
*/
171+
public String toString()
172+
{
173+
StringBuilder sb = new StringBuilder();
174+
sb.append(super.toString());
175+
if (this.iab_bed_meta_id != null)
176+
{
177+
sb.append(String.format("iab_bed_meta_id = %d%n", this.iab_bed_meta_id));
178+
}
179+
if (this.iab_channel_id != null)
180+
{
181+
sb.append(String.format("iab_channel_id = %d%n", this.iab_channel_id));
182+
}
183+
if (this.iab_audio_description != null)
184+
{
185+
sb.append(String.format("iab_audio_description = %d%n", this.iab_audio_description));
186+
}
187+
if (this.iab_audio_description_text != null)
188+
{
189+
sb.append(String.format("iab_audio_description_text = %s%n", this.iab_audio_description_text));
190+
}
191+
return sb.toString();
192+
}
193+
}
194+
}

src/main/java/com/netflix/imflibrary/st2067_201/IABEssenceDescriptor.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.netflix.imflibrary.IMFErrorLogger;
44
import com.netflix.imflibrary.KLVPacket;
55
import com.netflix.imflibrary.MXFUID;
6+
import com.netflix.imflibrary.annotations.MXFProperty;
67
import com.netflix.imflibrary.st0377.header.GenericSoundEssenceDescriptor;
78
import com.netflix.imflibrary.st0377.header.StructuralMetadata;
89
import com.netflix.imflibrary.st0377.header.UL;
@@ -14,7 +15,7 @@
1415
import java.util.Map;
1516

1617
/**
17-
* Object model corresponding to IABEssenceDescriptor structural metadata set defined in st2067-201:201x
18+
* Object model corresponding to IABEssenceDescriptor structural metadata set defined in SMPTE ST 2067-201:2026
1819
*/
1920
@Immutable
2021
public class IABEssenceDescriptor extends GenericSoundEssenceDescriptor {
@@ -34,11 +35,21 @@ public List<Long> getSampleRate() {
3435
}
3536

3637
/**
37-
* Object corresponding to a parsed IABEssenceDescriptor structural metadata set defined in st2067_201
38+
* Accessor for the optional IABMaxObjectCount item (ST 2067-201:2026, Table 3 / 5.8.2).
39+
* @return the maximum count of Object Definitions per IAFrame, or null if the item is absent
40+
*/
41+
public Integer getMaxObjectCount() {
42+
return ((IABEssenceDescriptorBO) this.genericSoundEssenceDescriptorBO).getMaxObjectCount();
43+
}
44+
45+
/**
46+
* Object corresponding to a parsed IABEssenceDescriptor structural metadata set defined in SMPTE ST 2067-201:2026
3847
*/
3948
@Immutable
4049
public static final class IABEssenceDescriptorBO extends GenericSoundEssenceDescriptor.GenericSoundEssenceDescriptorBO {
4150

51+
@MXFProperty(size=2) private final Integer iab_max_object_count = null; //Uint16, optional (ST 2067-201:2026, Table 3)
52+
4253
/**
4354
* Constructor for a IABEssenceDescriptor ByteObject.
4455
*
@@ -64,6 +75,14 @@ public boolean equals(Object other) {
6475
return super.equals(other);
6576
}
6677

78+
/**
79+
* Accessor for the optional IABMaxObjectCount item.
80+
* @return the maximum count of Object Definitions per IAFrame, or null if the item is absent
81+
*/
82+
public Integer getMaxObjectCount() {
83+
return this.iab_max_object_count;
84+
}
85+
6786
/**
6887
* A method that returns a string representation of a IABEssenceDescriptorBO object
6988
*
@@ -73,6 +92,10 @@ public String toString()
7392
{
7493
StringBuilder sb = new StringBuilder();
7594
sb.append(super.toString());
95+
if (this.iab_max_object_count != null)
96+
{
97+
sb.append(String.format("iab_max_object_count = %d%n", this.iab_max_object_count));
98+
}
7699
return sb.toString();
77100
}
78101
}

src/main/java/com/netflix/imflibrary/st2067_201/IABSoundfieldLabelSubDescriptor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import java.util.Map;
1616

1717
/**
18-
* Object model corresponding to MultiChannelAudioLabelSubDescriptor structural metadata set defined in st20167-201:201x
18+
* Object model corresponding to the IAB Soundfield Label SubDescriptor structural metadata set defined in Annex C of SMPTE ST 2067-201:2026
1919
*/
2020
@Immutable
2121
public final class IABSoundfieldLabelSubDescriptor extends MCALabelSubDescriptor {
@@ -53,7 +53,7 @@ public String toString()
5353
}
5454

5555
/**
56-
* Object corresponding to a parsed IABSoundfieldLabelSubDescriptor structural metadata set defined in st20167-201:201x
56+
* Object corresponding to a parsed IABSoundfieldLabelSubDescriptor structural metadata set defined in Annex C of SMPTE ST 2067-201:2026
5757
*/
5858
@Immutable
5959
@SuppressWarnings({"PMD.FinalFieldCouldBeStatic"})

0 commit comments

Comments
 (0)