use std::collections::HashSet;
use crate::diagnostics::{Category, Location, Severity, ValidationIssue};
use crate::validation::{App2E2021, ConstraintsValidator};
use crate::cpl::{CompositionPlaylist, McaTagSymbol};
use crate::validation::iab_codes::{self as iab_codes, IabCode};
pub struct AppIabPlugin2019;
impl ConstraintsValidator for AppIabPlugin2019 {
fn spec_id(&self) -> &str {
"ST 2067-201:2019 (IAB Level 0 Plug-in)"
}
fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
let mut issues = Vec::new();
App2E2021.validate_all(cpl, true, &mut issues);
validate_iab_descriptors(cpl, iab_codes::St2067_201_2019::for_code, true, &mut issues);
validate_iab_sequences(cpl, iab_codes::St2067_201_2019::for_code, &mut issues);
issues
}
}
pub struct AppIabPlugin2021;
impl ConstraintsValidator for AppIabPlugin2021 {
fn spec_id(&self) -> &str {
"ST 2067-201:2021 (IAB Level 0 Plug-in)"
}
fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
let mut issues = Vec::new();
App2E2021.validate_all(cpl, true, &mut issues);
validate_iab_descriptors(cpl, iab_codes::St2067_201_2021::for_code, false, &mut issues);
validate_iab_sequences(cpl, iab_codes::St2067_201_2021::for_code, &mut issues);
issues
}
}
pub const URI_2019: &str = "http://www.smpte-ra.org/ns/2067-201/2019";
pub const URI_2019_SCHEMAS: &str = "http://www.smpte-ra.org/schemas/2067-201/2019";
pub const URI_2021: &str = "http://www.smpte-ra.org/ns/2067-201/2021";
pub const URI_2021_SCHEMAS: &str = "http://www.smpte-ra.org/schemas/2067-201/2021";
fn ul_matches(provided: &str, canonical: &str) -> bool {
fn parse_ul(s: &str) -> Option<[u8; 16]> {
let hex = s.strip_prefix("urn:smpte:ul:")?;
let groups: Vec<&str> = hex.split('.').collect();
if groups.len() != 4 {
return None;
}
let mut bytes = [0u8; 16];
let mut idx = 0;
for group in &groups {
if group.len() != 8 {
return None;
}
for i in (0..8).step_by(2) {
bytes[idx] = u8::from_str_radix(&group[i..i + 2], 16).ok()?;
idx += 1;
}
}
bytes[7] = 0; Some(bytes)
}
match (parse_ul(provided), parse_ul(canonical)) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
fn validate_iab_descriptors(
cpl: &CompositionPlaylist,
code: fn(IabCode) -> &'static str,
check_channel_count: bool,
issues: &mut Vec<ValidationIssue>,
) {
const IAB_CONTAINER_FORMAT: &str =
"urn:smpte:ul:060e2b34.0401010d.0d010301.021d0101";
const IAB_SOUND_COMPRESSION: &str =
"urn:smpte:ul:060e2b34.04010105.0e090604.00000000";
const IAB_MCA_LABEL_DICT_ID: &str =
"urn:smpte:ul:060e2b34.0401010d.03020221.00000000";
let edl = match &cpl.essence_descriptor_list {
Some(edl) => edl,
None => return,
};
for ed in &edl.essence_descriptors {
let iab = match &ed.iab_essence_descriptor {
Some(iab) => iab,
None => continue,
};
let ed_loc = Location::new()
.with_cpl(cpl.id.to_string())
.with_path(format!("EssenceDescriptor/{}", ed.id));
if iab.codec.is_some() {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::CodecForbidden),
format!(
"IABEssenceDescriptor {}: Codec item shall not be present \
(ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
if iab.electrospatial_formulation.is_some() {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::ElectrospatialFormulationForbidden),
format!(
"IABEssenceDescriptor {}: ElectrospatialFormulation shall not be \
present (ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
match iab.quantization_bits {
None => {
issues.push(
ValidationIssue::new(
Severity::Warning,
Category::Audio,
code(IabCode::QuantizationBitsMissing),
format!(
"IABEssenceDescriptor {}: QuantizationBits is missing; \
ST 2067-201 §5.9 requires 24",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
Some(qb) if qb != 24 => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::QuantizationBitsInvalid),
format!(
"IABEssenceDescriptor {}: QuantizationBits {} shall be 24 \
(ST 2067-201 §5.9)",
ed.id, qb,
),
)
.with_location(ed_loc.clone()),
);
}
_ => {}
}
match &iab.container_format {
None => {
issues.push(
ValidationIssue::new(
Severity::Warning,
Category::Audio,
code(IabCode::ContainerFormatMissing),
format!(
"IABEssenceDescriptor {}: ContainerFormat is missing; \
should be {IAB_CONTAINER_FORMAT}",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
Some(cf) if !ul_matches(cf, IAB_CONTAINER_FORMAT) => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::EssenceContainerInvalid),
format!(
"IABEssenceDescriptor {}: ContainerFormat `{cf}` is not the \
required IAB container UL {IAB_CONTAINER_FORMAT} \
(ST 2067-201 §5.3)",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
_ => {}
}
match &iab.audio_sample_rate {
None => {
issues.push(
ValidationIssue::new(
Severity::Warning,
Category::Audio,
code(IabCode::AudioSamplingRateMissing),
format!(
"IABEssenceDescriptor {}: AudioSampleRate is missing; \
ST 2067-201 §5.9 requires 48000/1",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
Some(rate) if rate.numerator != 48000 || rate.denominator != 1 => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::AudioSamplingRateInvalid),
format!(
"IABEssenceDescriptor {}: AudioSampleRate {}/{} is not 48000/1 \
(ST 2067-201 §5.9)",
ed.id, rate.numerator, rate.denominator,
),
)
.with_location(ed_loc.clone()),
);
}
_ => {}
}
match &iab.sound_compression {
None => {
issues.push(
ValidationIssue::new(
Severity::Warning,
Category::Audio,
code(IabCode::SoundCompressionMissing),
format!(
"IABEssenceDescriptor {}: SoundCompression is missing; \
should be {IAB_SOUND_COMPRESSION}",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
Some(sc) if !ul_matches(sc, IAB_SOUND_COMPRESSION) => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::SoundCompressionInvalid),
format!(
"IABEssenceDescriptor {}: SoundCompression `{sc}` is not the \
required IAB UL {IAB_SOUND_COMPRESSION} (ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
_ => {}
}
if check_channel_count {
match iab.channel_count {
Some(cc) if cc != 0 => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::ChannelCountNotZero),
format!(
"IABEssenceDescriptor {}: ChannelCount {cc} shall be the \
distinguished value 0 (ST 2067-201 §5.9, ST 377-1 Annex F.5)",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
}
_ => {}
}
}
let soundfield = iab
.sub_descriptors
.as_ref()
.and_then(|sd| sd.iab_soundfield_label_sub_descriptor.as_ref());
if soundfield.is_none() {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::SubDescriptorMissing),
format!(
"IABEssenceDescriptor {}: IABSoundfieldLabelSubDescriptor shall be \
present (ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(ed_loc.clone()),
);
continue; }
let sf = soundfield.unwrap();
let sub_loc = Location::new()
.with_cpl(cpl.id.to_string())
.with_path(format!(
"EssenceDescriptor/{}/IABSoundfieldLabelSubDescriptor",
ed.id
));
match &sf.mca_tag_symbol {
None => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::MCATagSymbolMissing),
format!(
"IABEssenceDescriptor {}: IABSoundfieldLabelSubDescriptor \
MCATagSymbol shall be \"IAB\" (ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(sub_loc.clone()),
);
}
Some(sym) if *sym != McaTagSymbol::Iab => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::MCATagSymbolInvalid),
format!(
"IABEssenceDescriptor {}: IABSoundfieldLabelSubDescriptor \
MCATagSymbol shall be \"IAB\", got \"{sym:?}\" \
(ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(sub_loc.clone()),
);
}
_ => {}
}
match sf.mca_tag_name.as_deref() {
None => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::MCATagNameMissing),
format!(
"IABEssenceDescriptor {}: IABSoundfieldLabelSubDescriptor \
MCATagName shall be \"IAB\" (ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(sub_loc.clone()),
);
}
Some(name) if name != "IAB" => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::MCATagNameInvalid),
format!(
"IABEssenceDescriptor {}: IABSoundfieldLabelSubDescriptor \
MCATagName shall be \"IAB\", got \"{name}\" \
(ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(sub_loc.clone()),
);
}
_ => {}
}
match sf.mca_label_dictionary_id.as_deref() {
None => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::MCALabelDictionaryIDMissing),
format!(
"IABEssenceDescriptor {}: IABSoundfieldLabelSubDescriptor \
MCALabelDictionaryID is missing; shall be {IAB_MCA_LABEL_DICT_ID} \
(ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(sub_loc.clone()),
);
}
Some(id) if !ul_matches(id, IAB_MCA_LABEL_DICT_ID) => {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::MCALabelDictionaryIDInvalid),
format!(
"IABEssenceDescriptor {}: IABSoundfieldLabelSubDescriptor \
MCALabelDictionaryID `{id}` shall be {IAB_MCA_LABEL_DICT_ID} \
(ST 2067-201 §5.9)",
ed.id,
),
)
.with_location(sub_loc.clone()),
);
}
_ => {}
}
}
}
fn validate_iab_sequences(
cpl: &CompositionPlaylist,
code: fn(IabCode) -> &'static str,
issues: &mut Vec<ValidationIssue>,
) {
let iab_ed_ids: HashSet<String> = cpl
.essence_descriptor_list
.as_ref()
.map(|edl| {
edl.essence_descriptors
.iter()
.filter(|ed| ed.iab_essence_descriptor.is_some())
.map(|ed| ed.id.to_string())
.collect()
})
.unwrap_or_default();
for segment in &cpl.segment_list.segments {
let sl = &segment.sequence_list;
if sl.iab_sequences.is_empty() {
continue;
}
let seg_loc = Location::new()
.with_cpl(cpl.id.to_string())
.with_path(format!("Segment/{}", segment.id));
if sl.main_audio_sequences.is_empty() {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::MainAudioMissing),
format!(
"Segment {}: IABSequence present but no MainAudioSequence found; \
ST 2067-201 §6.2 requires a MainAudioSequence alongside IABSequence",
segment.id,
),
)
.with_location(seg_loc.clone()),
);
}
for iab_seq in &sl.iab_sequences {
let seq_loc = Location::new()
.with_cpl(cpl.id.to_string())
.with_path(format!("Segment/{}/IABSequence/{}", segment.id, iab_seq.id));
if iab_seq.resource_list.resources.is_empty() {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::IABSequenceNoResources),
format!(
"IABSequence {}: shall contain at least one Resource \
(ST 2067-201 §6.2)",
iab_seq.id,
),
)
.with_location(seq_loc.clone()),
);
}
for resource in &iab_seq.resource_list.resources {
if let Some(ref se) = resource.source_encoding {
let se_str = se.to_string();
if !iab_ed_ids.contains(&se_str) {
issues.push(
ValidationIssue::new(
Severity::Error,
Category::Audio,
code(IabCode::IABSequenceSourceEncodingInvalid),
format!(
"IABSequence {}: Resource {} SourceEncoding {se_str} \
does not reference an IABEssenceDescriptor \
(ST 2067-201 §6.2)",
iab_seq.id, resource.id,
),
)
.with_location(seq_loc.clone()),
);
}
}
}
}
}
}