1pub mod codes;
31pub mod iab;
32pub mod iab_codes;
33pub mod isxd;
34pub mod isxd_codes;
35
36pub use iab::{AppIabPlugin2019, AppIabPlugin2021, AppIabPlugin2026, URI_2019, URI_2019_SCHEMAS};
37pub use isxd::{AppIsxdPlugin2022, URI_2022};
38
39use std::collections::{HashMap, HashSet};
40
41use self::codes::{St2067_21_2020, St2067_21_2023, St2067_21_2025};
42use crate::assetmap::codes::CoreConstraintsCode;
43use crate::cpl::codes::{St2067_3Code, St2067_3_2013, St2067_3_2016};
44use crate::cpl::CompositionPlaylist;
45use crate::cpl::{CodingEquations, ColorPrimaries, CplNamespace, EditRate, TransferCharacteristic};
46use crate::diagnostics::codes::ValidationCode;
47use crate::diagnostics::{Category, Location, Severity, ValidationIssue};
48
49pub trait ConstraintsValidator {
59 fn spec_id(&self) -> &str;
61
62 fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue>;
65}
66
67pub trait ValidatorRegistry {
73 fn resolve_namespace(&self, namespace_uri: &str) -> Option<Box<dyn ConstraintsValidator>>;
75
76 fn resolve_for_cpl(&self, cpl: &CompositionPlaylist) -> Vec<Box<dyn ConstraintsValidator>> {
78 let mut validators: Vec<Box<dyn ConstraintsValidator>> = Vec::new();
79
80 let core_ns = match &cpl.namespace {
81 CplNamespace::Smpte2067_3_2013 => Some("http://www.smpte-ra.org/schemas/2067-2/2013"),
82 CplNamespace::Smpte2067_3_2016 => Some("http://www.smpte-ra.org/schemas/2067-2/2016"),
83 _ => None,
84 };
85 if let Some(ns) = core_ns {
86 if let Some(v) = self.resolve_namespace(ns) {
87 validators.push(v);
88 }
89 }
90
91 if let Some(ref ext) = cpl.extension_properties {
92 if let Some(ref app_id) = ext.application_identification {
93 for uri in app_id.split_whitespace() {
94 if let Some(v) = self.resolve_namespace(uri) {
95 validators.push(v);
96 }
97 }
98 }
99 }
100
101 validators
102 }
103}
104
105pub struct BuiltinValidatorRegistry;
107
108impl ValidatorRegistry for BuiltinValidatorRegistry {
109 fn resolve_namespace(&self, namespace_uri: &str) -> Option<Box<dyn ConstraintsValidator>> {
110 match namespace_uri {
111 "http://www.smpte-ra.org/schemas/2067-2/2013" => Some(Box::new(CoreConstraints2013)),
112 "http://www.smpte-ra.org/schemas/2067-2/2016" => Some(Box::new(CoreConstraints2016)),
113 "http://www.smpte-ra.org/ns/2067-2/2020" => Some(Box::new(CoreConstraints2020)),
114 "http://www.smpte-ra.org/ns/2067-21/2020" => Some(Box::new(App2E2020)),
115 "http://www.smpte-ra.org/schemas/2067-21/2014"
116 | "http://www.smpte-ra.org/schemas/2067-21/2015"
117 | "http://www.smpte-ra.org/schemas/2067-21/2016"
118 | "http://www.smpte-ra.org/ns/2067-21/2021"
119 | "http://www.smpte-ra.org/ns/2067-21/2023" => Some(Box::new(App2E2021)),
120 _ => None,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum CoreSpecTarget {
131 St2067_2_2013,
132 St2067_2_2016,
133 St2067_2_2020,
134}
135
136impl CoreSpecTarget {
137 pub fn namespace_uri(&self) -> &'static str {
138 match self {
139 Self::St2067_2_2013 => "http://www.smpte-ra.org/schemas/2067-2/2013",
140 Self::St2067_2_2016 => "http://www.smpte-ra.org/schemas/2067-2/2016",
141 Self::St2067_2_2020 => "http://www.smpte-ra.org/ns/2067-2/2020",
142 }
143 }
144}
145
146impl std::str::FromStr for CoreSpecTarget {
147 type Err = String;
148
149 fn from_str(s: &str) -> Result<Self, Self::Err> {
150 match s {
151 "v2013" => Ok(Self::St2067_2_2013),
152 "v2016" => Ok(Self::St2067_2_2016),
153 "v2020" => Ok(Self::St2067_2_2020),
154 other => Err(format!(
155 "Unsupported coreSpec '{}'. Use auto|v2013|v2016|v2020",
156 other
157 )),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum AppSpecTarget {
164 St2067_21_2020,
165 St2067_21_2021,
166 St2067_21_2023,
167}
168
169impl AppSpecTarget {
170 pub fn application_identification_uri(&self) -> &'static str {
171 match self {
172 Self::St2067_21_2020 => "http://www.smpte-ra.org/ns/2067-21/2020",
173 Self::St2067_21_2021 => "http://www.smpte-ra.org/ns/2067-21/2021",
174 Self::St2067_21_2023 => "http://www.smpte-ra.org/ns/2067-21/2023",
175 }
176 }
177}
178
179impl std::str::FromStr for AppSpecTarget {
180 type Err = String;
181
182 fn from_str(s: &str) -> Result<Self, Self::Err> {
183 match s {
184 "v2020" => Ok(Self::St2067_21_2020),
185 "v2021" => Ok(Self::St2067_21_2021),
186 "v2023" => Ok(Self::St2067_21_2023),
187 other => Err(format!(
188 "Unsupported app2eSpec '{}'. Use auto|none|v2020|v2021|v2023",
189 other
190 )),
191 }
192 }
193}
194
195pub fn parse_core_spec_target(s: &str) -> Result<Option<CoreSpecTarget>, String> {
197 match s {
198 "auto" => Ok(None),
199 other => Ok(Some(other.parse()?)),
200 }
201}
202
203pub fn parse_app_spec_targets(s: &str) -> Result<Option<Vec<AppSpecTarget>>, String> {
206 match s {
207 "auto" => Ok(None),
208 "none" => Ok(Some(vec![])),
209 other => Ok(Some(vec![other.parse()?])),
210 }
211}
212
213#[derive(Debug, Clone, Default)]
214pub struct ValidatorSelection {
215 pub core_spec: Option<CoreSpecTarget>,
217
218 pub app_specs: Option<Vec<AppSpecTarget>>,
222
223 pub core_namespace_uri: Option<String>,
229
230 pub application_identification_uris: Option<Vec<String>>,
235}
236
237pub struct ConfigurableValidatorRegistry {
240 selection: ValidatorSelection,
241}
242
243impl ConfigurableValidatorRegistry {
244 pub fn new(selection: ValidatorSelection) -> Self {
245 Self { selection }
246 }
247}
248
249impl ValidatorRegistry for ConfigurableValidatorRegistry {
250 fn resolve_namespace(&self, namespace_uri: &str) -> Option<Box<dyn ConstraintsValidator>> {
251 BuiltinValidatorRegistry.resolve_namespace(namespace_uri)
252 }
253
254 fn resolve_for_cpl(&self, cpl: &CompositionPlaylist) -> Vec<Box<dyn ConstraintsValidator>> {
255 let mut validators: Vec<Box<dyn ConstraintsValidator>> = Vec::new();
256
257 let core_ns = if let Some(core_spec) = self.selection.core_spec {
258 Some(core_spec.namespace_uri())
259 } else if let Some(uri) = self.selection.core_namespace_uri.as_deref() {
260 Some(uri)
261 } else {
262 match &cpl.namespace {
263 CplNamespace::Smpte2067_3_2013 => {
264 Some("http://www.smpte-ra.org/schemas/2067-2/2013")
265 }
266 CplNamespace::Smpte2067_3_2016 => {
267 Some("http://www.smpte-ra.org/schemas/2067-2/2016")
268 }
269 _ => None,
270 }
271 };
272
273 if let Some(ns) = core_ns {
274 if let Some(v) = self.resolve_namespace(ns) {
275 validators.push(v);
276 }
277 }
278
279 if let Some(ref app_specs) = self.selection.app_specs {
280 for spec in app_specs {
281 if let Some(v) = self.resolve_namespace(spec.application_identification_uri()) {
282 validators.push(v);
283 }
284 }
285 } else if let Some(ref app_uris) = self.selection.application_identification_uris {
286 for uri in app_uris {
287 if let Some(v) = self.resolve_namespace(uri) {
288 validators.push(v);
289 }
290 }
291 } else if let Some(ref ext) = cpl.extension_properties {
292 if let Some(ref app_id) = ext.application_identification {
293 for uri in app_id.split_whitespace() {
294 if let Some(v) = self.resolve_namespace(uri) {
295 validators.push(v);
296 }
297 }
298 }
299 }
300
301 validators
302 }
303}
304
305pub fn get_validator(namespace_uri: &str) -> Option<Box<dyn ConstraintsValidator>> {
309 BuiltinValidatorRegistry.resolve_namespace(namespace_uri)
310}
311
312pub fn get_validators_for_cpl(cpl: &CompositionPlaylist) -> Vec<Box<dyn ConstraintsValidator>> {
318 BuiltinValidatorRegistry.resolve_for_cpl(cpl)
319}
320
321pub fn validate_cpl(cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
325 validate_cpl_with_builtin_registry(cpl)
326}
327
328pub fn validate_cpl_with_builtin_registry(cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
330 let validators = get_validators_for_cpl(cpl);
331 let mut all_issues = Vec::new();
332 for v in &validators {
333 all_issues.extend(v.validate_cpl(cpl));
334 }
335 all_issues
336}
337
338pub fn validate_cpl_with_registry(
343 cpl: &CompositionPlaylist,
344 registry: &dyn ValidatorRegistry,
345) -> Vec<ValidationIssue> {
346 let validators = registry.resolve_for_cpl(cpl);
347 let mut all_issues = Vec::new();
348 for v in &validators {
349 all_issues.extend(v.validate_cpl(cpl));
350 }
351 all_issues
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
364pub enum ColorSystem {
365 Color1,
367 Color2,
369 Color3,
371 Color4,
373 Color5,
375 Color6,
377 Color7,
379 Color8,
381}
382
383impl ColorSystem {
384 pub fn from_components(
389 primaries: &ColorPrimaries,
390 transfer: &TransferCharacteristic,
391 coding_eq: Option<&CodingEquations>,
392 ) -> Option<Self> {
393 match (primaries, transfer, coding_eq) {
397 (
399 ColorPrimaries::Bt601_625,
400 TransferCharacteristic::Bt709,
401 Some(CodingEquations::Bt601),
402 ) => Some(Self::Color1),
403 (
404 ColorPrimaries::Bt601_525,
405 TransferCharacteristic::Bt709,
406 Some(CodingEquations::Bt601),
407 ) => Some(Self::Color2),
408 (
409 ColorPrimaries::Bt709,
410 TransferCharacteristic::Bt709,
411 Some(CodingEquations::Bt709),
412 ) => Some(Self::Color3),
413 (
414 ColorPrimaries::Bt709,
415 TransferCharacteristic::XvYcc709,
416 Some(CodingEquations::Bt709),
417 ) => Some(Self::Color4),
418 (
419 ColorPrimaries::Bt2020,
420 TransferCharacteristic::Bt2020,
421 Some(CodingEquations::Bt2020Ncl),
422 ) => Some(Self::Color5),
423 (
424 ColorPrimaries::Bt2020,
425 TransferCharacteristic::PqSt2084,
426 Some(CodingEquations::Bt2020Ncl),
427 ) => Some(Self::Color7),
428 (
429 ColorPrimaries::Bt2020,
430 TransferCharacteristic::Hlg,
431 Some(CodingEquations::Bt2020Ncl),
432 ) => Some(Self::Color8),
433 (ColorPrimaries::Bt601_625, TransferCharacteristic::Bt709, None) => Some(Self::Color1),
435 (ColorPrimaries::Bt601_525, TransferCharacteristic::Bt709, None) => Some(Self::Color2),
436 (ColorPrimaries::Bt709, TransferCharacteristic::Bt709, None) => Some(Self::Color3),
437 (ColorPrimaries::Bt709, TransferCharacteristic::XvYcc709, None) => Some(Self::Color4),
438 (ColorPrimaries::Bt2020, TransferCharacteristic::Bt2020, None) => Some(Self::Color5),
439 (ColorPrimaries::P3D65, TransferCharacteristic::PqSt2084, None) => Some(Self::Color6),
440 (ColorPrimaries::Bt2020, TransferCharacteristic::PqSt2084, None) => Some(Self::Color7),
441 (ColorPrimaries::Bt2020, TransferCharacteristic::Hlg, None) => Some(Self::Color8),
442 _ => None,
443 }
444 }
445
446 pub fn is_hdr(&self) -> bool {
448 matches!(self, Self::Color6 | Self::Color7 | Self::Color8)
449 }
450
451 pub fn requires_hdr_metadata(&self) -> bool {
454 matches!(self, Self::Color6 | Self::Color7)
455 }
456}
457
458impl std::fmt::Display for ColorSystem {
459 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460 match self {
461 Self::Color1 => write!(f, "COLOR.1 (BT.601-625 / BT.709 / BT.601)"),
462 Self::Color2 => write!(f, "COLOR.2 (BT.601-525 / BT.709 / BT.601)"),
463 Self::Color3 => write!(f, "COLOR.3 (BT.709 / BT.709 / BT.709)"),
464 Self::Color4 => write!(f, "COLOR.4 (BT.709 / xvYCC 709 / BT.709)"),
465 Self::Color5 => write!(f, "COLOR.5 (BT.2020 / BT.2020 / BT.2020 NCL)"),
466 Self::Color6 => write!(f, "COLOR.6 (P3 D65 / PQ / RGB)"),
467 Self::Color7 => write!(f, "COLOR.7 (BT.2020 / PQ / BT.2020 NCL)"),
468 Self::Color8 => write!(f, "COLOR.8 (BT.2020 / HLG / BT.2020 NCL)"),
469 }
470 }
471}
472
473fn is_valid_any_uri(s: &str) -> bool {
489 !s.chars().any(|c| c.is_ascii_whitespace())
490}
491
492fn validate_core_structure(
498 cpl: &CompositionPlaylist,
499 code: fn(CoreConstraintsCode) -> &'static str,
500 issues: &mut Vec<ValidationIssue>,
501) {
502 issues.extend(crate::xsd::validate_parsed_cpl(cpl));
508
509 let loc = Location::new().with_cpl(cpl.id);
510
511 if let (Some(ref tc), Some(ref er)) = (&cpl.composition_timecode, &cpl.edit_rate) {
524 if let Some(tc_rate) = tc.timecode_rate {
525 let edit_fps = if er.denominator > 0 {
527 (er.numerator as f64 / er.denominator as f64).round() as u32
528 } else {
529 0
530 };
531 if tc_rate != edit_fps {
532 issues.push(
533 ValidationIssue::new(
534 Severity::Warning,
535 Category::Timing,
536 code(CoreConstraintsCode::CompositionTimecodeRateMismatch),
537 format!(
538 "CompositionTimecode.TimecodeRate {} does not match CPL EditRate {}/{} (≈{} fps)",
539 tc_rate, er.numerator, er.denominator, edit_fps,
540 ),
541 )
542 .with_location(loc.clone()),
543 );
544 }
545 }
546 }
547
548 issues.extend(crate::cpl::validate_cpl_constraints(cpl));
553
554 validate_uuid_uniqueness(cpl, code, issues);
556
557 validate_resource_constraints(cpl, code, issues);
559
560 validate_virtual_track_continuity(cpl, code, issues);
562
563 validate_virtual_track_edit_rates(cpl, code, issues);
565
566 validate_audio_mca_labels(cpl, code, issues);
568
569 validate_timed_text_extended(cpl, code, issues);
571
572 validate_segment_track_durations(cpl, issues);
574
575 validate_digital_signature_notice(cpl, code, issues);
577
578 validate_dangling_essence_descriptors(cpl, code, issues);
581}
582
583fn validate_uuid_uniqueness(
591 cpl: &CompositionPlaylist,
592 code: fn(CoreConstraintsCode) -> &'static str,
593 issues: &mut Vec<ValidationIssue>,
594) {
595 let cpl_loc = Location::new().with_cpl(cpl.id);
596
597 let mut segment_ids = HashSet::new();
599 for (i, segment) in cpl.segment_list.segments.iter().enumerate() {
600 let id_str = segment.id.to_string();
601 if !segment_ids.insert(id_str.clone()) {
602 issues.push(
603 ValidationIssue::new(
604 Severity::Error,
605 Category::Structure,
606 code(CoreConstraintsCode::UniqueSegmentId),
607 format!("Duplicate Segment Id '{}' at index {}", id_str, i),
608 )
609 .with_location(cpl_loc.clone().with_segment(i)),
610 );
611 }
612 }
613
614 if let Some(ref edl) = cpl.essence_descriptor_list {
617 let mut ed_ids = HashSet::new();
618 for ed in &edl.essence_descriptors {
619 let id_str = ed.id.to_string();
620 if !ed_ids.insert(id_str.clone()) {
621 issues.push(
622 ValidationIssue::new(
623 Severity::Error,
624 Category::Structure,
625 code(CoreConstraintsCode::UniqueEssenceDescriptorId),
626 format!("Duplicate EssenceDescriptor Id '{}'", id_str),
627 )
628 .with_location(cpl_loc.clone()),
629 );
630 }
631 }
632 }
633
634 let mut resource_ids = HashSet::new();
636 for (seg_idx, segment) in cpl.segment_list.segments.iter().enumerate() {
637 let mut check_resources = |resources: &[crate::cpl::Resource], track_type: &str| {
638 for resource in resources {
639 let id_str = resource.id.to_string();
640 if !resource_ids.insert(id_str.clone()) {
641 issues.push(
642 ValidationIssue::new(
643 Severity::Error,
644 Category::Structure,
645 code(CoreConstraintsCode::UniqueResourceId),
646 format!(
647 "Duplicate Resource Id '{}' in {} segment {}",
648 id_str,
649 track_type,
650 seg_idx + 1,
651 ),
652 )
653 .with_location(cpl_loc.clone().with_segment(seg_idx)),
654 );
655 }
656 }
657 };
658
659 let sl = &segment.sequence_list;
660 for seq in &sl.main_image_sequences {
661 check_resources(&seq.resource_list.resources, "MainImageSequence");
662 }
663 for seq in &sl.main_audio_sequences {
664 check_resources(&seq.resource_list.resources, "MainAudioSequence");
665 }
666 for seq in &sl.subtitles_sequences {
667 check_resources(&seq.resource_list.resources, "SubtitlesSequence");
668 }
669 for seq in &sl.hearing_impaired_captions_sequences {
670 check_resources(
671 &seq.resource_list.resources,
672 "HearingImpairedCaptionsSequence",
673 );
674 }
675 for seq in &sl.forced_narrative_sequences {
676 check_resources(&seq.resource_list.resources, "ForcedNarrativeSequence");
677 }
678 for seq in &sl.iab_sequences {
679 check_resources(&seq.resource_list.resources, "IABSequence");
680 }
681 for seq in &sl.marker_sequences {
682 check_resources(&seq.resource_list.resources, "MarkerSequence");
683 }
684 }
685}
686
687fn validate_resource_constraints(
699 cpl: &CompositionPlaylist,
700 code: fn(CoreConstraintsCode) -> &'static str,
701 issues: &mut Vec<ValidationIssue>,
702) {
703 use crate::cpl::SequenceAccess;
704
705 for (seg_idx, segment) in cpl.segment_list.segments.iter().enumerate() {
706 let validate_resources =
708 |seq: &dyn SequenceAccess,
709 track_type: &str,
710 is_marker: bool,
711 issues: &mut Vec<ValidationIssue>| {
712 for (res_idx, resource) in seq.resource_list().resources.iter().enumerate() {
713 let res_loc = Location::new()
714 .with_cpl(cpl.id)
715 .with_segment(seg_idx)
716 .with_resource(res_idx);
717
718 if resource.intrinsic_duration == 0 {
720 issues.push(
721 ValidationIssue::new(
722 Severity::Error,
723 Category::Timing,
724 code(CoreConstraintsCode::IntrinsicDuration),
725 format!(
726 "{} resource {} IntrinsicDuration shall be greater than 0",
727 track_type, resource.id,
728 ),
729 )
730 .with_location(res_loc.clone()),
731 );
732 }
733
734 let entry_point = resource.entry_point.unwrap_or(0);
736 if resource.entry_point.is_some()
737 && resource.intrinsic_duration > 0
738 && entry_point >= resource.intrinsic_duration
739 {
740 issues.push(
741 ValidationIssue::new(
742 Severity::Error,
743 Category::Timing,
744 code(CoreConstraintsCode::EntryPoint),
745 format!(
746 "{} resource {}: EntryPoint ({}) shall be less than \
747 IntrinsicDuration ({})",
748 track_type,
749 resource.id,
750 entry_point,
751 resource.intrinsic_duration,
752 ),
753 )
754 .with_location(res_loc.clone()),
755 );
756 }
757
758 if let Some(source_duration) = resource.source_duration {
761 if source_duration == 0 {
762 issues.push(
763 ValidationIssue::new(
764 Severity::Error,
765 Category::Timing,
766 code(CoreConstraintsCode::SourceDuration),
767 format!(
768 "{} resource {}: SourceDuration shall be greater than 0",
769 track_type, resource.id,
770 ),
771 )
772 .with_location(res_loc.clone()),
773 );
774 }
775 if entry_point + source_duration > resource.intrinsic_duration {
776 issues.push(
777 ValidationIssue::new(
778 Severity::Error,
779 Category::Timing,
780 code(CoreConstraintsCode::ResourceDuration),
781 format!(
782 "{} resource {}: EntryPoint ({}) + SourceDuration ({}) = {} \
783 exceeds IntrinsicDuration ({})",
784 track_type, resource.id,
785 entry_point, source_duration,
786 entry_point + source_duration,
787 resource.intrinsic_duration,
788 ),
789 )
790 .with_location(res_loc.clone()),
791 );
792 }
793 }
794
795 if let Some(repeat_count) = resource.repeat_count {
797 if repeat_count == 0 {
798 issues.push(
799 ValidationIssue::new(
800 Severity::Error,
801 Category::Timing,
802 code(CoreConstraintsCode::RepeatCount),
803 format!(
804 "{} resource {} RepeatCount shall be greater than 0",
805 track_type, resource.id,
806 ),
807 )
808 .with_location(res_loc.clone()),
809 );
810 }
811 }
812
813 if !is_marker && resource.track_file_id.is_none() {
815 issues.push(
816 ValidationIssue::new(
817 Severity::Error,
818 Category::Reference,
819 code(CoreConstraintsCode::TrackFileId),
820 format!(
821 "{} resource {} is missing TrackFileId",
822 track_type, resource.id,
823 ),
824 )
825 .with_location(res_loc.clone()),
826 );
827 }
828 }
829 };
830
831 let sl = &segment.sequence_list;
832 for seq in &sl.main_image_sequences {
833 validate_resources(seq, "MainImageSequence", false, issues);
834 }
835 for seq in &sl.main_audio_sequences {
836 validate_resources(seq, "MainAudioSequence", false, issues);
837 }
838 for seq in &sl.subtitles_sequences {
839 validate_resources(seq, "SubtitlesSequence", false, issues);
840 }
841 for seq in &sl.hearing_impaired_captions_sequences {
842 validate_resources(seq, "HearingImpairedCaptionsSequence", false, issues);
843 }
844 for seq in &sl.forced_narrative_sequences {
845 validate_resources(seq, "ForcedNarrativeSequence", false, issues);
846 }
847 for seq in &sl.iab_sequences {
848 validate_resources(seq, "IABSequence", false, issues);
849 }
850 for seq in &sl.marker_sequences {
851 validate_resources(seq, "MarkerSequence", true, issues);
852 }
853 }
854}
855
856fn collect_track_ids(segment: &crate::cpl::Segment) -> HashMap<String, &'static str> {
862 use crate::cpl::SequenceAccess;
863
864 let mut track_ids = HashMap::new();
865 let sl = &segment.sequence_list;
866
867 for seq in &sl.main_image_sequences {
868 track_ids.insert(seq.track_id().to_string(), "MainImageSequence");
869 }
870 for seq in &sl.main_audio_sequences {
871 track_ids.insert(seq.track_id().to_string(), "MainAudioSequence");
872 }
873 for seq in &sl.subtitles_sequences {
874 track_ids.insert(seq.track_id().to_string(), "SubtitlesSequence");
875 }
876 for seq in &sl.hearing_impaired_captions_sequences {
877 track_ids.insert(
878 seq.track_id().to_string(),
879 "HearingImpairedCaptionsSequence",
880 );
881 }
882 for seq in &sl.forced_narrative_sequences {
883 track_ids.insert(seq.track_id().to_string(), "ForcedNarrativeSequence");
884 }
885 for seq in &sl.iab_sequences {
886 track_ids.insert(seq.track_id().to_string(), "IABSequence");
887 }
888 track_ids
891}
892
893fn validate_virtual_track_continuity(
899 cpl: &CompositionPlaylist,
900 code: fn(CoreConstraintsCode) -> &'static str,
901 issues: &mut Vec<ValidationIssue>,
902) {
903 let segments = &cpl.segment_list.segments;
904 if segments.len() < 2 {
905 return; }
907
908 let mut all_track_ids: HashMap<String, &'static str> = HashMap::new();
910 let mut per_segment_tracks: Vec<HashSet<String>> = Vec::new();
911
912 for segment in segments {
913 let tracks = collect_track_ids(segment);
914 let track_set: HashSet<String> = tracks.keys().cloned().collect();
915 for (id, tt) in &tracks {
916 all_track_ids.entry(id.clone()).or_insert(tt);
917 }
918 per_segment_tracks.push(track_set);
919 }
920
921 for (track_id, track_type) in &all_track_ids {
923 for (seg_idx, seg_tracks) in per_segment_tracks.iter().enumerate() {
924 if !seg_tracks.contains(track_id) {
925 issues.push(
926 ValidationIssue::new(
927 Severity::Error,
928 Category::Structure,
929 code(CoreConstraintsCode::VirtualTrackContinuity),
930 format!(
931 "{} virtual track '{}' is missing from segment {} \
932 but is present in other segments; \
933 a virtual track shall be present in every segment",
934 track_type,
935 track_id,
936 seg_idx + 1,
937 ),
938 )
939 .with_location(Location::new().with_cpl(cpl.id).with_segment(seg_idx)),
940 );
941 }
942 }
943 }
944}
945
946fn validate_virtual_track_edit_rates(
955 cpl: &CompositionPlaylist,
956 code: fn(CoreConstraintsCode) -> &'static str,
957 issues: &mut Vec<ValidationIssue>,
958) {
959 use crate::cpl::SequenceAccess;
960
961 let mut track_edit_rates: HashMap<String, EditRate> = HashMap::new();
965
966 for (seg_idx, segment) in cpl.segment_list.segments.iter().enumerate() {
967 let check_sequence =
968 |seq: &dyn SequenceAccess,
969 track_type: &str,
970 issues: &mut Vec<ValidationIssue>,
971 track_edit_rates: &mut HashMap<String, EditRate>| {
972 let track_id = seq.track_id().to_string();
973 for resource in &seq.resource_list().resources {
974 let resolved_er = match resource.edit_rate.or(cpl.edit_rate) {
977 Some(er) => er,
978 None => continue,
979 };
980 match track_edit_rates.get(&track_id) {
981 None => {
982 track_edit_rates.insert(track_id.clone(), resolved_er);
984 }
985 Some(&first_er) => {
986 if resolved_er != first_er {
988 issues.push(
989 ValidationIssue::new(
990 Severity::Error,
991 Category::Timing,
992 code(CoreConstraintsCode::VirtualTrackEditRate),
993 format!(
994 "{} virtual track '{}': resource {} has EditRate {}/{} \
995 but earlier resources have {}/{}; \
996 all resources in a virtual track shall have the same edit rate",
997 track_type, track_id, resource.id,
998 resolved_er.numerator, resolved_er.denominator,
999 first_er.numerator, first_er.denominator,
1000 ),
1001 )
1002 .with_location(
1003 Location::new()
1004 .with_cpl(cpl.id)
1005 .with_segment(seg_idx),
1006 ),
1007 );
1008 }
1009 }
1010 }
1011 }
1012 };
1013
1014 let sl = &segment.sequence_list;
1015 for seq in &sl.main_image_sequences {
1016 check_sequence(seq, "MainImageSequence", issues, &mut track_edit_rates);
1017 }
1018 for seq in &sl.main_audio_sequences {
1019 check_sequence(seq, "MainAudioSequence", issues, &mut track_edit_rates);
1020 }
1021 for seq in &sl.subtitles_sequences {
1022 check_sequence(seq, "SubtitlesSequence", issues, &mut track_edit_rates);
1023 }
1024 for seq in &sl.hearing_impaired_captions_sequences {
1025 check_sequence(
1026 seq,
1027 "HearingImpairedCaptionsSequence",
1028 issues,
1029 &mut track_edit_rates,
1030 );
1031 }
1032 for seq in &sl.forced_narrative_sequences {
1033 check_sequence(
1034 seq,
1035 "ForcedNarrativeSequence",
1036 issues,
1037 &mut track_edit_rates,
1038 );
1039 }
1040 for seq in &sl.iab_sequences {
1041 check_sequence(seq, "IABSequence", issues, &mut track_edit_rates);
1042 }
1043 }
1044}
1045
1046fn validate_timed_text_extended(
1055 cpl: &CompositionPlaylist,
1056 code: fn(CoreConstraintsCode) -> &'static str,
1057 issues: &mut Vec<ValidationIssue>,
1058) {
1059 let edl = match &cpl.essence_descriptor_list {
1060 Some(edl) => edl,
1061 None => return,
1062 };
1063
1064 for ed in &edl.essence_descriptors {
1065 let tt = match &ed.dc_timed_text_descriptor {
1066 Some(tt) => tt,
1067 None => continue,
1068 };
1069
1070 let ed_loc = Location::new()
1071 .with_cpl(cpl.id)
1072 .with_path(format!("EssenceDescriptor/{}", ed.id));
1073
1074 if tt.sample_rate.is_none() {
1076 issues.push(
1077 ValidationIssue::new(
1078 Severity::Warning,
1079 Category::Subtitle,
1080 code(CoreConstraintsCode::TimedTextSampleRate),
1081 format!(
1082 "DCTimedTextDescriptor {}: SampleRate is missing; \
1083 should be present for frame-accurate subtitle timing",
1084 ed.id,
1085 ),
1086 )
1087 .with_location(ed_loc.clone()),
1088 );
1089 }
1090
1091 for tag in &tt.rfc5646_language_tag_list {
1093 let s = tag.as_str();
1094 if s.is_empty() {
1095 issues.push(
1096 ValidationIssue::new(
1097 Severity::Warning,
1098 Category::Subtitle,
1099 code(CoreConstraintsCode::TimedTextEmptyLanguageTag),
1100 format!(
1101 "DCTimedTextDescriptor {}: empty language tag in RFC5646LanguageTagList",
1102 ed.id,
1103 ),
1104 )
1105 .with_location(ed_loc.clone()),
1106 );
1107 } else if !s.chars().next().unwrap_or(' ').is_ascii_alphabetic() {
1108 issues.push(
1109 ValidationIssue::new(
1110 Severity::Warning,
1111 Category::Subtitle,
1112 code(CoreConstraintsCode::TimedTextMalformedLanguageTag),
1113 format!(
1114 "DCTimedTextDescriptor {}: language tag '{}' does not start with an ASCII letter (RFC 5646 primary subtag)",
1115 ed.id, s,
1116 ),
1117 )
1118 .with_location(ed_loc.clone()),
1119 );
1120 }
1121 }
1122 }
1123}
1124
1125fn expected_channel_count(tag: &crate::cpl::McaTagSymbol) -> Option<u32> {
1131 use crate::cpl::McaTagSymbol;
1132 match tag {
1133 McaTagSymbol::SgMono => Some(1),
1134 McaTagSymbol::SgSt => Some(2),
1135 McaTagSymbol::Sg51 => Some(6),
1136 McaTagSymbol::Sg71 | McaTagSymbol::Sg71Ds => Some(8),
1137 _ => None, }
1139}
1140
1141fn validate_audio_mca_labels(
1149 cpl: &CompositionPlaylist,
1150 code: fn(CoreConstraintsCode) -> &'static str,
1151 issues: &mut Vec<ValidationIssue>,
1152) {
1153 let edl = match &cpl.essence_descriptor_list {
1154 Some(edl) => edl,
1155 None => return,
1156 };
1157
1158 for ed in &edl.essence_descriptors {
1159 let Some(ref wave) = ed.wave_pcm_descriptor else {
1160 continue;
1161 };
1162
1163 let ed_loc = Location::new()
1164 .with_cpl(cpl.id)
1165 .with_path(format!("EssenceDescriptor/{}", ed.id));
1166
1167 if wave.audio_sample_rate.is_none() && wave.sample_rate.is_none() {
1169 issues.push(
1170 ValidationIssue::new(
1171 Severity::Warning,
1172 Category::Audio,
1173 code(CoreConstraintsCode::AudioSampleRate),
1174 format!(
1175 "WAVEPCMDescriptor {} has no AudioSampleRate or SampleRate",
1176 ed.id,
1177 ),
1178 )
1179 .with_location(ed_loc.clone()),
1180 );
1181 }
1182
1183 let channel_count = match wave.channel_count {
1185 Some(0) => {
1186 issues.push(
1187 ValidationIssue::new(
1188 Severity::Error,
1189 Category::Audio,
1190 code(CoreConstraintsCode::ChannelCount),
1191 format!("WAVEPCMDescriptor {} has ChannelCount of 0", ed.id,),
1192 )
1193 .with_location(ed_loc.clone()),
1194 );
1195 continue;
1196 }
1197 Some(n) => n,
1198 None => {
1199 issues.push(
1200 ValidationIssue::new(
1201 Severity::Warning,
1202 Category::Audio,
1203 code(CoreConstraintsCode::ChannelCount),
1204 format!("WAVEPCMDescriptor {} has no ChannelCount", ed.id,),
1205 )
1206 .with_location(ed_loc.clone()),
1207 );
1208 continue;
1209 }
1210 };
1211
1212 let sub = match &wave.sub_descriptors {
1214 Some(sub) => sub,
1215 None => {
1216 issues.push(
1217 ValidationIssue::new(
1218 Severity::Warning,
1219 Category::Audio,
1220 code(CoreConstraintsCode::MCASubDescriptors),
1221 format!(
1222 "WAVEPCMDescriptor {} ({} channels) has no SubDescriptors; \
1223 MCA labels are recommended for audio channel identification",
1224 ed.id, channel_count,
1225 ),
1226 )
1227 .with_location(ed_loc.clone()),
1228 );
1229 continue;
1230 }
1231 };
1232
1233 let sf = match &sub.soundfield_group_label_sub_descriptor {
1234 Some(sf) => sf,
1235 None => {
1236 issues.push(
1237 ValidationIssue::new(
1238 Severity::Warning,
1239 Category::Audio,
1240 code(CoreConstraintsCode::SoundfieldGroup),
1241 format!(
1242 "WAVEPCMDescriptor {} ({} channels) has SubDescriptors \
1243 but no SoundfieldGroupLabelSubDescriptor",
1244 ed.id, channel_count,
1245 ),
1246 )
1247 .with_location(ed_loc.clone()),
1248 );
1249 continue;
1250 }
1251 };
1252
1253 let tag = match &sf.mca_tag_symbol {
1255 Some(tag) => tag,
1256 None => {
1257 issues.push(
1258 ValidationIssue::new(
1259 Severity::Warning,
1260 Category::Audio,
1261 code(CoreConstraintsCode::MCATagSymbol),
1262 format!(
1263 "SoundfieldGroupLabelSubDescriptor for {} is missing MCATagSymbol",
1264 ed.id,
1265 ),
1266 )
1267 .with_location(ed_loc.clone()),
1268 );
1269 continue;
1270 }
1271 };
1272
1273 if let Some(expected) = expected_channel_count(tag) {
1275 if channel_count != expected {
1276 issues.push(
1277 ValidationIssue::new(
1278 Severity::Error,
1279 Category::Audio,
1280 code(CoreConstraintsCode::SoundfieldChannelCount),
1281 format!(
1282 "WAVEPCMDescriptor {} has ChannelCount {} but MCATagSymbol '{}' \
1283 expects {} channels",
1284 ed.id, channel_count, tag, expected,
1285 ),
1286 )
1287 .with_location(ed_loc),
1288 );
1289 }
1290 }
1291 }
1292}
1293
1294fn validate_segment_track_durations(cpl: &CompositionPlaylist, issues: &mut Vec<ValidationIssue>) {
1300 use crate::cpl::SequenceAccess;
1301
1302 fn sequence_duration_seconds(
1309 seq: &dyn SequenceAccess,
1310 cpl_edit_rate: Option<&EditRate>,
1311 ) -> Option<f64> {
1312 let total: f64 = seq
1313 .resource_list()
1314 .resources
1315 .iter()
1316 .map(|r| {
1317 let er = r.edit_rate.as_ref().or(cpl_edit_rate)?;
1318 let numer = er.numerator as f64;
1319 let denom = er.denominator as f64;
1320 if numer == 0.0 || denom == 0.0 {
1321 return Some(0.0);
1322 }
1323 let fps = numer / denom;
1324 let entry = r.entry_point.unwrap_or(0) as f64;
1325 let dur = r
1326 .source_duration
1327 .map(|d| d as f64)
1328 .unwrap_or_else(|| (r.intrinsic_duration as f64) - entry);
1329 let repeat = r.repeat_count.unwrap_or(1).max(1) as f64;
1330 Some((dur / fps) * repeat)
1331 })
1332 .try_fold(0.0_f64, |acc, v| v.map(|x| acc + x))?;
1333 Some(total)
1334 }
1335
1336 const TOLERANCE_SECONDS: f64 = 0.001;
1340
1341 let seg_dur_code: &'static str = match &cpl.namespace {
1342 CplNamespace::Dci429_7 | CplNamespace::Smpte2067_3_2013 => {
1343 St2067_3_2013::for_code(St2067_3Code::SegmentDuration)
1344 }
1345 CplNamespace::Smpte2067_3_2016 | CplNamespace::Unknown(_) => {
1346 St2067_3_2016::for_code(St2067_3Code::SegmentDuration)
1347 }
1348 };
1349
1350 for (seg_idx, segment) in cpl.segment_list.segments.iter().enumerate() {
1351 let sl = &segment.sequence_list;
1352 let seg_loc = Location::new().with_cpl(cpl.id).with_segment(seg_idx);
1353
1354 let er = cpl.edit_rate.as_ref();
1355
1356 let mut track_durations: Vec<(&str, String, f64)> = Vec::new();
1359
1360 let push =
1361 |v: &mut Vec<(&str, String, f64)>, label: &'static str, seq: &dyn SequenceAccess| {
1362 if let Some(secs) = sequence_duration_seconds(seq, er) {
1363 v.push((label, seq.track_id().to_string(), secs));
1364 }
1365 };
1366
1367 for seq in &sl.main_image_sequences {
1368 push(&mut track_durations, "MainImage", seq);
1369 }
1370 for seq in &sl.main_audio_sequences {
1371 push(&mut track_durations, "MainAudio", seq);
1372 }
1373 for seq in &sl.subtitles_sequences {
1374 push(&mut track_durations, "Subtitles", seq);
1375 }
1376 for seq in &sl.hearing_impaired_captions_sequences {
1377 push(&mut track_durations, "HICaptions", seq);
1378 }
1379 for seq in &sl.forced_narrative_sequences {
1380 push(&mut track_durations, "ForcedNarrative", seq);
1381 }
1382 for seq in &sl.iab_sequences {
1383 push(&mut track_durations, "IAB", seq);
1384 }
1385 for seq in &sl.isxd_sequences {
1386 push(&mut track_durations, "ISXD", seq);
1387 }
1388
1389 if track_durations.len() < 2 {
1390 continue; }
1392
1393 let first_secs = track_durations[0].2;
1395 for (track_type, track_id, duration_secs) in &track_durations[1..] {
1396 if (duration_secs - first_secs).abs() > TOLERANCE_SECONDS {
1397 issues.push(
1398 ValidationIssue::new(
1399 Severity::Error,
1400 Category::Timing,
1401 seg_dur_code,
1402 format!(
1403 "Segment {} {} track {}: duration {:.3}s differs from {} track {}: duration {:.3}s — \
1404 all virtual tracks in a segment shall have equal duration",
1405 seg_idx + 1,
1406 track_type,
1407 &track_id[..8.min(track_id.len())],
1408 duration_secs,
1409 track_durations[0].0,
1410 &track_durations[0].1[..8.min(track_durations[0].1.len())],
1411 first_secs,
1412 ),
1413 )
1414 .with_location(seg_loc.clone()),
1415 );
1416 }
1417 }
1418 }
1419}
1420
1421fn validate_digital_signature_notice(
1427 cpl: &CompositionPlaylist,
1428 code: fn(CoreConstraintsCode) -> &'static str,
1429 issues: &mut Vec<ValidationIssue>,
1430) {
1431 let supports_signatures = matches!(cpl.namespace, CplNamespace::Smpte2067_3_2016);
1435 if supports_signatures {
1436 issues.push(
1437 ValidationIssue::new(
1438 Severity::Info,
1439 Category::Security,
1440 code(CoreConstraintsCode::DigitalSignature),
1441 "Digital signature validation (ST 2067-2 §8) is not currently performed; \
1442 Signer/Signature XML elements are not parsed",
1443 )
1444 .with_location(Location::new().with_cpl(cpl.id)),
1445 );
1446 }
1447}
1448
1449fn validate_dangling_essence_descriptors(
1456 cpl: &CompositionPlaylist,
1457 code: fn(CoreConstraintsCode) -> &'static str,
1458 issues: &mut Vec<ValidationIssue>,
1459) {
1460 let edl = match &cpl.essence_descriptor_list {
1461 Some(edl) => edl,
1462 None => return,
1463 };
1464
1465 let mut referenced: HashSet<String> = HashSet::new();
1467
1468 for segment in &cpl.segment_list.segments {
1469 let sl = &segment.sequence_list;
1470 for seq in &sl.main_image_sequences {
1471 for r in &seq.resource_list.resources {
1472 if let Some(ref se) = r.source_encoding {
1473 referenced.insert(se.to_string());
1474 }
1475 }
1476 }
1477 for seq in &sl.main_audio_sequences {
1478 for r in &seq.resource_list.resources {
1479 if let Some(ref se) = r.source_encoding {
1480 referenced.insert(se.to_string());
1481 }
1482 }
1483 }
1484 for seq in &sl.subtitles_sequences {
1485 for r in &seq.resource_list.resources {
1486 if let Some(ref se) = r.source_encoding {
1487 referenced.insert(se.to_string());
1488 }
1489 }
1490 }
1491 for seq in &sl.iab_sequences {
1492 for r in &seq.resource_list.resources {
1493 if let Some(ref se) = r.source_encoding {
1494 referenced.insert(se.to_string());
1495 }
1496 }
1497 }
1498 for seq in &sl.hearing_impaired_captions_sequences {
1499 for r in &seq.resource_list.resources {
1500 if let Some(ref se) = r.source_encoding {
1501 referenced.insert(se.to_string());
1502 }
1503 }
1504 }
1505 for seq in &sl.forced_narrative_sequences {
1506 for r in &seq.resource_list.resources {
1507 if let Some(ref se) = r.source_encoding {
1508 referenced.insert(se.to_string());
1509 }
1510 }
1511 }
1512 for seq in &sl.isxd_sequences {
1513 for r in &seq.resource_list.resources {
1514 if let Some(ref se) = r.source_encoding {
1515 referenced.insert(se.to_string());
1516 }
1517 }
1518 }
1519 }
1520
1521 for ed in &edl.essence_descriptors {
1523 let id = ed.id.to_string();
1524 if !referenced.contains(&id) {
1525 issues.push(
1526 ValidationIssue::new(
1527 Severity::Error,
1528 Category::Reference,
1529 code(CoreConstraintsCode::DanglingEssenceDescriptor),
1530 format!(
1531 "EssenceDescriptor {} is present in EssenceDescriptorList but not \
1532 referenced by any Resource's SourceEncoding (ST 2067-2 §6.4.2)",
1533 id
1534 ),
1535 )
1536 .with_location(Location::new().with_cpl(cpl.id)),
1537 );
1538 }
1539 }
1540}
1541
1542pub struct CoreConstraints2020;
1551
1552impl ConstraintsValidator for CoreConstraints2020 {
1553 fn spec_id(&self) -> &str {
1554 "ST 2067-2:2020"
1555 }
1556
1557 fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
1558 let mut issues = Vec::new();
1559 let loc = Location::new().with_cpl(cpl.id);
1560
1561 validate_core_structure(
1562 cpl,
1563 crate::assetmap::codes::St2067_2_2020_Core::for_code,
1564 &mut issues,
1565 );
1566
1567 if cpl.essence_descriptor_list.is_none() {
1569 issues.push(
1570 ValidationIssue::new(
1571 Severity::Error,
1572 Category::Structure,
1573 crate::assetmap::codes::St2067_2_2020_Core::for_code(
1574 CoreConstraintsCode::EssenceDescriptorList,
1575 ),
1576 "EssenceDescriptorList is required per ST 2067-2:2020",
1577 )
1578 .with_location(loc),
1579 );
1580 }
1581
1582 issues
1586 }
1587}
1588
1589pub struct CoreConstraints2016;
1597
1598impl ConstraintsValidator for CoreConstraints2016 {
1599 fn spec_id(&self) -> &str {
1600 "ST 2067-2:2016"
1601 }
1602
1603 fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
1604 let mut issues = Vec::new();
1605
1606 validate_core_structure(
1607 cpl,
1608 crate::assetmap::codes::St2067_2_2016_Core::for_code,
1609 &mut issues,
1610 );
1611
1612 if cpl.essence_descriptor_list.is_none() {
1614 issues.push(
1615 ValidationIssue::new(
1616 Severity::Error,
1617 Category::Structure,
1618 crate::assetmap::codes::St2067_2_2016_Core::for_code(
1619 CoreConstraintsCode::EssenceDescriptorList,
1620 ),
1621 "EssenceDescriptorList is required per ST 2067-2:2016",
1622 )
1623 .with_location(Location::new().with_cpl(cpl.id)),
1624 );
1625 }
1626
1627 issues
1630 }
1631}
1632
1633pub struct CoreConstraints2013;
1642
1643impl ConstraintsValidator for CoreConstraints2013 {
1644 fn spec_id(&self) -> &str {
1645 "ST 2067-2:2013"
1646 }
1647
1648 fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
1649 let mut issues = Vec::new();
1650
1651 validate_core_structure(
1652 cpl,
1653 crate::assetmap::codes::St2067_2_2013_Core::for_code,
1654 &mut issues,
1655 );
1656
1657 issues
1662 }
1663}
1664
1665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1671pub enum QuantizationSystem {
1672 Qe1,
1675 Qe2,
1678}
1679
1680pub fn component_ref_values(qe: QuantizationSystem, bit_depth: u32) -> Option<(u32, u32)> {
1684 match (qe, bit_depth) {
1685 (QuantizationSystem::Qe1, 8) => Some((16, 235)),
1687 (QuantizationSystem::Qe1, 10) => Some((64, 940)),
1688 (QuantizationSystem::Qe1, 12) => Some((256, 3760)),
1689 (QuantizationSystem::Qe1, 16) => Some((4096, 60160)),
1690 (QuantizationSystem::Qe2, 8) => Some((0, 255)),
1692 (QuantizationSystem::Qe2, 10) => Some((0, 1023)),
1693 (QuantizationSystem::Qe2, 12) => Some((0, 4095)),
1694 (QuantizationSystem::Qe2, 16) => Some((0, 65535)),
1695 _ => None,
1696 }
1697}
1698
1699pub fn cdci_ref_values(color_sys: &ColorSystem, bit_depth: u32) -> Option<(u32, u32, u32)> {
1703 match (color_sys, bit_depth) {
1704 (ColorSystem::Color1 | ColorSystem::Color2 | ColorSystem::Color3, 8) => {
1706 Some((16, 235, 225))
1707 }
1708 (ColorSystem::Color1 | ColorSystem::Color2 | ColorSystem::Color3, 10) => {
1709 Some((64, 940, 897))
1710 }
1711 (ColorSystem::Color1 | ColorSystem::Color2 | ColorSystem::Color3, 12) => {
1712 Some((256, 3760, 3585))
1713 }
1714 (ColorSystem::Color1 | ColorSystem::Color2 | ColorSystem::Color3, 16) => {
1715 Some((4096, 60160, 57345))
1716 }
1717 (ColorSystem::Color4, 8) => Some((16, 235, 254)),
1719 (ColorSystem::Color4, 10) => Some((64, 940, 1013)),
1720 (ColorSystem::Color5 | ColorSystem::Color7 | ColorSystem::Color8, 10) => {
1722 Some((64, 940, 897))
1723 }
1724 (ColorSystem::Color5 | ColorSystem::Color7 | ColorSystem::Color8, 12) => {
1725 Some((256, 3760, 3585))
1726 }
1727 (ColorSystem::Color5 | ColorSystem::Color7 | ColorSystem::Color8, 16) => {
1728 Some((4096, 60160, 57345))
1729 }
1730 _ => None,
1731 }
1732}
1733
1734const APP2E_APPLICATION_IDENTIFICATION: &str = "http://www.smpte-ra.org/ns/2067-21/2021";
1745
1746#[allow(clippy::collapsible_match)]
1764fn validate_j2k_profile(
1765 codec: &crate::cpl::VideoCodec,
1766 stored_width: u32,
1767 stored_height: u32,
1768 allow_ht: bool,
1769 loc: &Location,
1770 issues: &mut Vec<ValidationIssue>,
1771) {
1772 use crate::cpl::VideoCodec;
1773 match codec {
1774 VideoCodec::Jpeg2000Ht => {
1775 if !allow_ht {
1776 issues.push(
1778 ValidationIssue::new(
1779 Severity::Error,
1780 Category::Encoding,
1781 St2067_21_2023::J2KHtNotAllowed.code(),
1782 "JPEG 2000 HT (ISO 15444-15) is not permitted by App2E 2020. \
1783 HT-J2K was introduced in ST 2067-21:2021.",
1784 )
1785 .with_location(loc.clone()),
1786 );
1787 }
1788 }
1792 VideoCodec::Jpeg2000Imf4k => {
1793 if !(stored_width > 2048
1795 && stored_width <= 4096
1796 && stored_height > 0
1797 && stored_height <= 3112)
1798 {
1799 issues.push(
1800 ValidationIssue::new(
1801 Severity::Error,
1802 Category::Encoding,
1803 St2067_21_2023::J2K4KResolution.code(),
1804 format!(
1805 "JPEG 2000 IMF 4K Profile does not support image resolution \
1806 ({}/{}); width must be 2049–4096, height 1–3112",
1807 stored_width, stored_height
1808 ),
1809 )
1810 .with_location(loc.clone()),
1811 );
1812 }
1813 }
1814 VideoCodec::Jpeg2000Imf2k => {
1815 if !(stored_width > 0
1817 && stored_width <= 2048
1818 && stored_height > 0
1819 && stored_height <= 1556)
1820 {
1821 issues.push(
1822 ValidationIssue::new(
1823 Severity::Error,
1824 Category::Encoding,
1825 St2067_21_2023::J2K2KResolution.code(),
1826 format!(
1827 "JPEG 2000 IMF 2K Profile does not support image resolution \
1828 ({}/{}); width must be 1–2048, height 1–1556",
1829 stored_width, stored_height
1830 ),
1831 )
1832 .with_location(loc.clone()),
1833 );
1834 }
1835 }
1836 VideoCodec::Jpeg2000Broadcast => {
1837 if !(stored_width > 0
1839 && stored_width <= 3840
1840 && stored_height > 0
1841 && stored_height <= 2160)
1842 {
1843 issues.push(
1844 ValidationIssue::new(
1845 Severity::Error,
1846 Category::Encoding,
1847 St2067_21_2023::J2KBcpResolution.code(),
1848 format!(
1849 "JPEG 2000 Broadcast Contribution Profile does not support image \
1850 resolution ({}/{}); width must be 1–3840, height 1–2160",
1851 stored_width, stored_height
1852 ),
1853 )
1854 .with_location(loc.clone()),
1855 );
1856 }
1857 }
1858 VideoCodec::Jpeg2000 => {
1859 }
1864 _ => {
1865 }
1867 }
1868}
1869
1870pub struct App2E2021;
1880
1881impl App2E2021 {
1882 fn validate_image_descriptors(
1885 &self,
1886 cpl: &CompositionPlaylist,
1887 allow_ht: bool,
1888 issues: &mut Vec<ValidationIssue>,
1889 ) {
1890 let edl = match &cpl.essence_descriptor_list {
1891 Some(edl) => edl,
1892 None => return, };
1894
1895 for ed in &edl.essence_descriptors {
1896 if let Some(ref rgba) = ed.rgba_descriptor {
1897 self.validate_rgba_descriptor(&ed.id.to_string(), rgba, cpl, allow_ht, issues);
1898 }
1899 if let Some(ref cdci) = ed.cdci_descriptor {
1900 self.validate_cdci_descriptor(&ed.id.to_string(), cdci, cpl, allow_ht, issues);
1901 }
1902 }
1903 }
1904
1905 fn validate_rgba_descriptor(
1910 &self,
1911 ed_id: &str,
1912 rgba: &crate::cpl::RGBADescriptor,
1913 cpl: &CompositionPlaylist,
1914 allow_ht: bool,
1915 issues: &mut Vec<ValidationIssue>,
1916 ) {
1917 let loc = Location::new()
1918 .with_cpl(cpl.id)
1919 .with_path(format!("EssenceDescriptor[{}]/RGBADescriptor", ed_id));
1920
1921 if let Some(ref codec) = rgba.picture_compression {
1925 if !codec.is_jpeg2000_family() {
1926 issues.push(
1927 ValidationIssue::new(
1928 Severity::Error,
1929 Category::Encoding,
1930 St2067_21_2023::J2KRequired.code().to_string(),
1931 format!(
1932 "PictureCompression shall be JPEG 2000 for App2E, found: {}",
1933 codec
1934 ),
1935 )
1936 .with_location(loc.clone()),
1937 );
1938 } else {
1939 let w = rgba.stored_width.unwrap_or(0);
1941 let h = rgba.stored_height.unwrap_or(0);
1942 validate_j2k_profile(codec, w, h, allow_ht, &loc, issues);
1943 }
1944 }
1945
1946 if let Some(ref fl) = rgba.frame_layout {
1948 if fl != "FullFrame" && fl != "SeparateFields" {
1949 issues.push(
1950 ValidationIssue::new(
1951 Severity::Error,
1952 Category::Video,
1953 St2067_21_2023::FrameLayout.code().to_string(),
1954 format!(
1955 "FrameLayout shall be FullFrame (00h) or SeparateFields (01h), found: {}",
1956 fl
1957 ),
1958 )
1959 .with_location(loc.clone()),
1960 );
1961 }
1962 }
1963
1964 if rgba.stored_f2_offset.is_some() {
1966 issues.push(
1967 ValidationIssue::new(
1968 Severity::Error,
1969 Category::Video,
1970 St2067_21_2023::StoredF2Offset.code().to_string(),
1971 "StoredF2Offset shall not be present (Table 8)",
1972 )
1973 .with_location(loc.clone()),
1974 );
1975 }
1976
1977 if let Some(sw) = rgba.sampled_width {
1979 if let Some(stored_w) = rgba.stored_width {
1980 if sw != stored_w {
1981 issues.push(
1982 ValidationIssue::new(
1983 Severity::Error,
1984 Category::Video,
1985 St2067_21_2023::SampledWidth.code().to_string(),
1986 format!(
1987 "SampledWidth ({}) shall not be present or shall be equal to StoredWidth ({})",
1988 sw, stored_w
1989 ),
1990 )
1991 .with_location(loc.clone()),
1992 );
1993 }
1994 }
1995 }
1996
1997 if let Some(sh) = rgba.sampled_height {
1999 if let Some(stored_h) = rgba.stored_height {
2000 if sh != stored_h {
2001 issues.push(
2002 ValidationIssue::new(
2003 Severity::Error,
2004 Category::Video,
2005 St2067_21_2023::SampledHeight.code().to_string(),
2006 format!(
2007 "SampledHeight ({}) shall not be present or shall be equal to StoredHeight ({})",
2008 sh, stored_h
2009 ),
2010 )
2011 .with_location(loc.clone()),
2012 );
2013 }
2014 }
2015 }
2016
2017 if let Some(sxo) = rgba.sampled_x_offset {
2019 if sxo != 0 {
2020 issues.push(
2021 ValidationIssue::new(
2022 Severity::Error,
2023 Category::Video,
2024 St2067_21_2023::SampledXOffset.code().to_string(),
2025 format!(
2026 "SampledXOffset shall not be present or shall be 0, found: {}",
2027 sxo
2028 ),
2029 )
2030 .with_location(loc.clone()),
2031 );
2032 }
2033 }
2034
2035 if let Some(syo) = rgba.sampled_y_offset {
2037 if syo != 0 {
2038 issues.push(
2039 ValidationIssue::new(
2040 Severity::Error,
2041 Category::Video,
2042 St2067_21_2023::SampledYOffset.code().to_string(),
2043 format!(
2044 "SampledYOffset shall not be present or shall be 0, found: {}",
2045 syo
2046 ),
2047 )
2048 .with_location(loc.clone()),
2049 );
2050 }
2051 }
2052
2053 if rgba.alpha_transparency.is_some() {
2055 issues.push(
2056 ValidationIssue::new(
2057 Severity::Error,
2058 Category::Video,
2059 St2067_21_2023::AlphaTransparency.code().to_string(),
2060 "AlphaTransparency shall not be present (Table 8)",
2061 )
2062 .with_location(loc.clone()),
2063 );
2064 }
2065
2066 if rgba.image_alignment_offset.is_some() {
2068 issues.push(
2069 ValidationIssue::new(
2070 Severity::Error,
2071 Category::Video,
2072 St2067_21_2023::ImageAlignmentOffset.code().to_string(),
2073 "ImageAlignmentOffset shall not be present (Table 8)",
2074 )
2075 .with_location(loc.clone()),
2076 );
2077 }
2078
2079 if rgba.image_start_offset.is_some() {
2081 issues.push(
2082 ValidationIssue::new(
2083 Severity::Error,
2084 Category::Video,
2085 St2067_21_2023::ImageStartOffset.code().to_string(),
2086 "ImageStartOffset shall not be present (Table 8)",
2087 )
2088 .with_location(loc.clone()),
2089 );
2090 }
2091
2092 if rgba.image_end_offset.is_some() {
2094 issues.push(
2095 ValidationIssue::new(
2096 Severity::Error,
2097 Category::Video,
2098 St2067_21_2023::ImageEndOffset.code().to_string(),
2099 "ImageEndOffset shall not be present (Table 8)",
2100 )
2101 .with_location(loc.clone()),
2102 );
2103 }
2104
2105 if let Some(ref fl) = rgba.frame_layout {
2107 if fl == "FullFrame" && rgba.field_dominance.is_some() {
2108 issues.push(
2109 ValidationIssue::new(
2110 Severity::Error,
2111 Category::Video,
2112 St2067_21_2023::FieldDominance.code().to_string(),
2113 "FieldDominance shall not be present for progressive (FullFrame) content",
2114 )
2115 .with_location(loc.clone()),
2116 );
2117 }
2118 if fl == "SeparateFields" && rgba.field_dominance.is_none() {
2119 issues.push(
2120 ValidationIssue::new(
2121 Severity::Error,
2122 Category::Video,
2123 St2067_21_2023::FieldDominance.code().to_string(),
2124 "FieldDominance shall be present for interlaced (SeparateFields) content",
2125 )
2126 .with_location(loc.clone()),
2127 );
2128 }
2129 }
2130
2131 match &rgba.color_primaries {
2139 Some(cp) if matches!(cp, ColorPrimaries::Unknown(_)) => {
2140 issues.push(
2141 ValidationIssue::new(
2142 Severity::Error,
2143 Category::Video,
2144 St2067_21_2023::ColorPrimariesUnknown.code().to_string(),
2145 format!("Unrecognized ColorPrimaries UL: {}", cp),
2146 )
2147 .with_location(loc.clone()),
2148 );
2149 }
2150 None => {
2151 issues.push(
2152 ValidationIssue::new(
2153 Severity::Error,
2154 Category::Video,
2155 St2067_21_2023::ColorPrimariesMissing.code().to_string(),
2156 "ColorPrimaries shall be present (Table 8)",
2157 )
2158 .with_location(loc.clone()),
2159 );
2160 }
2161 _ => {}
2162 }
2163
2164 match &rgba.transfer_characteristic {
2166 Some(tc) if matches!(tc, TransferCharacteristic::Unknown(_)) => {
2167 issues.push(
2168 ValidationIssue::new(
2169 Severity::Error,
2170 Category::Video,
2171 St2067_21_2023::TransferCharacteristicUnknown
2172 .code()
2173 .to_string(),
2174 format!("Unrecognized TransferCharacteristic UL: {}", tc),
2175 )
2176 .with_location(loc.clone()),
2177 );
2178 }
2179 None => {
2180 issues.push(
2181 ValidationIssue::new(
2182 Severity::Error,
2183 Category::Video,
2184 St2067_21_2023::TransferCharacteristicMissing
2185 .code()
2186 .to_string(),
2187 "TransferCharacteristic shall be present (Table 8)",
2188 )
2189 .with_location(loc.clone()),
2190 );
2191 }
2192 _ => {}
2193 }
2194
2195 if rgba.component_max_ref.is_none() {
2199 issues.push(
2200 ValidationIssue::new(
2201 Severity::Error,
2202 Category::Video,
2203 St2067_21_2023::ComponentMaxRef.code().to_string(),
2204 "ComponentMaxRef shall be present (Table 10)",
2205 )
2206 .with_location(loc.clone()),
2207 );
2208 }
2209
2210 if rgba.component_min_ref.is_none() {
2212 issues.push(
2213 ValidationIssue::new(
2214 Severity::Error,
2215 Category::Video,
2216 St2067_21_2023::ComponentMinRef.code().to_string(),
2217 "ComponentMinRef shall be present (Table 10)",
2218 )
2219 .with_location(loc.clone()),
2220 );
2221 }
2222
2223 match &rgba.scanning_direction {
2225 Some(sd) if sd != "ScanningDirection_LeftToRightTopToBottom" => {
2226 issues.push(
2227 ValidationIssue::new(
2228 Severity::Error,
2229 Category::Video,
2230 St2067_21_2023::ScanningDirection.code().to_string(),
2231 format!(
2232 "ScanningDirection shall be 00h (LeftToRightTopToBottom), found: {}",
2233 sd
2234 ),
2235 )
2236 .with_location(loc.clone()),
2237 );
2238 }
2239 None => {
2240 issues.push(
2241 ValidationIssue::new(
2242 Severity::Error,
2243 Category::Video,
2244 St2067_21_2023::ScanningDirection.code().to_string(),
2245 "ScanningDirection shall be present (Table 10)",
2246 )
2247 .with_location(loc.clone()),
2248 );
2249 }
2250 _ => {} }
2252
2253 if rgba.alpha_max_ref.is_some() {
2255 issues.push(
2256 ValidationIssue::new(
2257 Severity::Error,
2258 Category::Video,
2259 St2067_21_2023::AlphaMaxRef.code().to_string(),
2260 "AlphaMaxRef shall not be present (Table 10)",
2261 )
2262 .with_location(loc.clone()),
2263 );
2264 }
2265
2266 if rgba.alpha_min_ref.is_some() {
2268 issues.push(
2269 ValidationIssue::new(
2270 Severity::Error,
2271 Category::Video,
2272 St2067_21_2023::AlphaMinRef.code().to_string(),
2273 "AlphaMinRef shall not be present (Table 10)",
2274 )
2275 .with_location(loc.clone()),
2276 );
2277 }
2278
2279 if rgba.palette.is_some() {
2281 issues.push(
2282 ValidationIssue::new(
2283 Severity::Error,
2284 Category::Video,
2285 St2067_21_2023::Palette.code().to_string(),
2286 "Palette shall not be present (Table 10)",
2287 )
2288 .with_location(loc.clone()),
2289 );
2290 }
2291
2292 if rgba.palette_layout.is_some() {
2294 issues.push(
2295 ValidationIssue::new(
2296 Severity::Error,
2297 Category::Video,
2298 St2067_21_2023::PaletteLayout.code().to_string(),
2299 "PaletteLayout shall not be present (Table 10)",
2300 )
2301 .with_location(loc.clone()),
2302 );
2303 }
2304
2305 if let (Some(min_ref), Some(max_ref)) = (rgba.component_min_ref, rgba.component_max_ref) {
2309 let qe = if min_ref == 0 {
2310 QuantizationSystem::Qe2
2311 } else {
2312 QuantizationSystem::Qe1
2313 };
2314 let bit_depth = match max_ref {
2316 235 | 255 => Some(8u32),
2317 940 | 1023 => Some(10),
2318 3760 | 4095 => Some(12),
2319 60160 | 65535 => Some(16),
2320 _ => None,
2321 };
2322 if let Some(bd) = bit_depth {
2323 if let Some((expected_min, expected_max)) = component_ref_values(qe, bd) {
2324 if min_ref != expected_min || max_ref != expected_max {
2325 issues.push(
2326 ValidationIssue::new(
2327 Severity::Error,
2328 Category::Video,
2329 St2067_21_2023::ComponentRefValues.code().to_string(),
2330 format!(
2331 "ComponentMinRef={}, ComponentMaxRef={} do not match \
2332 {:?} at {} bits (expected min={}, max={})",
2333 min_ref, max_ref, qe, bd, expected_min, expected_max
2334 ),
2335 )
2336 .with_location(loc.clone()),
2337 );
2338 }
2339 }
2340 }
2341 }
2342
2343 if let (Some(cp), Some(tc)) = (&rgba.color_primaries, &rgba.transfer_characteristic) {
2346 let color_sys = ColorSystem::from_components(cp, tc, None);
2347 if color_sys.is_none()
2348 && !matches!(cp, ColorPrimaries::Unknown(_))
2349 && !matches!(tc, TransferCharacteristic::Unknown(_))
2350 {
2351 issues.push(
2352 ValidationIssue::new(
2353 Severity::Error,
2354 Category::Video,
2355 St2067_21_2023::ColorSystem.code().to_string(),
2356 format!(
2357 "ColorPrimaries={} + TransferCharacteristic={} does not form a \
2358 recognized Color System for RGB",
2359 cp, tc
2360 ),
2361 )
2362 .with_location(loc.clone()),
2363 );
2364 }
2365 }
2366
2367 self.check_hdr_metadata(rgba.transfer_characteristic.as_ref(), cpl, &loc, issues);
2369
2370 self.validate_j2k_sub_descriptor(
2372 rgba.sub_descriptors.as_ref(),
2373 rgba.picture_compression.as_ref(),
2374 &loc,
2375 issues,
2376 );
2377 }
2378
2379 fn validate_cdci_descriptor(
2384 &self,
2385 ed_id: &str,
2386 cdci: &crate::cpl::CDCIDescriptor,
2387 cpl: &CompositionPlaylist,
2388 allow_ht: bool,
2389 issues: &mut Vec<ValidationIssue>,
2390 ) {
2391 let loc = Location::new()
2392 .with_cpl(cpl.id)
2393 .with_path(format!("EssenceDescriptor[{}]/CDCIDescriptor", ed_id));
2394
2395 if let Some(ref codec) = cdci.picture_compression {
2399 if !codec.is_jpeg2000_family() {
2400 issues.push(
2401 ValidationIssue::new(
2402 Severity::Error,
2403 Category::Encoding,
2404 St2067_21_2023::J2KRequired.code().to_string(),
2405 format!(
2406 "PictureCompression shall be JPEG 2000 for App2E, found: {}",
2407 codec
2408 ),
2409 )
2410 .with_location(loc.clone()),
2411 );
2412 } else {
2413 let w = cdci.stored_width.unwrap_or(0);
2415 let h = cdci.stored_height.unwrap_or(0);
2416 validate_j2k_profile(codec, w, h, allow_ht, &loc, issues);
2417 }
2418 }
2419
2420 if let Some(ref fl) = cdci.frame_layout {
2422 if fl != "FullFrame" && fl != "SeparateFields" {
2423 issues.push(
2424 ValidationIssue::new(
2425 Severity::Error,
2426 Category::Video,
2427 St2067_21_2023::FrameLayout.code().to_string(),
2428 format!(
2429 "FrameLayout shall be FullFrame (00h) or SeparateFields (01h), found: {}",
2430 fl
2431 ),
2432 )
2433 .with_location(loc.clone()),
2434 );
2435 }
2436 }
2437
2438 if cdci.stored_f2_offset.is_some() {
2440 issues.push(
2441 ValidationIssue::new(
2442 Severity::Error,
2443 Category::Video,
2444 St2067_21_2023::StoredF2Offset.code().to_string(),
2445 "StoredF2Offset shall not be present (Table 8)",
2446 )
2447 .with_location(loc.clone()),
2448 );
2449 }
2450
2451 if let Some(sw) = cdci.sampled_width {
2453 if let Some(stored_w) = cdci.stored_width {
2454 if sw != stored_w {
2455 issues.push(
2456 ValidationIssue::new(
2457 Severity::Error,
2458 Category::Video,
2459 St2067_21_2023::SampledWidth.code().to_string(),
2460 format!(
2461 "SampledWidth ({}) shall not be present or shall be equal to StoredWidth ({})",
2462 sw, stored_w
2463 ),
2464 )
2465 .with_location(loc.clone()),
2466 );
2467 }
2468 }
2469 }
2470
2471 if let Some(sh) = cdci.sampled_height {
2473 if let Some(stored_h) = cdci.stored_height {
2474 if sh != stored_h {
2475 issues.push(
2476 ValidationIssue::new(
2477 Severity::Error,
2478 Category::Video,
2479 St2067_21_2023::SampledHeight.code().to_string(),
2480 format!(
2481 "SampledHeight ({}) shall not be present or shall be equal to StoredHeight ({})",
2482 sh, stored_h
2483 ),
2484 )
2485 .with_location(loc.clone()),
2486 );
2487 }
2488 }
2489 }
2490
2491 if let Some(sxo) = cdci.sampled_x_offset {
2493 if sxo != 0 {
2494 issues.push(
2495 ValidationIssue::new(
2496 Severity::Error,
2497 Category::Video,
2498 St2067_21_2023::SampledXOffset.code().to_string(),
2499 format!(
2500 "SampledXOffset shall not be present or shall be 0, found: {}",
2501 sxo
2502 ),
2503 )
2504 .with_location(loc.clone()),
2505 );
2506 }
2507 }
2508
2509 if let Some(syo) = cdci.sampled_y_offset {
2511 if syo != 0 {
2512 issues.push(
2513 ValidationIssue::new(
2514 Severity::Error,
2515 Category::Video,
2516 St2067_21_2023::SampledYOffset.code().to_string(),
2517 format!(
2518 "SampledYOffset shall not be present or shall be 0, found: {}",
2519 syo
2520 ),
2521 )
2522 .with_location(loc.clone()),
2523 );
2524 }
2525 }
2526
2527 if cdci.alpha_transparency.is_some() {
2529 issues.push(
2530 ValidationIssue::new(
2531 Severity::Error,
2532 Category::Video,
2533 St2067_21_2023::AlphaTransparency.code().to_string(),
2534 "AlphaTransparency shall not be present (Table 8)",
2535 )
2536 .with_location(loc.clone()),
2537 );
2538 }
2539
2540 if cdci.image_alignment_offset.is_some() {
2542 issues.push(
2543 ValidationIssue::new(
2544 Severity::Error,
2545 Category::Video,
2546 St2067_21_2023::ImageAlignmentOffset.code().to_string(),
2547 "ImageAlignmentOffset shall not be present (Table 8)",
2548 )
2549 .with_location(loc.clone()),
2550 );
2551 }
2552
2553 if cdci.image_start_offset.is_some() {
2555 issues.push(
2556 ValidationIssue::new(
2557 Severity::Error,
2558 Category::Video,
2559 St2067_21_2023::ImageStartOffset.code().to_string(),
2560 "ImageStartOffset shall not be present (Table 8)",
2561 )
2562 .with_location(loc.clone()),
2563 );
2564 }
2565
2566 if cdci.image_end_offset.is_some() {
2568 issues.push(
2569 ValidationIssue::new(
2570 Severity::Error,
2571 Category::Video,
2572 St2067_21_2023::ImageEndOffset.code().to_string(),
2573 "ImageEndOffset shall not be present (Table 8)",
2574 )
2575 .with_location(loc.clone()),
2576 );
2577 }
2578
2579 if let Some(ref fl) = cdci.frame_layout {
2581 if fl == "FullFrame" && cdci.field_dominance.is_some() {
2582 issues.push(
2583 ValidationIssue::new(
2584 Severity::Error,
2585 Category::Video,
2586 St2067_21_2023::FieldDominance.code().to_string(),
2587 "FieldDominance shall not be present for progressive (FullFrame) content",
2588 )
2589 .with_location(loc.clone()),
2590 );
2591 }
2592 if fl == "SeparateFields" && cdci.field_dominance.is_none() {
2593 issues.push(
2594 ValidationIssue::new(
2595 Severity::Error,
2596 Category::Video,
2597 St2067_21_2023::FieldDominance.code().to_string(),
2598 "FieldDominance shall be present for interlaced (SeparateFields) content",
2599 )
2600 .with_location(loc.clone()),
2601 );
2602 }
2603 }
2604
2605 match &cdci.color_primaries {
2607 Some(cp) if matches!(cp, ColorPrimaries::Unknown(_)) => {
2608 issues.push(
2609 ValidationIssue::new(
2610 Severity::Error,
2611 Category::Video,
2612 St2067_21_2023::ColorPrimariesUnknown.code().to_string(),
2613 format!("Unrecognized ColorPrimaries UL: {}", cp),
2614 )
2615 .with_location(loc.clone()),
2616 );
2617 }
2618 None => {
2619 issues.push(
2620 ValidationIssue::new(
2621 Severity::Error,
2622 Category::Video,
2623 St2067_21_2023::ColorPrimariesMissing.code().to_string(),
2624 "ColorPrimaries shall be present (Table 8)",
2625 )
2626 .with_location(loc.clone()),
2627 );
2628 }
2629 _ => {}
2630 }
2631
2632 match &cdci.transfer_characteristic {
2634 Some(tc) if matches!(tc, TransferCharacteristic::Unknown(_)) => {
2635 issues.push(
2636 ValidationIssue::new(
2637 Severity::Error,
2638 Category::Video,
2639 St2067_21_2023::TransferCharacteristicUnknown
2640 .code()
2641 .to_string(),
2642 format!("Unrecognized TransferCharacteristic UL: {}", tc),
2643 )
2644 .with_location(loc.clone()),
2645 );
2646 }
2647 None => {
2648 issues.push(
2649 ValidationIssue::new(
2650 Severity::Error,
2651 Category::Video,
2652 St2067_21_2023::TransferCharacteristicMissing
2653 .code()
2654 .to_string(),
2655 "TransferCharacteristic shall be present (Table 8)",
2656 )
2657 .with_location(loc.clone()),
2658 );
2659 }
2660 _ => {}
2661 }
2662
2663 match &cdci.coding_equations {
2665 Some(ce) if matches!(ce, CodingEquations::Unknown(_)) => {
2666 issues.push(
2667 ValidationIssue::new(
2668 Severity::Error,
2669 Category::Video,
2670 St2067_21_2023::CodingEquationsUnknown.code().to_string(),
2671 format!("Unrecognized CodingEquations UL: {}", ce),
2672 )
2673 .with_location(loc.clone()),
2674 );
2675 }
2676 None => {
2677 issues.push(
2678 ValidationIssue::new(
2679 Severity::Error,
2680 Category::Video,
2681 St2067_21_2023::CodingEquationsMissing.code().to_string(),
2682 "CodingEquations shall be present for Y'C'BC'R (Table 8)",
2683 )
2684 .with_location(loc.clone()),
2685 );
2686 }
2687 _ => {}
2688 }
2689
2690 match cdci.component_depth {
2694 Some(depth) if !matches!(depth, 8 | 10 | 12 | 16) => {
2695 issues.push(
2696 ValidationIssue::new(
2697 Severity::Error,
2698 Category::Video,
2699 St2067_21_2023::ComponentDepth.code().to_string(),
2700 format!(
2701 "ComponentDepth {} is not allowed; shall be 8, 10, 12, or 16",
2702 depth
2703 ),
2704 )
2705 .with_location(loc.clone()),
2706 );
2707 }
2708 None => {
2709 issues.push(
2710 ValidationIssue::new(
2711 Severity::Error,
2712 Category::Video,
2713 St2067_21_2023::ComponentDepth.code().to_string(),
2714 "ComponentDepth shall be present (Table 12)",
2715 )
2716 .with_location(loc.clone()),
2717 );
2718 }
2719 _ => {}
2720 }
2721
2722 match cdci.horizontal_subsampling {
2725 Some(hs) if hs != 1 && hs != 2 => {
2726 issues.push(
2727 ValidationIssue::new(
2728 Severity::Error,
2729 Category::Video,
2730 St2067_21_2023::HorizontalSubsampling.code().to_string(),
2731 format!(
2732 "HorizontalSubsampling shall be 1 (4:4:4) or 2 (4:2:2), found: {}",
2733 hs
2734 ),
2735 )
2736 .with_location(loc.clone()),
2737 );
2738 }
2739 None => {
2740 issues.push(
2741 ValidationIssue::new(
2742 Severity::Error,
2743 Category::Video,
2744 St2067_21_2023::HorizontalSubsampling.code().to_string(),
2745 "HorizontalSubsampling shall be present and equal to 1 (4:4:4) or 2 (4:2:2)",
2746 )
2747 .with_location(loc.clone()),
2748 );
2749 }
2750 _ => {}
2751 }
2752
2753 match cdci.vertical_subsampling {
2755 Some(vs) if vs != 1 => {
2756 issues.push(
2757 ValidationIssue::new(
2758 Severity::Error,
2759 Category::Video,
2760 St2067_21_2023::VerticalSubsampling.code().to_string(),
2761 format!("VerticalSubsampling shall be 1, found: {}", vs),
2762 )
2763 .with_location(loc.clone()),
2764 );
2765 }
2766 None => {
2767 issues.push(
2768 ValidationIssue::new(
2769 Severity::Error,
2770 Category::Video,
2771 St2067_21_2023::VerticalSubsampling.code().to_string(),
2772 "VerticalSubsampling shall be present and equal to 1",
2773 )
2774 .with_location(loc.clone()),
2775 );
2776 }
2777 _ => {}
2778 }
2779
2780 match cdci.color_siting {
2782 Some(cs) if cs != 0 => {
2783 issues.push(
2784 ValidationIssue::new(
2785 Severity::Error,
2786 Category::Video,
2787 St2067_21_2023::ColorSiting.code().to_string(),
2788 format!("ColorSiting shall be 0, found: {}", cs),
2789 )
2790 .with_location(loc.clone()),
2791 );
2792 }
2793 None => {
2794 issues.push(
2795 ValidationIssue::new(
2796 Severity::Error,
2797 Category::Video,
2798 St2067_21_2023::ColorSiting.code().to_string(),
2799 "ColorSiting shall be present and equal to 0",
2800 )
2801 .with_location(loc.clone()),
2802 );
2803 }
2804 _ => {}
2805 }
2806
2807 if cdci.reversed_byte_order.is_some() {
2809 issues.push(
2810 ValidationIssue::new(
2811 Severity::Error,
2812 Category::Video,
2813 St2067_21_2023::ReversedByteOrder.code().to_string(),
2814 "ReversedByteOrder shall not be present (Table 12)",
2815 )
2816 .with_location(loc.clone()),
2817 );
2818 }
2819
2820 if cdci.padding_bits.is_some() {
2822 issues.push(
2823 ValidationIssue::new(
2824 Severity::Error,
2825 Category::Video,
2826 St2067_21_2023::PaddingBits.code().to_string(),
2827 "PaddingBits shall not be present (Table 12)",
2828 )
2829 .with_location(loc.clone()),
2830 );
2831 }
2832
2833 if cdci.alpha_sample_depth.is_some() {
2835 issues.push(
2836 ValidationIssue::new(
2837 Severity::Error,
2838 Category::Video,
2839 St2067_21_2023::AlphaSampleDepth.code().to_string(),
2840 "AlphaSampleDepth shall not be present (Table 12)",
2841 )
2842 .with_location(loc.clone()),
2843 );
2844 }
2845
2846 if let (Some(cp), Some(tc), Some(ce)) = (
2850 &cdci.color_primaries,
2851 &cdci.transfer_characteristic,
2852 &cdci.coding_equations,
2853 ) {
2854 let color_sys = ColorSystem::from_components(cp, tc, Some(ce));
2856 if let Some(ref cs) = color_sys {
2857 if let Some(depth) = cdci.component_depth {
2859 if let Some((exp_black, exp_white, exp_range)) = cdci_ref_values(cs, depth) {
2860 if let Some(black) = cdci.black_ref_level {
2861 if black != exp_black {
2862 issues.push(
2863 ValidationIssue::new(
2864 Severity::Error,
2865 Category::Video,
2866 St2067_21_2023::BlackRefLevel.code().to_string(),
2867 format!(
2868 "BlackRefLevel={} for {} at {}-bit; expected {}",
2869 black, cs, depth, exp_black
2870 ),
2871 )
2872 .with_location(loc.clone()),
2873 );
2874 }
2875 }
2876 if let Some(white) = cdci.white_ref_level {
2877 if white != exp_white {
2878 issues.push(
2879 ValidationIssue::new(
2880 Severity::Error,
2881 Category::Video,
2882 St2067_21_2023::WhiteRefLevel.code().to_string(),
2883 format!(
2884 "WhiteRefLevel={} for {} at {}-bit; expected {}",
2885 white, cs, depth, exp_white
2886 ),
2887 )
2888 .with_location(loc.clone()),
2889 );
2890 }
2891 }
2892 if let Some(range) = cdci.color_range {
2893 if range != exp_range {
2894 issues.push(
2895 ValidationIssue::new(
2896 Severity::Error,
2897 Category::Video,
2898 St2067_21_2023::ColorRange.code().to_string(),
2899 format!(
2900 "ColorRange={} for {} at {}-bit; expected {}",
2901 range, cs, depth, exp_range
2902 ),
2903 )
2904 .with_location(loc.clone()),
2905 );
2906 }
2907 }
2908 }
2909 }
2910 } else if !matches!(cp, ColorPrimaries::Unknown(_))
2911 && !matches!(tc, TransferCharacteristic::Unknown(_))
2912 && !matches!(ce, CodingEquations::Unknown(_))
2913 {
2914 issues.push(
2915 ValidationIssue::new(
2916 Severity::Error,
2917 Category::Video,
2918 St2067_21_2023::ColorSystem.code().to_string(),
2919 format!(
2920 "ColorPrimaries={} + TransferCharacteristic={} + CodingEquations={} \
2921 does not form a recognized Color System",
2922 cp, tc, ce
2923 ),
2924 )
2925 .with_location(loc.clone()),
2926 );
2927 }
2928 }
2929
2930 self.check_hdr_metadata(cdci.transfer_characteristic.as_ref(), cpl, &loc, issues);
2932
2933 self.validate_j2k_sub_descriptor(
2935 cdci.sub_descriptors.as_ref(),
2936 cdci.picture_compression.as_ref(),
2937 &loc,
2938 issues,
2939 );
2940 }
2941
2942 fn check_hdr_metadata(
2948 &self,
2949 tc: Option<&TransferCharacteristic>,
2950 cpl: &CompositionPlaylist,
2951 loc: &Location,
2952 issues: &mut Vec<ValidationIssue>,
2953 ) {
2954 if let Some(TransferCharacteristic::PqSt2084) = tc {
2955 let has_hdr_metadata = cpl
2956 .extension_properties
2957 .as_ref()
2958 .map(|ext| ext.max_cll.is_some() && ext.max_fall.is_some())
2959 .unwrap_or(false);
2960 if !has_hdr_metadata {
2961 issues.push(
2962 ValidationIssue::new(
2963 Severity::Info,
2964 Category::Video,
2965 St2067_21_2023::MaxCLLMaxFALL.code().to_string(),
2966 "MaxCLL and MaxFALL are not present for PQ (ST 2084) content; \
2967 per §7.5 they are optional (0..1 cardinality)",
2968 )
2969 .with_location(loc.clone())
2970 .with_suggestion("Consider adding MaxCLL and MaxFALL to ExtensionProperties"),
2971 );
2972 }
2973 }
2974 }
2975
2976 fn validate_j2k_sub_descriptor(
2980 &self,
2981 sub_descriptors: Option<&crate::cpl::VideoSubDescriptors>,
2982 picture_compression: Option<&crate::cpl::VideoCodec>,
2983 loc: &Location,
2984 issues: &mut Vec<ValidationIssue>,
2985 ) {
2986 let is_j2k = picture_compression
2988 .map(|pc| pc.is_jpeg2000_family())
2989 .unwrap_or(false);
2990 if !is_j2k {
2991 return;
2992 }
2993
2994 let j2k_sub = sub_descriptors.and_then(|sd| sd.jpeg2000_sub_descriptor.as_ref());
2995
2996 let j2k_sub = match j2k_sub {
2997 Some(sub) => sub,
2998 None => {
2999 issues.push(
3002 ValidationIssue::new(
3003 Severity::Warning,
3004 Category::Encoding,
3005 St2067_21_2023::Jpeg2000SubDescriptor.code().to_string(),
3006 "JPEG2000SubDescriptor is missing; Table 14 requires this descriptor for JPEG 2000 picture essence",
3007 )
3008 .with_location(loc.clone())
3009 .with_suggestion("Include JPEG2000SubDescriptor metadata in CPL/mapping when available"),
3010 );
3011 return;
3012 }
3013 };
3014
3015 if j2k_sub.coding_style_default.is_none() {
3017 issues.push(
3018 ValidationIssue::new(
3019 Severity::Error,
3020 Category::Encoding,
3021 St2067_21_2023::CodingStyle.code().to_string(),
3022 "CodingStyleDefault (Coding Style) shall be present (Table 14)",
3023 )
3024 .with_location(loc.clone()),
3025 );
3026 }
3027
3028 if j2k_sub.j2c_layout.is_none() {
3030 issues.push(
3031 ValidationIssue::new(
3032 Severity::Error,
3033 Category::Encoding,
3034 St2067_21_2023::J2CLayout.code().to_string(),
3035 "J2CLayout shall be present (Table 14, §6.5.2)",
3036 )
3037 .with_location(loc.clone()),
3038 );
3039 }
3040
3041 if let Some(rsiz) = j2k_sub.rsiz {
3044 if rsiz & 0x4000 != 0 && j2k_sub.j2k_extended_capabilities.is_none() {
3045 issues.push(
3046 ValidationIssue::new(
3047 Severity::Error,
3048 Category::Encoding,
3049 St2067_21_2023::J2KExtendedCapabilities.code().to_string(),
3050 "J2KExtendedCapabilities shall be present when ISO/IEC 15444-15 coding is used (Table 14)",
3051 )
3052 .with_location(loc.clone()),
3053 );
3054 }
3055 }
3056 }
3057
3058 fn validate_homogeneous_image_essence(
3060 &self,
3061 cpl: &CompositionPlaylist,
3062 issues: &mut Vec<ValidationIssue>,
3063 ) {
3064 let edl = match &cpl.essence_descriptor_list {
3065 Some(edl) => edl,
3066 None => return,
3067 };
3068
3069 let mut color_systems: Vec<ColorSystem> = Vec::new();
3070
3071 for ed in &edl.essence_descriptors {
3072 if let Some(ref cdci) = ed.cdci_descriptor {
3073 if let (Some(cp), Some(tc), Some(ce)) = (
3074 &cdci.color_primaries,
3075 &cdci.transfer_characteristic,
3076 &cdci.coding_equations,
3077 ) {
3078 if let Some(cs) = ColorSystem::from_components(cp, tc, Some(ce)) {
3079 color_systems.push(cs);
3080 }
3081 }
3082 }
3083 if let Some(ref rgba) = ed.rgba_descriptor {
3084 if let (Some(cp), Some(tc)) = (&rgba.color_primaries, &rgba.transfer_characteristic)
3085 {
3086 if let Some(cs) = ColorSystem::from_components(cp, tc, None) {
3087 color_systems.push(cs);
3088 }
3089 }
3090 }
3091 }
3092
3093 if !color_systems.is_empty() {
3094 let first = &color_systems[0];
3095 if !color_systems.iter().all(|cs| cs == first) {
3096 let unique: HashSet<_> = color_systems.iter().collect();
3097 let mut systems: Vec<_> = unique.iter().map(|cs| cs.to_string()).collect();
3098 systems.sort();
3099 issues.push(
3100 ValidationIssue::new(
3101 Severity::Error,
3102 Category::Video,
3103 St2067_21_2023::HomogeneousImageEssence.code(),
3104 format!(
3105 "Heterogeneous image essence: found {} different color systems ({}); \
3106 all image essence in a composition shall use the same color system",
3107 unique.len(),
3108 systems.join(", ")
3109 ),
3110 )
3111 .with_location(Location::new().with_cpl(cpl.id)),
3112 );
3113 }
3114 }
3115 }
3116
3117 fn validate_segment_duration(
3122 &self,
3123 cpl: &CompositionPlaylist,
3124 issues: &mut Vec<ValidationIssue>,
3125 ) {
3126 let edit_rate = match &cpl.edit_rate {
3128 Some(er) if er.numerator > 0 && er.denominator > 0 => er,
3129 _ => return,
3130 };
3131
3132 let edl = match &cpl.essence_descriptor_list {
3134 Some(edl) => edl,
3135 None => return,
3136 };
3137
3138 let mut non_integer_audio = false;
3139 for ed in &edl.essence_descriptors {
3140 if let Some(ref wave) = ed.wave_pcm_descriptor {
3141 if let Some(ref asr) = wave.audio_sample_rate {
3142 let numerator = asr.numerator as u64 * edit_rate.denominator as u64;
3146 let denominator = asr.denominator as u64 * edit_rate.numerator as u64;
3147 if denominator > 0 && !numerator.is_multiple_of(denominator) {
3148 non_integer_audio = true;
3149 break;
3150 }
3151 }
3152 }
3153 }
3154
3155 if !non_integer_audio {
3156 return;
3157 }
3158
3159 for (seg_idx, segment) in cpl.segment_list.segments.iter().enumerate() {
3162 let mut segment_duration: u64 = 0;
3163 for seq in &segment.sequence_list.main_image_sequences {
3164 for resource in &seq.resource_list.resources {
3165 let effective = resource
3166 .source_duration
3167 .unwrap_or(resource.intrinsic_duration);
3168 segment_duration += effective;
3169 }
3170 }
3171
3172 if segment_duration > 0 && !segment_duration.is_multiple_of(5) {
3173 issues.push(
3174 ValidationIssue::new(
3175 Severity::Error,
3176 Category::Timing,
3177 St2067_21_2023::SegmentDurationMultiple.code().to_string(),
3178 format!(
3179 "Segment {} duration ({} edit units) is not a multiple of 5; \
3180 §7.4 requires segment duration to be an integer multiple of \
3181 5/Composition Edit Rate when audio samples per edit unit is non-integer",
3182 seg_idx + 1,
3183 segment_duration,
3184 ),
3185 )
3186 .with_location(
3187 Location::new()
3188 .with_cpl(cpl.id)
3189 .with_segment(seg_idx),
3190 ),
3191 );
3192 }
3193 }
3194 }
3195
3196 pub fn validate_all(
3202 &self,
3203 cpl: &CompositionPlaylist,
3204 allow_ht: bool,
3205 issues: &mut Vec<ValidationIssue>,
3206 ) {
3207 self.validate_image_descriptors(cpl, allow_ht, issues);
3209 self.validate_homogeneous_image_essence(cpl, issues);
3211 self.validate_segment_duration(cpl, issues);
3213 self.validate_audio_quantization(cpl, issues);
3215 self.validate_image_resolution(cpl, issues);
3217 self.validate_image_frame_rate(cpl, issues);
3219 self.validate_audio_sample_rate(cpl, issues);
3221 self.validate_descriptor_completeness(cpl, issues);
3223 self.validate_frame_layout_progressive(cpl, issues);
3225 self.validate_audio_channel_homogeneity(cpl, issues);
3227 self.validate_caption_track_constraints(cpl, issues);
3229 self.validate_content_maturity_rating(cpl, issues);
3231 self.validate_locale_list(cpl, issues);
3233 }
3234}
3235
3236impl ConstraintsValidator for App2E2021 {
3237 fn spec_id(&self) -> &str {
3238 "ST 2067-21:2023 (App2E)"
3239 }
3240
3241 fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
3242 let mut issues = Vec::new();
3243
3244 if let Some(ref ext) = cpl.extension_properties {
3246 if let Some(ref app_id) = ext.application_identification {
3247 if app_id != APP2E_APPLICATION_IDENTIFICATION {
3248 issues.push(
3249 ValidationIssue::new(
3250 Severity::Warning,
3251 Category::Metadata,
3252 St2067_21_2023::AppIdMismatch.code().to_string(),
3253 format!(
3254 "ApplicationIdentification '{}' does not match Table 15 value '{}'",
3255 app_id, APP2E_APPLICATION_IDENTIFICATION
3256 ),
3257 )
3258 .with_location(Location::new().with_cpl(cpl.id)),
3259 );
3260 }
3261 }
3262 }
3263
3264 self.validate_all(
3265 cpl,
3266 true, &mut issues,
3268 );
3269
3270 issues
3271 }
3272}
3273
3274const APP2E_2020_APPLICATION_IDENTIFICATION: &str = "http://www.smpte-ra.org/ns/2067-21/2020";
3280
3281pub struct App2E2020;
3286
3287impl ConstraintsValidator for App2E2020 {
3288 fn spec_id(&self) -> &str {
3289 "ST 2067-21:2020 (App2E)"
3290 }
3291
3292 fn validate_cpl(&self, cpl: &CompositionPlaylist) -> Vec<ValidationIssue> {
3293 let mut issues = Vec::new();
3294
3295 if let Some(ref ext) = cpl.extension_properties {
3297 if let Some(ref app_id) = ext.application_identification {
3298 if app_id != APP2E_2020_APPLICATION_IDENTIFICATION {
3299 issues.push(
3300 ValidationIssue::new(
3301 Severity::Warning,
3302 Category::Metadata,
3303 St2067_21_2020::AppIdMismatch.code().to_string(),
3304 format!(
3305 "ApplicationIdentification '{}' does not match Table 15 value '{}'",
3306 app_id, APP2E_2020_APPLICATION_IDENTIFICATION
3307 ),
3308 )
3309 .with_location(Location::new().with_cpl(cpl.id)),
3310 );
3311 }
3312 }
3313 }
3314
3315 App2E2021.validate_all(
3316 cpl,
3317 false, &mut issues,
3319 );
3320
3321 issues
3322 }
3323}
3324
3325impl App2E2021 {
3326 fn validate_audio_quantization(
3328 &self,
3329 cpl: &CompositionPlaylist,
3330 issues: &mut Vec<ValidationIssue>,
3331 ) {
3332 let edl = match &cpl.essence_descriptor_list {
3333 Some(edl) => edl,
3334 None => return,
3335 };
3336
3337 for ed in &edl.essence_descriptors {
3338 if let Some(ref wave) = ed.wave_pcm_descriptor {
3339 if let Some(qb) = wave.quantization_bits {
3340 if qb != 16 && qb != 24 {
3341 issues.push(
3342 ValidationIssue::new(
3343 Severity::Error,
3344 Category::Audio,
3345 St2067_21_2023::QuantizationBits.code().to_string(),
3346 format!(
3347 "WAVEPCMDescriptor {} has QuantizationBits {} \
3348 but ST 2067-21 §6.5 requires 16 or 24",
3349 ed.id, qb,
3350 ),
3351 )
3352 .with_location(
3353 Location::new()
3354 .with_cpl(cpl.id)
3355 .with_path(format!("EssenceDescriptor/{}", ed.id)),
3356 ),
3357 );
3358 }
3359 }
3360 }
3361 }
3362 }
3363}
3364
3365struct ImageSystemTier {
3372 name: &'static str,
3373 dimensions: &'static [(u32, u32)],
3374}
3375
3376const ALLOWED_FRAME_RATES: &[(u32, u32)] = &[
3380 (24, 1), (24000, 1001), (25, 1), (30, 1), (30000, 1001), (48, 1), (48000, 1001), (50, 1), (60, 1), (60000, 1001), ];
3391
3392const IMAGE_SYSTEM_TIERS: &[ImageSystemTier] = &[
3394 ImageSystemTier {
3395 name: "2K (Table 4)",
3396 dimensions: &[(1920, 1080), (2048, 1080)],
3397 },
3398 ImageSystemTier {
3399 name: "4K (Table 5)",
3400 dimensions: &[(3840, 2160), (4096, 2160)],
3401 },
3402 ImageSystemTier {
3403 name: "8K (Table 6)",
3404 dimensions: &[(7680, 4320)],
3405 },
3406];
3407
3408impl App2E2021 {
3409 fn validate_image_resolution(
3413 &self,
3414 cpl: &CompositionPlaylist,
3415 issues: &mut Vec<ValidationIssue>,
3416 ) {
3417 let edl = match &cpl.essence_descriptor_list {
3418 Some(edl) => edl,
3419 None => return,
3420 };
3421
3422 let all_allowed: Vec<(u32, u32)> = IMAGE_SYSTEM_TIERS
3423 .iter()
3424 .flat_map(|t| t.dimensions.iter().copied())
3425 .collect();
3426
3427 for ed in &edl.essence_descriptors {
3428 let (width, height, desc_type) = if let Some(ref rgba) = ed.rgba_descriptor {
3429 (rgba.stored_width, rgba.stored_height, "RGBA")
3430 } else if let Some(ref cdci) = ed.cdci_descriptor {
3431 (cdci.stored_width, cdci.stored_height, "CDCI")
3432 } else {
3433 continue;
3434 };
3435
3436 let (w, h) = match (width, height) {
3437 (Some(w), Some(h)) => (w, h),
3438 _ => continue, };
3440
3441 if !all_allowed.contains(&(w, h)) {
3442 let tier_names: Vec<&str> = IMAGE_SYSTEM_TIERS.iter().map(|t| t.name).collect();
3443 issues.push(
3444 ValidationIssue::new(
3445 Severity::Error,
3446 Category::Video,
3447 St2067_21_2023::Resolution.code().to_string(),
3448 format!(
3449 "{} descriptor {}: StoredWidth×StoredHeight {}×{} is not an allowed App2E image system dimension (allowed tiers: {})",
3450 desc_type, ed.id, w, h, tier_names.join(", ")
3451 ),
3452 )
3453 .with_location(
3454 Location::new()
3455 .with_cpl(cpl.id)
3456 .with_path(format!("EssenceDescriptor/{}", ed.id)),
3457 ),
3458 );
3459 }
3460 }
3461 }
3462
3463 fn validate_image_frame_rate(
3467 &self,
3468 cpl: &CompositionPlaylist,
3469 issues: &mut Vec<ValidationIssue>,
3470 ) {
3471 let edl = match &cpl.essence_descriptor_list {
3472 Some(edl) => edl,
3473 None => return,
3474 };
3475
3476 for ed in &edl.essence_descriptors {
3477 let (sample_rate, desc_type) = if let Some(ref rgba) = ed.rgba_descriptor {
3478 (rgba.sample_rate.as_ref(), "RGBA")
3479 } else if let Some(ref cdci) = ed.cdci_descriptor {
3480 (cdci.sample_rate.as_ref(), "CDCI")
3481 } else {
3482 continue;
3483 };
3484
3485 let rate = match sample_rate {
3486 Some(r) => r,
3487 None => continue, };
3489
3490 let is_allowed = ALLOWED_FRAME_RATES
3491 .iter()
3492 .any(|&(n, d)| rate.numerator == n && rate.denominator == d);
3493
3494 if !is_allowed {
3495 let allowed_str: Vec<String> = ALLOWED_FRAME_RATES
3496 .iter()
3497 .map(|&(n, d)| {
3498 let fps = n as f64 / d as f64;
3499 format!("{:.3}", fps)
3500 })
3501 .collect();
3502 issues.push(
3503 ValidationIssue::new(
3504 Severity::Error,
3505 Category::Video,
3506 St2067_21_2023::FrameRate.code().to_string(),
3507 format!(
3508 "{} descriptor {}: SampleRate {}/{} ({:.3} fps) is not an allowed App2E frame rate (allowed: {} fps)",
3509 desc_type, ed.id, rate.numerator, rate.denominator,
3510 rate.as_f64(),
3511 allowed_str.join(", ")
3512 ),
3513 )
3514 .with_location(
3515 Location::new()
3516 .with_cpl(cpl.id)
3517 .with_path(format!("EssenceDescriptor/{}", ed.id)),
3518 ),
3519 );
3520 }
3521 }
3522 }
3523
3524 fn validate_audio_sample_rate(
3526 &self,
3527 cpl: &CompositionPlaylist,
3528 issues: &mut Vec<ValidationIssue>,
3529 ) {
3530 let edl = match &cpl.essence_descriptor_list {
3531 Some(edl) => edl,
3532 None => return,
3533 };
3534
3535 for ed in &edl.essence_descriptors {
3536 let wave = match &ed.wave_pcm_descriptor {
3537 Some(w) => w,
3538 None => continue,
3539 };
3540
3541 if let Some(ref rate) = wave.audio_sample_rate {
3542 if rate.numerator != 48000 || rate.denominator != 1 {
3544 issues.push(
3545 ValidationIssue::new(
3546 Severity::Error,
3547 Category::Audio,
3548 St2067_21_2023::AudioSampleRate.code().to_string(),
3549 format!(
3550 "WAVEPCMDescriptor {}: AudioSampleRate {}/{} ({} Hz) is not 48000 Hz",
3551 ed.id, rate.numerator, rate.denominator,
3552 rate.numerator / rate.denominator.max(1)
3553 ),
3554 )
3555 .with_location(
3556 Location::new()
3557 .with_cpl(cpl.id)
3558 .with_path(format!("EssenceDescriptor/{}", ed.id)),
3559 ),
3560 );
3561 }
3562 }
3563 }
3564 }
3565
3566 fn validate_descriptor_completeness(
3572 &self,
3573 cpl: &CompositionPlaylist,
3574 issues: &mut Vec<ValidationIssue>,
3575 ) {
3576 let edl = match &cpl.essence_descriptor_list {
3577 Some(edl) => edl,
3578 None => return,
3579 };
3580
3581 for ed in &edl.essence_descriptors {
3582 let ed_loc = Location::new()
3583 .with_cpl(cpl.id)
3584 .with_path(format!("EssenceDescriptor/{}", ed.id));
3585
3586 if let Some(ref rgba) = ed.rgba_descriptor {
3588 let mut push = |code: &'static str, field: &'static str| {
3589 issues.push(
3590 ValidationIssue::new(
3591 Severity::Error,
3592 Category::Encoding,
3593 code.to_string(),
3594 format!(
3595 "RGBADescriptor {}: required field {} is missing",
3596 ed.id, field
3597 ),
3598 )
3599 .with_location(ed_loc.clone()),
3600 );
3601 };
3602 if rgba.stored_width.is_none() {
3603 push(St2067_21_2023::RequiredStoredWidth.code(), "StoredWidth");
3604 }
3605 if rgba.stored_height.is_none() {
3606 push(St2067_21_2023::RequiredStoredHeight.code(), "StoredHeight");
3607 }
3608 if rgba.sample_rate.is_none() {
3609 push(St2067_21_2023::RequiredSampleRate.code(), "SampleRate");
3610 }
3611 if rgba.frame_layout.is_none() {
3612 push(St2067_21_2023::RequiredFrameLayout.code(), "FrameLayout");
3613 }
3614 if rgba.color_primaries.is_none() {
3615 push(
3616 St2067_21_2023::RequiredColorPrimaries.code(),
3617 "ColorPrimaries",
3618 );
3619 }
3620 if rgba.transfer_characteristic.is_none() {
3621 push(
3622 St2067_21_2023::RequiredTransferCharacteristic.code(),
3623 "TransferCharacteristic",
3624 );
3625 }
3626 if rgba.picture_compression.is_none() {
3627 push(
3628 St2067_21_2023::RequiredPictureCompression.code(),
3629 "PictureCompression",
3630 );
3631 }
3632 }
3633
3634 if let Some(ref cdci) = ed.cdci_descriptor {
3635 let mut push = |code: &'static str, field: &'static str| {
3636 issues.push(
3637 ValidationIssue::new(
3638 Severity::Error,
3639 Category::Encoding,
3640 code.to_string(),
3641 format!(
3642 "CDCIDescriptor {}: required field {} is missing",
3643 ed.id, field
3644 ),
3645 )
3646 .with_location(ed_loc.clone()),
3647 );
3648 };
3649 if cdci.stored_width.is_none() {
3650 push(St2067_21_2023::RequiredStoredWidth.code(), "StoredWidth");
3651 }
3652 if cdci.stored_height.is_none() {
3653 push(St2067_21_2023::RequiredStoredHeight.code(), "StoredHeight");
3654 }
3655 if cdci.sample_rate.is_none() {
3656 push(St2067_21_2023::RequiredSampleRate.code(), "SampleRate");
3657 }
3658 if cdci.frame_layout.is_none() {
3659 push(St2067_21_2023::RequiredFrameLayout.code(), "FrameLayout");
3660 }
3661 if cdci.color_primaries.is_none() {
3662 push(
3663 St2067_21_2023::RequiredColorPrimaries.code(),
3664 "ColorPrimaries",
3665 );
3666 }
3667 if cdci.transfer_characteristic.is_none() {
3668 push(
3669 St2067_21_2023::RequiredTransferCharacteristic.code(),
3670 "TransferCharacteristic",
3671 );
3672 }
3673 if cdci.picture_compression.is_none() {
3674 push(
3675 St2067_21_2023::RequiredPictureCompression.code(),
3676 "PictureCompression",
3677 );
3678 }
3679 if cdci.component_depth.is_none() {
3680 push(
3681 St2067_21_2023::RequiredComponentDepth.code(),
3682 "ComponentDepth",
3683 );
3684 }
3685 }
3686
3687 if let Some(ref wave) = ed.wave_pcm_descriptor {
3688 let mut push = |code: &'static str, field: &'static str| {
3689 issues.push(
3690 ValidationIssue::new(
3691 Severity::Error,
3692 Category::Audio,
3693 code.to_string(),
3694 format!(
3695 "WAVEPCMDescriptor {}: required field {} is missing",
3696 ed.id, field
3697 ),
3698 )
3699 .with_location(ed_loc.clone()),
3700 );
3701 };
3702 if wave.channel_count.is_none() {
3703 push(St2067_21_2023::RequiredChannelCount.code(), "ChannelCount");
3704 }
3705 if wave.quantization_bits.is_none() {
3706 push(
3707 St2067_21_2023::RequiredQuantizationBits.code(),
3708 "QuantizationBits",
3709 );
3710 }
3711 }
3712 }
3713 }
3714
3715 fn validate_frame_layout_progressive(
3720 &self,
3721 cpl: &CompositionPlaylist,
3722 issues: &mut Vec<ValidationIssue>,
3723 ) {
3724 let edl = match &cpl.essence_descriptor_list {
3725 Some(edl) => edl,
3726 None => return,
3727 };
3728
3729 for ed in &edl.essence_descriptors {
3730 let (frame_layout, desc_type) = if let Some(ref rgba) = ed.rgba_descriptor {
3731 (rgba.frame_layout.as_deref(), "RGBA")
3732 } else if let Some(ref cdci) = ed.cdci_descriptor {
3733 (cdci.frame_layout.as_deref(), "CDCI")
3734 } else {
3735 continue;
3736 };
3737
3738 if let Some(fl) = frame_layout {
3739 if fl != "FullFrame" {
3740 issues.push(
3741 ValidationIssue::new(
3742 Severity::Error,
3743 Category::Video,
3744 St2067_21_2023::FrameLayoutInterlaced.code().to_string(),
3745 format!(
3746 "{} descriptor {}: FrameLayout '{}' is not permitted for App2E; \
3747 all image systems (Tables 4-6) shall be FullFrame (progressive)",
3748 desc_type, ed.id, fl,
3749 ),
3750 )
3751 .with_location(
3752 Location::new()
3753 .with_cpl(cpl.id)
3754 .with_path(format!("EssenceDescriptor/{}", ed.id)),
3755 )
3756 .with_suggestion("Set FrameLayout to FullFrame (00h)"),
3757 );
3758 }
3759 }
3760 }
3761 }
3762
3763 #[allow(clippy::ptr_arg)]
3766 fn validate_audio_channel_homogeneity(
3767 &self,
3768 _cpl: &CompositionPlaylist,
3769 _issues: &mut Vec<ValidationIssue>,
3770 ) {
3771 }
3772
3773 fn validate_caption_track_constraints(
3776 &self,
3777 cpl: &CompositionPlaylist,
3778 issues: &mut Vec<ValidationIssue>,
3779 ) {
3780 let ed_map: HashMap<String, bool> = cpl
3782 .essence_descriptor_list
3783 .as_ref()
3784 .map(|edl| {
3785 edl.essence_descriptors
3786 .iter()
3787 .map(|ed| (ed.id.to_string(), ed.dc_timed_text_descriptor.is_some()))
3788 .collect()
3789 })
3790 .unwrap_or_default();
3791
3792 for (seg_idx, segment) in cpl.segment_list.segments.iter().enumerate() {
3794 for seq in &segment.sequence_list.hearing_impaired_captions_sequences {
3795 for (res_idx, resource) in seq.resource_list.resources.iter().enumerate() {
3796 if let Some(ref se) = resource.source_encoding {
3797 let se_str = se.to_string();
3798 if let Some(&has_ttml) = ed_map.get(&se_str) {
3799 if !has_ttml {
3800 issues.push(
3801 ValidationIssue::new(
3802 Severity::Error,
3803 Category::Subtitle,
3804 St2067_21_2025::HICTimedText.code().to_string(),
3805 format!(
3806 "HearingImpairedCaptions resource {} references descriptor '{}' \
3807 which is not a DCTimedTextDescriptor",
3808 res_idx, se_str,
3809 ),
3810 )
3811 .with_location(
3812 Location::new()
3813 .with_cpl(cpl.id)
3814 .with_segment(seg_idx)
3815 .with_resource(res_idx),
3816 ),
3817 );
3818 }
3819 }
3820 }
3821 }
3822 }
3823
3824 for seq in &segment.sequence_list.forced_narrative_sequences {
3826 for (res_idx, resource) in seq.resource_list.resources.iter().enumerate() {
3827 if let Some(ref se) = resource.source_encoding {
3828 let se_str = se.to_string();
3829 if let Some(&has_ttml) = ed_map.get(&se_str) {
3830 if !has_ttml {
3831 issues.push(
3832 ValidationIssue::new(
3833 Severity::Error,
3834 Category::Subtitle,
3835 St2067_21_2025::FNTimedText.code().to_string(),
3836 format!(
3837 "ForcedNarrative resource {} references descriptor '{}' \
3838 which is not a DCTimedTextDescriptor",
3839 res_idx, se_str,
3840 ),
3841 )
3842 .with_location(
3843 Location::new()
3844 .with_cpl(cpl.id)
3845 .with_segment(seg_idx)
3846 .with_resource(res_idx),
3847 ),
3848 );
3849 }
3850 }
3851 }
3852 }
3853 }
3854 }
3855 }
3856
3857 fn validate_content_maturity_rating(
3860 &self,
3861 cpl: &CompositionPlaylist,
3862 issues: &mut Vec<ValidationIssue>,
3863 ) {
3864 if let Some(ref ext) = cpl.extension_properties {
3868 if ext.application_identification.is_none() {
3869 issues.push(
3870 ValidationIssue::new(
3871 Severity::Error,
3872 Category::Metadata,
3873 St2067_21_2023::ApplicationIdentification.code(),
3874 "ApplicationIdentification is required for App2E compositions",
3875 )
3876 .with_location(Location::new().with_cpl(cpl.id)),
3877 );
3878 }
3879 }
3880
3881 if let Some(ref locale_list) = cpl.locale_list {
3883 for (locale_idx, locale) in locale_list.locales.iter().enumerate() {
3884 if let Some(ref cmr_list) = locale.content_maturity_rating_list {
3885 for (rating_idx, rating) in cmr_list.ratings.iter().enumerate() {
3886 if rating.agency.trim().is_empty() {
3888 issues.push(
3889 ValidationIssue::new(
3890 Severity::Error,
3891 Category::Metadata,
3892 St2067_21_2023::ContentMaturityRatingAgency.code(),
3893 format!(
3894 "ContentMaturityRating[{}] in Locale[{}] has empty Agency",
3895 rating_idx, locale_idx
3896 ),
3897 )
3898 .with_location(Location::new().with_cpl(cpl.id)),
3899 );
3900 } else if !is_valid_any_uri(&rating.agency) {
3901 issues.push(
3903 ValidationIssue::new(
3904 Severity::Error,
3905 Category::Metadata,
3906 St2067_21_2023::ContentMaturityRatingAgencyUri.code(),
3907 format!(
3908 "ContentMaturityRating[{}] in Locale[{}] Agency '{}' \
3909 is not a valid xs:anyURI (contains whitespace)",
3910 rating_idx, locale_idx, rating.agency,
3911 ),
3912 )
3913 .with_location(Location::new().with_cpl(cpl.id)),
3914 );
3915 }
3916 }
3917 }
3918 }
3919 }
3920 }
3921
3922 fn validate_locale_list(&self, cpl: &CompositionPlaylist, issues: &mut Vec<ValidationIssue>) {
3927 let locale_list = match &cpl.locale_list {
3928 Some(ll) => ll,
3929 None => return,
3930 };
3931
3932 let cpl_loc = Location::new().with_cpl(cpl.id);
3933
3934 for (i, locale) in locale_list.locales.iter().enumerate() {
3935 if let Some(ref ll) = locale.language_list {
3937 for tag in &ll.languages {
3938 let s = tag.as_str();
3939 if s.is_empty() {
3940 issues.push(
3941 ValidationIssue::new(
3942 Severity::Warning,
3943 Category::Metadata,
3944 St2067_21_2023::EmptyLanguageTag.code().to_string(),
3945 format!("Locale[{}]: empty language tag in LanguageList", i),
3946 )
3947 .with_location(cpl_loc.clone()),
3948 );
3949 } else if !s.chars().next().unwrap_or(' ').is_ascii_alphabetic() {
3950 issues.push(
3951 ValidationIssue::new(
3952 Severity::Warning,
3953 Category::Metadata,
3954 St2067_21_2023::MalformedLanguageTag.code().to_string(),
3955 format!(
3956 "Locale[{}]: language tag '{}' does not start with an ASCII letter (RFC 5646)",
3957 i, s,
3958 ),
3959 )
3960 .with_location(cpl_loc.clone()),
3961 );
3962 }
3963 }
3964 }
3965
3966 if let Some(ref rl) = locale.region_list {
3968 for region in &rl.regions {
3969 let is_alpha2 =
3970 region.len() == 2 && region.chars().all(|c| c.is_ascii_uppercase());
3971 let is_un_m49 = region.len() == 3 && region.chars().all(|c| c.is_ascii_digit());
3972 if !is_alpha2 && !is_un_m49 {
3973 issues.push(
3974 ValidationIssue::new(
3975 Severity::Warning,
3976 Category::Metadata,
3977 St2067_21_2023::RegionCode.code().to_string(),
3978 format!(
3979 "Locale[{}]: region '{}' is not a valid BCP 47 region subtag (expected 2-letter ISO 3166-1 or 3-digit UN M.49)",
3980 i, region,
3981 ),
3982 )
3983 .with_location(cpl_loc.clone()),
3984 );
3985 }
3986 }
3987 }
3988 }
3989 }
3990}
3991
3992#[cfg(test)]
3999mod spec_target_tests {
4000 use super::*;
4001
4002 #[test]
4003 fn core_spec_target_from_str_valid() {
4004 assert_eq!(
4005 "v2013".parse::<CoreSpecTarget>().unwrap(),
4006 CoreSpecTarget::St2067_2_2013
4007 );
4008 assert_eq!(
4009 "v2016".parse::<CoreSpecTarget>().unwrap(),
4010 CoreSpecTarget::St2067_2_2016
4011 );
4012 assert_eq!(
4013 "v2020".parse::<CoreSpecTarget>().unwrap(),
4014 CoreSpecTarget::St2067_2_2020
4015 );
4016 }
4017
4018 #[test]
4019 fn core_spec_target_from_str_invalid() {
4020 assert!("v2099".parse::<CoreSpecTarget>().is_err());
4021 assert!("auto".parse::<CoreSpecTarget>().is_err());
4022 assert!("".parse::<CoreSpecTarget>().is_err());
4023 }
4024
4025 #[test]
4026 fn app_spec_target_from_str_valid() {
4027 assert_eq!(
4028 "v2020".parse::<AppSpecTarget>().unwrap(),
4029 AppSpecTarget::St2067_21_2020
4030 );
4031 assert_eq!(
4032 "v2021".parse::<AppSpecTarget>().unwrap(),
4033 AppSpecTarget::St2067_21_2021
4034 );
4035 assert_eq!(
4036 "v2023".parse::<AppSpecTarget>().unwrap(),
4037 AppSpecTarget::St2067_21_2023
4038 );
4039 }
4040
4041 #[test]
4042 fn app_spec_target_from_str_invalid() {
4043 assert!("none".parse::<AppSpecTarget>().is_err());
4044 assert!("garbage".parse::<AppSpecTarget>().is_err());
4045 }
4046
4047 #[test]
4048 fn parse_core_spec_target_auto() {
4049 assert_eq!(parse_core_spec_target("auto").unwrap(), None);
4050 }
4051
4052 #[test]
4053 fn parse_core_spec_target_specific() {
4054 assert_eq!(
4055 parse_core_spec_target("v2020").unwrap(),
4056 Some(CoreSpecTarget::St2067_2_2020),
4057 );
4058 }
4059
4060 #[test]
4061 fn parse_app_spec_targets_auto() {
4062 assert_eq!(parse_app_spec_targets("auto").unwrap(), None);
4063 }
4064
4065 #[test]
4066 fn parse_app_spec_targets_none() {
4067 assert_eq!(parse_app_spec_targets("none").unwrap(), Some(vec![]));
4068 }
4069
4070 #[test]
4071 fn parse_app_spec_targets_specific() {
4072 assert_eq!(
4073 parse_app_spec_targets("v2023").unwrap(),
4074 Some(vec![AppSpecTarget::St2067_21_2023]),
4075 );
4076 }
4077
4078 #[test]
4079 fn parse_app_spec_targets_invalid() {
4080 assert!(parse_app_spec_targets("garbage").is_err());
4081 }
4082}
4083
4084#[cfg(test)]
4085mod tests {
4086 use super::*;
4087 use crate::assetmap::ImfUuid;
4088 use crate::cpl::{
4089 CDCIDescriptor, CompositionTimecode, ContentKindElement, ContentMaturityRating,
4090 ContentMaturityRatingList, ContentVersion, ContentVersionList, DCTimedTextDescriptor,
4091 EssenceDescriptor, EssenceDescriptorList, ExtensionProperties, ForcedNarrativeSequence,
4092 HearingImpairedCaptionsSequence, LanguageList, LanguageString, Locale, LocaleList,
4093 MainAudioSequence, MainImageSequence, MarkerInfo, MarkerLabelElement, MarkerSequence,
4094 RGBADescriptor, RegionList, Resource, ResourceList, Segment, SegmentList, SequenceList,
4095 SubtitlesSequence, WAVEPCMDescriptor,
4096 };
4097 use crate::cpl::{ContentKind, EditRate, LanguageTag, MarkerLabel, VideoCodec};
4098 use crate::diagnostics::codes::ValidationCode;
4099 use crate::validation::codes::St2067_21_2023;
4100
4101 fn uuid(n: u8) -> ImfUuid {
4104 ImfUuid::parse(&format!("00000000-0000-0000-0000-{:012}", n)).unwrap()
4105 }
4106
4107 fn empty_sequence_list() -> SequenceList {
4108 SequenceList {
4109 marker_sequences: vec![],
4110 main_image_sequences: vec![],
4111 main_audio_sequences: vec![],
4112 subtitles_sequences: vec![],
4113 hearing_impaired_captions_sequences: vec![],
4114 forced_narrative_sequences: vec![],
4115 iab_sequences: vec![],
4116 isxd_sequences: vec![],
4117 }
4118 }
4119
4120 fn make_resource(source_encoding: Option<ImfUuid>) -> Resource {
4121 Resource {
4122 id: uuid(99),
4123 annotation: None,
4124 edit_rate: None,
4125 intrinsic_duration: 100,
4126 entry_point: None,
4127 source_duration: None,
4128 source_encoding,
4129 track_file_id: Some(uuid(50)),
4130 repeat_count: None,
4131 key_id: None,
4132 hash: None,
4133 markers: vec![],
4134 }
4135 }
4136
4137 fn minimal_cpl() -> CompositionPlaylist {
4138 CompositionPlaylist {
4139 namespace: CplNamespace::Smpte2067_3_2016,
4140 id: uuid(1),
4141 annotation: None,
4142 issue_date: "2024-01-01T00:00:00Z".to_string(),
4143 issuer: None,
4144 creator: None,
4145 content_originator: None,
4146 content_title: LanguageString {
4147 text: "Test".to_string(),
4148 language: Some(LanguageTag::new("en")),
4149 },
4150 content_kind: ContentKindElement::from(ContentKind::Feature),
4151 content_version_list: None,
4152 locale_list: None,
4153 essence_descriptor_list: None,
4154 edit_rate: None,
4155 total_running_time: None,
4156 extension_properties: None,
4157 composition_timecode: None,
4158 segment_list: SegmentList {
4159 segments: vec![Segment {
4160 id: uuid(2),
4161 sequence_list: empty_sequence_list(),
4162 }],
4163 },
4164 has_signer: false,
4165 has_signature: false,
4166 source_xml: None,
4167 }
4168 }
4169
4170 fn cpl_with_cdci_descriptor(
4171 primaries: ColorPrimaries,
4172 transfer: TransferCharacteristic,
4173 coding_eq: CodingEquations,
4174 depth: u32,
4175 ) -> CompositionPlaylist {
4176 let ed_id = uuid(10);
4177 let mut cpl = minimal_cpl();
4178 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
4179 essence_descriptors: vec![EssenceDescriptor {
4180 id: ed_id,
4181 rgba_descriptor: None,
4182 cdci_descriptor: Some(CDCIDescriptor {
4183 instance_id: None,
4184 stored_width: Some(1920),
4185 stored_height: Some(1080),
4186 display_width: Some(1920),
4187 display_height: Some(1080),
4188 sample_rate: Some(EditRate::new(24, 1)),
4189 image_aspect_ratio: None,
4190 color_primaries: Some(primaries),
4191 transfer_characteristic: Some(transfer),
4192 coding_equations: Some(coding_eq),
4193 picture_compression: Some(VideoCodec::Jpeg2000),
4194 component_depth: Some(depth),
4195 frame_layout: Some("FullFrame".to_string()),
4196 display_f2_offset: None,
4197 horizontal_subsampling: Some(2),
4198 vertical_subsampling: Some(1),
4199 color_siting: Some(0),
4200 black_ref_level: Some(64),
4201 white_ref_level: Some(940),
4202 color_range: Some(897),
4203 stored_f2_offset: None,
4204 sampled_width: None,
4205 sampled_height: None,
4206 sampled_x_offset: None,
4207 sampled_y_offset: None,
4208 alpha_transparency: None,
4209 image_alignment_offset: None,
4210 image_start_offset: None,
4211 image_end_offset: None,
4212 field_dominance: None,
4213 reversed_byte_order: None,
4214 padding_bits: None,
4215 alpha_sample_depth: None,
4216 linked_track_id: None,
4217 active_width: None,
4218 active_height: None,
4219 sub_descriptors: None,
4220 }),
4221 wave_pcm_descriptor: None,
4222 dc_timed_text_descriptor: None,
4223 iab_essence_descriptor: None,
4224 isxd_data_essence_descriptor: None,
4225 }],
4226 });
4227 let mut sl = empty_sequence_list();
4228 sl.main_image_sequences.push(MainImageSequence {
4229 id: uuid(3),
4230 track_id: uuid(4),
4231 resource_list: ResourceList {
4232 resources: vec![make_resource(Some(ed_id))],
4233 },
4234 });
4235 sl.main_audio_sequences.push(MainAudioSequence {
4236 id: uuid(5),
4237 track_id: uuid(6),
4238 resource_list: ResourceList {
4239 resources: vec![make_resource(Some(uuid(11)))],
4240 },
4241 });
4242 cpl.segment_list.segments[0].sequence_list = sl;
4243 cpl.essence_descriptor_list
4244 .as_mut()
4245 .unwrap()
4246 .essence_descriptors
4247 .push(EssenceDescriptor {
4248 id: uuid(11),
4249 rgba_descriptor: None,
4250 cdci_descriptor: None,
4251 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
4252 instance_id: None,
4253 sample_rate: None,
4254 audio_sample_rate: None,
4255 channel_count: Some(6),
4256 quantization_bits: Some(24),
4257 linked_track_id: None,
4258 sub_descriptors: None,
4259 }),
4260 dc_timed_text_descriptor: None,
4261 iab_essence_descriptor: None,
4262 isxd_data_essence_descriptor: None,
4263 });
4264 cpl
4265 }
4266
4267 fn cpl_with_rgba_descriptor(
4268 primaries: ColorPrimaries,
4269 transfer: TransferCharacteristic,
4270 ) -> CompositionPlaylist {
4271 let ed_id = uuid(10);
4272 let mut cpl = minimal_cpl();
4273 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
4274 essence_descriptors: vec![EssenceDescriptor {
4275 id: ed_id,
4276 rgba_descriptor: Some(RGBADescriptor {
4277 instance_id: None,
4278 display_width: Some(1920),
4279 display_height: Some(1080),
4280 stored_width: Some(1920),
4281 stored_height: Some(1080),
4282 sample_rate: Some(EditRate::new(24, 1)),
4283 image_aspect_ratio: None,
4284 color_primaries: Some(primaries),
4285 transfer_characteristic: Some(transfer),
4286 coding_equations: None,
4287 picture_compression: Some(VideoCodec::Jpeg2000Ht),
4288 frame_layout: Some("FullFrame".to_string()),
4289 display_f2_offset: None,
4290 component_max_ref: Some(1023),
4291 component_min_ref: Some(0),
4292 scanning_direction: Some(
4293 "ScanningDirection_LeftToRightTopToBottom".to_string(),
4294 ),
4295 stored_f2_offset: None,
4296 sampled_width: None,
4297 sampled_height: None,
4298 sampled_x_offset: None,
4299 sampled_y_offset: None,
4300 alpha_transparency: None,
4301 image_alignment_offset: None,
4302 image_start_offset: None,
4303 image_end_offset: None,
4304 field_dominance: None,
4305 alpha_max_ref: None,
4306 alpha_min_ref: None,
4307 palette: None,
4308 palette_layout: None,
4309 linked_track_id: None,
4310 sub_descriptors: None,
4311 }),
4312 cdci_descriptor: None,
4313 wave_pcm_descriptor: None,
4314 dc_timed_text_descriptor: None,
4315 iab_essence_descriptor: None,
4316 isxd_data_essence_descriptor: None,
4317 }],
4318 });
4319 let mut sl = empty_sequence_list();
4320 sl.main_image_sequences.push(MainImageSequence {
4321 id: uuid(3),
4322 track_id: uuid(4),
4323 resource_list: ResourceList {
4324 resources: vec![make_resource(Some(ed_id))],
4325 },
4326 });
4327 cpl.segment_list.segments[0].sequence_list = sl;
4328 cpl
4329 }
4330
4331 #[test]
4334 fn factory_returns_core_2020_for_namespace() {
4335 let v = get_validator("http://www.smpte-ra.org/ns/2067-2/2020");
4336 assert!(v.is_some());
4337 assert_eq!(v.unwrap().spec_id(), "ST 2067-2:2020");
4338 }
4339
4340 #[test]
4341 fn factory_returns_core_2016_for_namespace() {
4342 let v = get_validator("http://www.smpte-ra.org/schemas/2067-2/2016");
4343 assert!(v.is_some());
4344 assert_eq!(v.unwrap().spec_id(), "ST 2067-2:2016");
4345 }
4346
4347 #[test]
4348 fn factory_returns_core_2013_for_namespace() {
4349 let v = get_validator("http://www.smpte-ra.org/schemas/2067-2/2013");
4350 assert!(v.is_some());
4351 assert_eq!(v.unwrap().spec_id(), "ST 2067-2:2013");
4352 }
4353
4354 #[test]
4355 fn factory_returns_app2e_for_2021_namespace() {
4356 let v = get_validator("http://www.smpte-ra.org/ns/2067-21/2021");
4357 assert!(v.is_some());
4358 assert_eq!(v.unwrap().spec_id(), "ST 2067-21:2023 (App2E)");
4359 }
4360
4361 #[test]
4362 fn factory_returns_app2e_for_2023_namespace() {
4363 let v = get_validator("http://www.smpte-ra.org/ns/2067-21/2023");
4364 assert!(v.is_some());
4365 assert_eq!(v.unwrap().spec_id(), "ST 2067-21:2023 (App2E)");
4366 }
4367
4368 #[test]
4369 fn factory_returns_none_for_unknown() {
4370 assert!(get_validator("http://example.com/unknown").is_none());
4371 }
4372
4373 #[test]
4374 fn registry_resolves_core_namespace() {
4375 let registry = BuiltinValidatorRegistry;
4376 let v = registry.resolve_namespace("http://www.smpte-ra.org/ns/2067-2/2020");
4377 assert!(v.is_some());
4378 assert_eq!(v.unwrap().spec_id(), "ST 2067-2:2020");
4379 }
4380
4381 #[test]
4382 fn registry_returns_none_for_unknown_namespace() {
4383 let registry = BuiltinValidatorRegistry;
4384 assert!(registry
4385 .resolve_namespace("http://example.com/unknown")
4386 .is_none());
4387 }
4388
4389 #[test]
4394 fn get_validators_for_cpl_returns_core_plus_app2e() {
4395 let mut cpl = minimal_cpl();
4396 cpl.extension_properties = Some(ExtensionProperties {
4397 application_identification: Some("http://www.smpte-ra.org/ns/2067-21/2021".to_string()),
4398 max_cll: None,
4399 max_fall: None,
4400 });
4401 let validators = get_validators_for_cpl(&cpl);
4402 assert_eq!(validators.len(), 2);
4403 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4404 assert_eq!(validators[1].spec_id(), "ST 2067-21:2023 (App2E)");
4405 }
4406
4407 #[test]
4408 fn registry_resolve_for_cpl_handles_multiple_app_uris() {
4409 let mut cpl = minimal_cpl();
4410 cpl.extension_properties = Some(ExtensionProperties {
4411 application_identification: Some(
4412 "http://www.smpte-ra.org/ns/2067-21/2021 http://example.com/unknown".to_string(),
4413 ),
4414 max_cll: None,
4415 max_fall: None,
4416 });
4417 let registry = BuiltinValidatorRegistry;
4418 let validators = registry.resolve_for_cpl(&cpl);
4419 assert_eq!(validators.len(), 2);
4420 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4421 assert_eq!(validators[1].spec_id(), "ST 2067-21:2023 (App2E)");
4422 }
4423
4424 #[test]
4425 fn registry_and_factory_have_same_resolution() {
4426 let mut cpl = minimal_cpl();
4427 cpl.extension_properties = Some(ExtensionProperties {
4428 application_identification: Some("http://www.smpte-ra.org/ns/2067-21/2021".to_string()),
4429 max_cll: None,
4430 max_fall: None,
4431 });
4432
4433 let via_factory = get_validators_for_cpl(&cpl);
4434 let registry = BuiltinValidatorRegistry;
4435 let via_registry = registry.resolve_for_cpl(&cpl);
4436
4437 let factory_ids: Vec<_> = via_factory
4438 .iter()
4439 .map(|v| v.spec_id().to_string())
4440 .collect();
4441 let registry_ids: Vec<_> = via_registry
4442 .iter()
4443 .map(|v| v.spec_id().to_string())
4444 .collect();
4445 assert_eq!(factory_ids, registry_ids);
4446 }
4447
4448 #[test]
4449 fn configurable_registry_overrides_core_namespace_version() {
4450 let cpl = minimal_cpl();
4451 let registry = ConfigurableValidatorRegistry::new(ValidatorSelection {
4452 core_spec: Some(CoreSpecTarget::St2067_2_2016),
4453 ..Default::default()
4454 });
4455
4456 let validators = registry.resolve_for_cpl(&cpl);
4457 assert_eq!(validators.len(), 1);
4458 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4459 }
4460
4461 #[test]
4462 fn configurable_registry_overrides_application_identification_uris() {
4463 let cpl = minimal_cpl();
4464 let registry = ConfigurableValidatorRegistry::new(ValidatorSelection {
4465 app_specs: Some(vec![AppSpecTarget::St2067_21_2021]),
4466 ..Default::default()
4467 });
4468
4469 let validators = registry.resolve_for_cpl(&cpl);
4470 assert_eq!(validators.len(), 2);
4471 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4472 assert_eq!(validators[1].spec_id(), "ST 2067-21:2023 (App2E)");
4473 }
4474
4475 #[test]
4476 fn validate_cpl_with_registry_matches_manual_merge() {
4477 let mut cpl = minimal_cpl();
4478 cpl.extension_properties = Some(ExtensionProperties {
4479 application_identification: Some("http://www.smpte-ra.org/ns/2067-21/2021".to_string()),
4480 max_cll: None,
4481 max_fall: None,
4482 });
4483
4484 let registry = BuiltinValidatorRegistry;
4485 let via_helper = validate_cpl_with_registry(&cpl, ®istry);
4486
4487 let validators = registry.resolve_for_cpl(&cpl);
4488 let mut manual = Vec::new();
4489 for v in &validators {
4490 manual.extend(v.validate_cpl(&cpl));
4491 }
4492
4493 assert_eq!(via_helper.len(), manual.len());
4494 }
4495
4496 #[test]
4499 fn get_validators_for_cpl_returns_core_2016_for_2016_namespace() {
4500 let mut cpl = minimal_cpl();
4501 cpl.namespace = CplNamespace::Smpte2067_3_2016;
4502 let validators = get_validators_for_cpl(&cpl);
4503 assert_eq!(validators.len(), 1);
4504 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4505 }
4506
4507 #[test]
4508 fn get_validators_for_cpl_returns_core_2013_for_2013_namespace() {
4509 let mut cpl = minimal_cpl();
4510 cpl.namespace = CplNamespace::Smpte2067_3_2013;
4511 let validators = get_validators_for_cpl(&cpl);
4512 assert_eq!(validators.len(), 1);
4513 assert_eq!(validators[0].spec_id(), "ST 2067-2:2013");
4514 }
4515
4516 #[test]
4517 fn get_validators_for_cpl_returns_empty_for_dci_legacy_namespace() {
4518 let mut cpl = minimal_cpl();
4520 cpl.namespace = CplNamespace::Dci429_7;
4521 let validators = get_validators_for_cpl(&cpl);
4522 assert_eq!(
4523 validators.len(),
4524 0,
4525 "DCI namespace should yield no validators"
4526 );
4527 }
4528
4529 #[test]
4530 fn get_validators_for_cpl_auto_detects_old_style_app2e_2016_namespace() {
4531 let mut cpl = minimal_cpl();
4533 cpl.extension_properties = Some(ExtensionProperties {
4534 application_identification: Some(
4535 "http://www.smpte-ra.org/schemas/2067-21/2016".to_string(),
4536 ),
4537 max_cll: None,
4538 max_fall: None,
4539 });
4540 let validators = get_validators_for_cpl(&cpl);
4541 assert_eq!(validators.len(), 2);
4542 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4543 assert_eq!(validators[1].spec_id(), "ST 2067-21:2023 (App2E)");
4544 }
4545
4546 #[test]
4547 fn configurable_registry_empty_app_specs_suppresses_app_profile() {
4548 let mut cpl = minimal_cpl();
4550 cpl.extension_properties = Some(ExtensionProperties {
4551 application_identification: Some("http://www.smpte-ra.org/ns/2067-21/2021".to_string()),
4552 max_cll: None,
4553 max_fall: None,
4554 });
4555 let registry = ConfigurableValidatorRegistry::new(ValidatorSelection {
4556 app_specs: Some(vec![]),
4557 ..Default::default()
4558 });
4559 let validators = registry.resolve_for_cpl(&cpl);
4560 assert_eq!(
4561 validators.len(),
4562 1,
4563 "empty app_specs should suppress app profile"
4564 );
4565 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4566 }
4567
4568 #[test]
4569 fn configurable_registry_raw_string_core_uri_override() {
4570 let mut cpl = minimal_cpl();
4572 cpl.namespace = CplNamespace::Smpte2067_3_2016;
4573 let registry = ConfigurableValidatorRegistry::new(ValidatorSelection {
4574 core_namespace_uri: Some("http://www.smpte-ra.org/schemas/2067-2/2016".to_string()),
4575 ..Default::default()
4576 });
4577 let validators = registry.resolve_for_cpl(&cpl);
4578 assert_eq!(validators.len(), 1);
4579 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4580 }
4581
4582 #[test]
4583 fn configurable_registry_raw_string_app_uri_override() {
4584 let cpl = minimal_cpl();
4585 let registry = ConfigurableValidatorRegistry::new(ValidatorSelection {
4586 application_identification_uris: Some(vec![
4587 "http://www.smpte-ra.org/ns/2067-21/2021".to_string()
4588 ]),
4589 ..Default::default()
4590 });
4591 let validators = registry.resolve_for_cpl(&cpl);
4592 assert_eq!(validators.len(), 2);
4593 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
4594 assert_eq!(validators[1].spec_id(), "ST 2067-21:2023 (App2E)");
4595 }
4596
4597 #[test]
4600 fn color_system_color1_bt601_625() {
4601 let cs = ColorSystem::from_components(
4602 &ColorPrimaries::Bt601_625,
4603 &TransferCharacteristic::Bt709,
4604 Some(&CodingEquations::Bt601),
4605 );
4606 assert_eq!(cs, Some(ColorSystem::Color1));
4607 assert!(!cs.unwrap().is_hdr());
4608 }
4609
4610 #[test]
4611 fn color_system_color2_bt601_525() {
4612 let cs = ColorSystem::from_components(
4613 &ColorPrimaries::Bt601_525,
4614 &TransferCharacteristic::Bt709,
4615 Some(&CodingEquations::Bt601),
4616 );
4617 assert_eq!(cs, Some(ColorSystem::Color2));
4618 }
4619
4620 #[test]
4621 fn color_system_color3_bt709() {
4622 let cs = ColorSystem::from_components(
4623 &ColorPrimaries::Bt709,
4624 &TransferCharacteristic::Bt709,
4625 Some(&CodingEquations::Bt709),
4626 );
4627 assert_eq!(cs, Some(ColorSystem::Color3));
4628 assert!(!cs.unwrap().is_hdr());
4629 }
4630
4631 #[test]
4632 fn color_system_color4_xvycc() {
4633 let cs = ColorSystem::from_components(
4634 &ColorPrimaries::Bt709,
4635 &TransferCharacteristic::XvYcc709,
4636 Some(&CodingEquations::Bt709),
4637 );
4638 assert_eq!(cs, Some(ColorSystem::Color4));
4639 }
4640
4641 #[test]
4642 fn color_system_color5_bt2020_sdr() {
4643 let cs = ColorSystem::from_components(
4644 &ColorPrimaries::Bt2020,
4645 &TransferCharacteristic::Bt2020,
4646 Some(&CodingEquations::Bt2020Ncl),
4647 );
4648 assert_eq!(cs, Some(ColorSystem::Color5));
4649 assert!(!cs.unwrap().is_hdr());
4650 }
4651
4652 #[test]
4653 fn color_system_color6_p3_pq_rgb() {
4654 let cs = ColorSystem::from_components(
4655 &ColorPrimaries::P3D65,
4656 &TransferCharacteristic::PqSt2084,
4657 None,
4658 );
4659 assert_eq!(cs, Some(ColorSystem::Color6));
4660 assert!(cs.unwrap().is_hdr());
4661 assert!(cs.unwrap().requires_hdr_metadata());
4662 }
4663
4664 #[test]
4665 fn color_system_color7_bt2020_pq() {
4666 let cs = ColorSystem::from_components(
4667 &ColorPrimaries::Bt2020,
4668 &TransferCharacteristic::PqSt2084,
4669 Some(&CodingEquations::Bt2020Ncl),
4670 );
4671 assert_eq!(cs, Some(ColorSystem::Color7));
4672 assert!(cs.unwrap().is_hdr());
4673 assert!(cs.unwrap().requires_hdr_metadata());
4674 }
4675
4676 #[test]
4677 fn color_system_color8_hlg() {
4678 let cs = ColorSystem::from_components(
4679 &ColorPrimaries::Bt2020,
4680 &TransferCharacteristic::Hlg,
4681 Some(&CodingEquations::Bt2020Ncl),
4682 );
4683 assert_eq!(cs, Some(ColorSystem::Color8));
4684 assert!(cs.unwrap().is_hdr());
4685 assert!(!cs.unwrap().requires_hdr_metadata());
4686 }
4687
4688 #[test]
4689 fn color_system_invalid_combination_returns_none() {
4690 let cs = ColorSystem::from_components(
4692 &ColorPrimaries::Bt709,
4693 &TransferCharacteristic::PqSt2084,
4694 Some(&CodingEquations::Bt601),
4695 );
4696 assert_eq!(cs, None);
4697 }
4698
4699 #[test]
4702 fn core_2020_requires_essence_descriptor_list() {
4703 let cpl = minimal_cpl(); let v = CoreConstraints2020;
4705 let issues = v.validate_cpl(&cpl);
4706 let edl_issues: Vec<_> = issues
4707 .iter()
4708 .filter(|i| i.code.contains("EssenceDescriptorList"))
4709 .collect();
4710 assert!(
4711 !edl_issues.is_empty(),
4712 "Should flag missing EssenceDescriptorList"
4713 );
4714 }
4715
4716 #[test]
4717 fn core_2013_allows_missing_essence_descriptor_list() {
4718 let mut cpl = minimal_cpl();
4719 cpl.namespace = CplNamespace::Smpte2067_3_2013;
4720 cpl.segment_list.segments[0]
4722 .sequence_list
4723 .main_image_sequences
4724 .push(MainImageSequence {
4725 id: uuid(3),
4726 track_id: uuid(4),
4727 resource_list: ResourceList {
4728 resources: vec![make_resource(None)],
4729 },
4730 });
4731 let v = CoreConstraints2013;
4732 let issues = v.validate_cpl(&cpl);
4733 let edl_issues: Vec<_> = issues
4734 .iter()
4735 .filter(|i| i.code.contains("EssenceDescriptorList"))
4736 .collect();
4737 assert!(
4738 edl_issues.is_empty(),
4739 "2013 should not require EssenceDescriptorList"
4740 );
4741 }
4742
4743 #[test]
4749 #[ignore = "ContentTitle non-empty is not XSD-expressible; needs semantic check if desired"]
4750 fn core_flags_empty_content_title() {
4751 let mut cpl = minimal_cpl();
4752 cpl.content_title.text = "".to_string();
4753 let v = CoreConstraints2020;
4754 let issues = v.validate_cpl(&cpl);
4755 assert!(
4756 issues.iter().any(|i| i.code.contains("ContentTitle")),
4757 "Should flag empty ContentTitle"
4758 );
4759 }
4760
4761 #[test]
4765 fn core_flags_empty_segment_list() {
4766 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
4767 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
4768 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
4769 <ContentTitle>Test</ContentTitle>
4770 <EditRate>24 1</EditRate>
4771 <SegmentList/>
4772 </CompositionPlaylist>"#;
4773 let cpl = crate::cpl::parse_cpl(xml).expect("should parse despite empty SegmentList");
4774 let issues = CoreConstraints2013.validate_cpl(&cpl);
4775 assert!(
4776 issues.iter().any(|i| i.code.contains("Segment")),
4777 "Empty SegmentList should be flagged: {:#?}",
4778 issues,
4779 );
4780 }
4781
4782 #[test]
4788 #[ignore = "Empty SequenceList is schema-valid (xs:any minOccurs=0); needs semantic check"]
4789 fn core_flags_segment_with_no_sequences() {
4790 let cpl = minimal_cpl(); let v = CoreConstraints2020;
4792 let issues = v.validate_cpl(&cpl);
4793 assert!(
4794 issues.iter().any(|i| i.code.contains("Segment")),
4795 "Should flag segment with no sequences"
4796 );
4797 }
4798
4799 #[test]
4800 fn core_2020_flags_unresolved_source_encoding() {
4801 let mut cpl = cpl_with_cdci_descriptor(
4802 ColorPrimaries::Bt709,
4803 TransferCharacteristic::Bt709,
4804 CodingEquations::Bt709,
4805 10,
4806 );
4807 cpl.essence_descriptor_list
4809 .as_mut()
4810 .unwrap()
4811 .essence_descriptors
4812 .retain(|ed| ed.wave_pcm_descriptor.is_none());
4813 let v = CoreConstraints2020;
4814 let issues = v.validate_cpl(&cpl);
4815 assert!(
4816 issues.iter().any(|i| i.code.contains("SourceEncoding")),
4817 "Should flag unresolved SourceEncoding"
4818 );
4819 }
4820
4821 #[test]
4824 fn app2e_validates_valid_color3_hd() {
4825 let cpl = cpl_with_cdci_descriptor(
4826 ColorPrimaries::Bt709,
4827 TransferCharacteristic::Bt709,
4828 CodingEquations::Bt709,
4829 10,
4830 );
4831 let v = App2E2021;
4832 let issues = v.validate_cpl(&cpl);
4833 let errors: Vec<_> = issues
4835 .iter()
4836 .filter(|i| i.severity == Severity::Error || i.severity == Severity::Critical)
4837 .collect();
4838 assert!(
4839 errors.is_empty(),
4840 "Valid COLOR.3 HD should pass App2E: {:?}",
4841 errors
4842 );
4843 }
4844
4845 #[test]
4846 fn app2e_flags_invalid_color_system() {
4847 let cpl = cpl_with_cdci_descriptor(
4849 ColorPrimaries::Bt709,
4850 TransferCharacteristic::PqSt2084,
4851 CodingEquations::Bt601,
4852 10,
4853 );
4854 let v = App2E2021;
4855 let issues = v.validate_cpl(&cpl);
4856 assert!(
4857 issues.iter().any(|i| i.code.contains("6.2/ColorSystem")),
4858 "Should flag invalid color system combination"
4859 );
4860 }
4861
4862 #[test]
4863 fn app2e_flags_non_j2k_codec() {
4864 let mut cpl = cpl_with_cdci_descriptor(
4865 ColorPrimaries::Bt709,
4866 TransferCharacteristic::Bt709,
4867 CodingEquations::Bt709,
4868 10,
4869 );
4870 if let Some(ref mut edl) = cpl.essence_descriptor_list {
4872 for ed in &mut edl.essence_descriptors {
4873 if let Some(ref mut cdci) = ed.cdci_descriptor {
4874 cdci.picture_compression = Some(VideoCodec::H265);
4875 }
4876 }
4877 }
4878 let v = App2E2021;
4879 let issues = v.validate_cpl(&cpl);
4880 assert!(
4881 issues.iter().any(|i| i.code.contains("6.2.5")),
4882 "Should flag non-JPEG-2000 codec"
4883 );
4884 }
4885
4886 #[test]
4887 fn app2e_flags_invalid_bit_depth() {
4888 let cpl = cpl_with_cdci_descriptor(
4889 ColorPrimaries::Bt709,
4890 TransferCharacteristic::Bt709,
4891 CodingEquations::Bt709,
4892 14, );
4894 let v = App2E2021;
4895 let issues = v.validate_cpl(&cpl);
4896 assert!(
4897 issues.iter().any(|i| i.code.contains("6.4/ComponentDepth")),
4898 "Should flag invalid bit depth"
4899 );
4900 }
4901
4902 #[test]
4903 fn app2e_notes_missing_hdr_metadata_for_pq() {
4904 let cpl = cpl_with_cdci_descriptor(
4907 ColorPrimaries::Bt2020,
4908 TransferCharacteristic::PqSt2084,
4909 CodingEquations::Bt2020Ncl,
4910 10,
4911 );
4912 let v = App2E2021;
4913 let issues = v.validate_cpl(&cpl);
4914 let hdr_issues: Vec<_> = issues.iter().filter(|i| i.code.contains("7.5")).collect();
4915 assert!(
4916 !hdr_issues.is_empty(),
4917 "Should note absent MaxCLL/MaxFALL for PQ content"
4918 );
4919 assert!(
4920 hdr_issues.iter().all(|i| i.severity == Severity::Info),
4921 "Missing MaxCLL/MaxFALL should be Info, not Error (0..1 cardinality per §7.5)"
4922 );
4923 }
4924
4925 #[test]
4926 fn app2e_passes_hdr_with_metadata() {
4927 let mut cpl = cpl_with_cdci_descriptor(
4928 ColorPrimaries::Bt2020,
4929 TransferCharacteristic::PqSt2084,
4930 CodingEquations::Bt2020Ncl,
4931 10,
4932 );
4933 cpl.extension_properties = Some(ExtensionProperties {
4934 application_identification: Some("http://www.smpte-ra.org/ns/2067-21/2021".to_string()),
4935 max_cll: Some(1000),
4936 max_fall: Some(400),
4937 });
4938 let v = App2E2021;
4939 let issues = v.validate_cpl(&cpl);
4940 assert!(
4941 !issues.iter().any(|i| i.code.contains("7.5")),
4942 "Should not flag HDR metadata when MaxCLL/MaxFALL are present"
4943 );
4944 }
4945
4946 #[test]
4947 fn app2e_hlg_does_not_require_hdr_metadata() {
4948 let cpl = cpl_with_cdci_descriptor(
4950 ColorPrimaries::Bt2020,
4951 TransferCharacteristic::Hlg,
4952 CodingEquations::Bt2020Ncl,
4953 10,
4954 );
4955 let v = App2E2021;
4956 let issues = v.validate_cpl(&cpl);
4957 assert!(
4958 !issues.iter().any(|i| i.code.contains("8.3.3")),
4959 "HLG should not require MaxCLL/MaxFALL"
4960 );
4961 }
4962
4963 #[test]
4964 fn app2e_flags_missing_color_primaries() {
4965 let mut cpl = cpl_with_cdci_descriptor(
4966 ColorPrimaries::Bt709,
4967 TransferCharacteristic::Bt709,
4968 CodingEquations::Bt709,
4969 10,
4970 );
4971 if let Some(ref mut edl) = cpl.essence_descriptor_list {
4973 for ed in &mut edl.essence_descriptors {
4974 if let Some(ref mut cdci) = ed.cdci_descriptor {
4975 cdci.color_primaries = None;
4976 }
4977 }
4978 }
4979 let v = App2E2021;
4980 let issues = v.validate_cpl(&cpl);
4981 assert!(
4982 issues
4983 .iter()
4984 .any(|i| i.code.contains("6.2.1/ColorPrimaries")),
4985 "Should flag missing ColorPrimaries"
4986 );
4987 }
4988
4989 #[test]
4990 fn app2e_flags_heterogeneous_color_systems() {
4991 let ed_id_1 = uuid(10);
4992 let ed_id_2 = uuid(11);
4993 let mut cpl = minimal_cpl();
4994 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
4995 essence_descriptors: vec![
4996 EssenceDescriptor {
4998 id: ed_id_1,
4999 rgba_descriptor: None,
5000 cdci_descriptor: Some(CDCIDescriptor {
5001 instance_id: None,
5002 stored_width: Some(1920),
5003 stored_height: Some(1080),
5004 display_width: Some(1920),
5005 display_height: Some(1080),
5006 sample_rate: None,
5007 image_aspect_ratio: None,
5008 color_primaries: Some(ColorPrimaries::Bt709),
5009 transfer_characteristic: Some(TransferCharacteristic::Bt709),
5010 coding_equations: Some(CodingEquations::Bt709),
5011 picture_compression: Some(VideoCodec::Jpeg2000),
5012 component_depth: Some(10),
5013 frame_layout: Some("FullFrame".to_string()),
5014 display_f2_offset: None,
5015 horizontal_subsampling: Some(2),
5016 vertical_subsampling: Some(1),
5017 color_siting: Some(0),
5018 black_ref_level: Some(64),
5019 white_ref_level: Some(940),
5020 color_range: Some(897),
5021 stored_f2_offset: None,
5022 sampled_width: None,
5023 sampled_height: None,
5024 sampled_x_offset: None,
5025 sampled_y_offset: None,
5026 alpha_transparency: None,
5027 image_alignment_offset: None,
5028 image_start_offset: None,
5029 image_end_offset: None,
5030 field_dominance: None,
5031 reversed_byte_order: None,
5032 padding_bits: None,
5033 alpha_sample_depth: None,
5034 linked_track_id: None,
5035 active_width: None,
5036 active_height: None,
5037 sub_descriptors: None,
5038 }),
5039 wave_pcm_descriptor: None,
5040 dc_timed_text_descriptor: None,
5041 iab_essence_descriptor: None,
5042 isxd_data_essence_descriptor: None,
5043 },
5044 EssenceDescriptor {
5046 id: ed_id_2,
5047 rgba_descriptor: None,
5048 cdci_descriptor: Some(CDCIDescriptor {
5049 instance_id: None,
5050 stored_width: Some(3840),
5051 stored_height: Some(2160),
5052 display_width: Some(3840),
5053 display_height: Some(2160),
5054 sample_rate: None,
5055 image_aspect_ratio: None,
5056 color_primaries: Some(ColorPrimaries::Bt2020),
5057 transfer_characteristic: Some(TransferCharacteristic::Bt2020),
5058 coding_equations: Some(CodingEquations::Bt2020Ncl),
5059 picture_compression: Some(VideoCodec::Jpeg2000),
5060 component_depth: Some(10),
5061 frame_layout: Some("FullFrame".to_string()),
5062 display_f2_offset: None,
5063 horizontal_subsampling: Some(2),
5064 vertical_subsampling: Some(1),
5065 color_siting: Some(0),
5066 black_ref_level: Some(64),
5067 white_ref_level: Some(940),
5068 color_range: Some(897),
5069 stored_f2_offset: None,
5070 sampled_width: None,
5071 sampled_height: None,
5072 sampled_x_offset: None,
5073 sampled_y_offset: None,
5074 alpha_transparency: None,
5075 image_alignment_offset: None,
5076 image_start_offset: None,
5077 image_end_offset: None,
5078 field_dominance: None,
5079 reversed_byte_order: None,
5080 padding_bits: None,
5081 alpha_sample_depth: None,
5082 linked_track_id: None,
5083 active_width: None,
5084 active_height: None,
5085 sub_descriptors: None,
5086 }),
5087 wave_pcm_descriptor: None,
5088 dc_timed_text_descriptor: None,
5089 iab_essence_descriptor: None,
5090 isxd_data_essence_descriptor: None,
5091 },
5092 ],
5093 });
5094 let mut sl = empty_sequence_list();
5095 sl.main_image_sequences.push(MainImageSequence {
5096 id: uuid(3),
5097 track_id: uuid(4),
5098 resource_list: ResourceList {
5099 resources: vec![make_resource(Some(ed_id_1))],
5100 },
5101 });
5102 cpl.segment_list.segments[0].sequence_list = sl;
5103
5104 let v = App2E2021;
5105 let issues = v.validate_cpl(&cpl);
5106 assert!(
5107 issues
5108 .iter()
5109 .any(|i| i.code.contains("7.2/HomogeneousImageEssence")),
5110 "Should flag heterogeneous color systems"
5111 );
5112 }
5113
5114 #[test]
5117 fn validate_cpl_dispatches_both_core_and_app2e() {
5118 let mut cpl = minimal_cpl(); cpl.extension_properties = Some(ExtensionProperties {
5120 application_identification: Some("http://www.smpte-ra.org/ns/2067-21/2021".to_string()),
5121 max_cll: None,
5122 max_fall: None,
5123 });
5124 let validators = get_validators_for_cpl(&cpl);
5125 assert_eq!(validators.len(), 2, "Should dispatch both core and app2e");
5126 assert_eq!(validators[0].spec_id(), "ST 2067-2:2016");
5127 assert_eq!(validators[1].spec_id(), "ST 2067-21:2023 (App2E)");
5128
5129 let issues = validate_cpl(&cpl);
5131 let has_core = issues.iter().any(|i| i.code.starts_with("ST2067-2:2016:"));
5132 assert!(
5133 has_core,
5134 "Core validator should produce issues for CPL without EDL"
5135 );
5136 }
5137
5138 #[test]
5139 fn color_system_display() {
5140 assert_eq!(
5141 ColorSystem::Color3.to_string(),
5142 "COLOR.3 (BT.709 / BT.709 / BT.709)"
5143 );
5144 assert_eq!(
5145 ColorSystem::Color7.to_string(),
5146 "COLOR.7 (BT.2020 / PQ / BT.2020 NCL)"
5147 );
5148 }
5149
5150 #[test]
5151 fn app2e_validates_j2k_sub_with_all_table14_fields() {
5152 use crate::cpl::{
5153 J2CLayout, J2KExtendedCapabilities, JPEG2000SubDescriptor, RGBADescriptor,
5154 RGBALayoutComponent, VideoSubDescriptors,
5155 };
5156
5157 let ed_id = uuid(10);
5158 let mut cpl = minimal_cpl();
5159 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
5160 essence_descriptors: vec![EssenceDescriptor {
5161 id: ed_id,
5162 rgba_descriptor: Some(RGBADescriptor {
5163 instance_id: None,
5164 display_width: Some(1920),
5165 display_height: Some(1080),
5166 stored_width: Some(1920),
5167 stored_height: Some(1080),
5168 sample_rate: Some(EditRate::new(24, 1)),
5169 image_aspect_ratio: None,
5170 color_primaries: Some(ColorPrimaries::P3D65),
5171 transfer_characteristic: Some(TransferCharacteristic::PqSt2084),
5172 coding_equations: None,
5173 picture_compression: Some(VideoCodec::Jpeg2000Ht),
5174 frame_layout: Some("FullFrame".to_string()),
5175 display_f2_offset: None,
5176 component_max_ref: Some(1023),
5177 component_min_ref: Some(0),
5178 scanning_direction: Some(
5179 "ScanningDirection_LeftToRightTopToBottom".to_string(),
5180 ),
5181 stored_f2_offset: None,
5182 sampled_width: None,
5183 sampled_height: None,
5184 sampled_x_offset: None,
5185 sampled_y_offset: None,
5186 alpha_transparency: None,
5187 image_alignment_offset: None,
5188 image_start_offset: None,
5189 image_end_offset: None,
5190 field_dominance: None,
5191 alpha_max_ref: None,
5192 alpha_min_ref: None,
5193 palette: None,
5194 palette_layout: None,
5195 linked_track_id: None,
5196 sub_descriptors: Some(VideoSubDescriptors {
5197 phdr_metadata_track_sub_descriptor: None,
5198 jpeg2000_sub_descriptor: Some(JPEG2000SubDescriptor {
5199 instance_id: None,
5200 rsiz: Some(16384), xsiz: Some(1920),
5202 ysiz: Some(1080),
5203 xo_siz: Some(0),
5204 yo_siz: Some(0),
5205 xt_siz: Some(1920),
5206 yt_siz: Some(1080),
5207 xto_siz: Some(0),
5208 yto_siz: Some(0),
5209 csiz: Some(3),
5210 coding_style_default: Some("01020001".to_string()),
5211 quantization_default: Some("2060".to_string()),
5212 j2c_layout: Some(J2CLayout {
5213 components: vec![
5214 RGBALayoutComponent {
5215 code: "CompRed".to_string(),
5216 component_size: 10,
5217 },
5218 RGBALayoutComponent {
5219 code: "CompGreen".to_string(),
5220 component_size: 10,
5221 },
5222 RGBALayoutComponent {
5223 code: "CompBlue".to_string(),
5224 component_size: 10,
5225 },
5226 ],
5227 }),
5228 j2k_extended_capabilities: Some(J2KExtendedCapabilities {
5229 pcap: Some(131072),
5230 }),
5231 picture_component_sizing: None,
5232 }),
5233 }),
5234 }),
5235 cdci_descriptor: None,
5236 wave_pcm_descriptor: None,
5237 dc_timed_text_descriptor: None,
5238 iab_essence_descriptor: None,
5239 isxd_data_essence_descriptor: None,
5240 }],
5241 });
5242 let mut sl = empty_sequence_list();
5243 sl.main_image_sequences.push(MainImageSequence {
5244 id: uuid(3),
5245 track_id: uuid(4),
5246 resource_list: ResourceList {
5247 resources: vec![make_resource(Some(ed_id))],
5248 },
5249 });
5250 cpl.segment_list.segments[0].sequence_list = sl;
5251
5252 let v = App2E2021;
5253 let issues = v.validate_cpl(&cpl);
5254 let errors: Vec<_> = issues
5255 .iter()
5256 .filter(|i| i.severity == Severity::Error || i.severity == Severity::Critical)
5257 .collect();
5258 assert!(
5259 errors.is_empty(),
5260 "Valid J2K sub descriptor should pass: {:?}",
5261 errors
5262 );
5263 }
5264
5265 #[test]
5266 fn app2e_flags_j2k_missing_coding_style() {
5267 use crate::cpl::{
5268 J2CLayout, J2KExtendedCapabilities, JPEG2000SubDescriptor, RGBADescriptor,
5269 RGBALayoutComponent, VideoSubDescriptors,
5270 };
5271
5272 let ed_id = uuid(10);
5273 let mut cpl = minimal_cpl();
5274 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
5275 essence_descriptors: vec![EssenceDescriptor {
5276 id: ed_id,
5277 rgba_descriptor: Some(RGBADescriptor {
5278 instance_id: None,
5279 display_width: Some(1920),
5280 display_height: Some(1080),
5281 stored_width: Some(1920),
5282 stored_height: Some(1080),
5283 sample_rate: Some(EditRate::new(24, 1)),
5284 image_aspect_ratio: None,
5285 color_primaries: Some(ColorPrimaries::P3D65),
5286 transfer_characteristic: Some(TransferCharacteristic::PqSt2084),
5287 coding_equations: None,
5288 picture_compression: Some(VideoCodec::Jpeg2000Ht),
5289 frame_layout: Some("FullFrame".to_string()),
5290 display_f2_offset: None,
5291 component_max_ref: Some(1023),
5292 component_min_ref: Some(0),
5293 scanning_direction: Some(
5294 "ScanningDirection_LeftToRightTopToBottom".to_string(),
5295 ),
5296 stored_f2_offset: None,
5297 sampled_width: None,
5298 sampled_height: None,
5299 sampled_x_offset: None,
5300 sampled_y_offset: None,
5301 alpha_transparency: None,
5302 image_alignment_offset: None,
5303 image_start_offset: None,
5304 image_end_offset: None,
5305 field_dominance: None,
5306 alpha_max_ref: None,
5307 alpha_min_ref: None,
5308 palette: None,
5309 palette_layout: None,
5310 linked_track_id: None,
5311 sub_descriptors: Some(VideoSubDescriptors {
5312 phdr_metadata_track_sub_descriptor: None,
5313 jpeg2000_sub_descriptor: Some(JPEG2000SubDescriptor {
5314 instance_id: None,
5315 rsiz: Some(16384),
5316 xsiz: Some(1920),
5317 ysiz: Some(1080),
5318 xo_siz: None,
5319 yo_siz: None,
5320 xt_siz: None,
5321 yt_siz: None,
5322 xto_siz: None,
5323 yto_siz: None,
5324 csiz: None,
5325 coding_style_default: None, quantization_default: None,
5327 j2c_layout: Some(J2CLayout {
5328 components: vec![RGBALayoutComponent {
5329 code: "CompRed".to_string(),
5330 component_size: 10,
5331 }],
5332 }),
5333 j2k_extended_capabilities: Some(J2KExtendedCapabilities {
5334 pcap: Some(131072),
5335 }),
5336 picture_component_sizing: None,
5337 }),
5338 }),
5339 }),
5340 cdci_descriptor: None,
5341 wave_pcm_descriptor: None,
5342 dc_timed_text_descriptor: None,
5343 iab_essence_descriptor: None,
5344 isxd_data_essence_descriptor: None,
5345 }],
5346 });
5347 let mut sl = empty_sequence_list();
5348 sl.main_image_sequences.push(MainImageSequence {
5349 id: uuid(3),
5350 track_id: uuid(4),
5351 resource_list: ResourceList {
5352 resources: vec![make_resource(Some(ed_id))],
5353 },
5354 });
5355 cpl.segment_list.segments[0].sequence_list = sl;
5356
5357 let v = App2E2021;
5358 let issues = v.validate_cpl(&cpl);
5359 assert!(
5360 issues.iter().any(|i| i.code.contains("6.5.2/CodingStyle")),
5361 "Should flag missing CodingStyleDefault: {:#?}",
5362 issues
5363 );
5364 }
5365
5366 #[test]
5367 fn app2e_flags_j2k_missing_j2c_layout() {
5368 use crate::cpl::{
5369 J2KExtendedCapabilities, JPEG2000SubDescriptor, RGBADescriptor, VideoSubDescriptors,
5370 };
5371
5372 let ed_id = uuid(10);
5373 let mut cpl = minimal_cpl();
5374 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
5375 essence_descriptors: vec![EssenceDescriptor {
5376 id: ed_id,
5377 rgba_descriptor: Some(RGBADescriptor {
5378 instance_id: None,
5379 display_width: Some(1920),
5380 display_height: Some(1080),
5381 stored_width: Some(1920),
5382 stored_height: Some(1080),
5383 sample_rate: Some(EditRate::new(24, 1)),
5384 image_aspect_ratio: None,
5385 color_primaries: Some(ColorPrimaries::P3D65),
5386 transfer_characteristic: Some(TransferCharacteristic::PqSt2084),
5387 coding_equations: None,
5388 picture_compression: Some(VideoCodec::Jpeg2000Ht),
5389 frame_layout: Some("FullFrame".to_string()),
5390 display_f2_offset: None,
5391 component_max_ref: Some(1023),
5392 component_min_ref: Some(0),
5393 scanning_direction: Some(
5394 "ScanningDirection_LeftToRightTopToBottom".to_string(),
5395 ),
5396 stored_f2_offset: None,
5397 sampled_width: None,
5398 sampled_height: None,
5399 sampled_x_offset: None,
5400 sampled_y_offset: None,
5401 alpha_transparency: None,
5402 image_alignment_offset: None,
5403 image_start_offset: None,
5404 image_end_offset: None,
5405 field_dominance: None,
5406 alpha_max_ref: None,
5407 alpha_min_ref: None,
5408 palette: None,
5409 palette_layout: None,
5410 linked_track_id: None,
5411 sub_descriptors: Some(VideoSubDescriptors {
5412 phdr_metadata_track_sub_descriptor: None,
5413 jpeg2000_sub_descriptor: Some(JPEG2000SubDescriptor {
5414 instance_id: None,
5415 rsiz: Some(16384),
5416 xsiz: Some(1920),
5417 ysiz: Some(1080),
5418 xo_siz: None,
5419 yo_siz: None,
5420 xt_siz: None,
5421 yt_siz: None,
5422 xto_siz: None,
5423 yto_siz: None,
5424 csiz: None,
5425 coding_style_default: Some("01020001".to_string()),
5426 quantization_default: None,
5427 j2c_layout: None, j2k_extended_capabilities: Some(J2KExtendedCapabilities {
5429 pcap: Some(131072),
5430 }),
5431 picture_component_sizing: None,
5432 }),
5433 }),
5434 }),
5435 cdci_descriptor: None,
5436 wave_pcm_descriptor: None,
5437 dc_timed_text_descriptor: None,
5438 iab_essence_descriptor: None,
5439 isxd_data_essence_descriptor: None,
5440 }],
5441 });
5442 let mut sl = empty_sequence_list();
5443 sl.main_image_sequences.push(MainImageSequence {
5444 id: uuid(3),
5445 track_id: uuid(4),
5446 resource_list: ResourceList {
5447 resources: vec![make_resource(Some(ed_id))],
5448 },
5449 });
5450 cpl.segment_list.segments[0].sequence_list = sl;
5451
5452 let v = App2E2021;
5453 let issues = v.validate_cpl(&cpl);
5454 assert!(
5455 issues.iter().any(|i| i.code.contains("6.5.2/J2CLayout")),
5456 "Should flag missing J2CLayout: {:#?}",
5457 issues
5458 );
5459 }
5460
5461 #[test]
5462 fn app2e_flags_j2k_missing_extended_capabilities_for_htj2k() {
5463 use crate::cpl::{
5464 J2CLayout, JPEG2000SubDescriptor, RGBADescriptor, RGBALayoutComponent,
5465 VideoSubDescriptors,
5466 };
5467
5468 let ed_id = uuid(10);
5469 let mut cpl = minimal_cpl();
5470 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
5471 essence_descriptors: vec![EssenceDescriptor {
5472 id: ed_id,
5473 rgba_descriptor: Some(RGBADescriptor {
5474 instance_id: None,
5475 display_width: Some(1920),
5476 display_height: Some(1080),
5477 stored_width: Some(1920),
5478 stored_height: Some(1080),
5479 sample_rate: Some(EditRate::new(24, 1)),
5480 image_aspect_ratio: None,
5481 color_primaries: Some(ColorPrimaries::P3D65),
5482 transfer_characteristic: Some(TransferCharacteristic::PqSt2084),
5483 coding_equations: None,
5484 picture_compression: Some(VideoCodec::Jpeg2000Ht),
5485 frame_layout: Some("FullFrame".to_string()),
5486 display_f2_offset: None,
5487 component_max_ref: Some(1023),
5488 component_min_ref: Some(0),
5489 scanning_direction: Some(
5490 "ScanningDirection_LeftToRightTopToBottom".to_string(),
5491 ),
5492 stored_f2_offset: None,
5493 sampled_width: None,
5494 sampled_height: None,
5495 sampled_x_offset: None,
5496 sampled_y_offset: None,
5497 alpha_transparency: None,
5498 image_alignment_offset: None,
5499 image_start_offset: None,
5500 image_end_offset: None,
5501 field_dominance: None,
5502 alpha_max_ref: None,
5503 alpha_min_ref: None,
5504 palette: None,
5505 palette_layout: None,
5506 linked_track_id: None,
5507 sub_descriptors: Some(VideoSubDescriptors {
5508 phdr_metadata_track_sub_descriptor: None,
5509 jpeg2000_sub_descriptor: Some(JPEG2000SubDescriptor {
5510 instance_id: None,
5511 rsiz: Some(16384), xsiz: Some(1920),
5513 ysiz: Some(1080),
5514 xo_siz: None,
5515 yo_siz: None,
5516 xt_siz: None,
5517 yt_siz: None,
5518 xto_siz: None,
5519 yto_siz: None,
5520 csiz: None,
5521 coding_style_default: Some("01020001".to_string()),
5522 quantization_default: None,
5523 j2c_layout: Some(J2CLayout {
5524 components: vec![RGBALayoutComponent {
5525 code: "CompRed".to_string(),
5526 component_size: 10,
5527 }],
5528 }),
5529 j2k_extended_capabilities: None, picture_component_sizing: None,
5531 }),
5532 }),
5533 }),
5534 cdci_descriptor: None,
5535 wave_pcm_descriptor: None,
5536 dc_timed_text_descriptor: None,
5537 iab_essence_descriptor: None,
5538 isxd_data_essence_descriptor: None,
5539 }],
5540 });
5541 let mut sl = empty_sequence_list();
5542 sl.main_image_sequences.push(MainImageSequence {
5543 id: uuid(3),
5544 track_id: uuid(4),
5545 resource_list: ResourceList {
5546 resources: vec![make_resource(Some(ed_id))],
5547 },
5548 });
5549 cpl.segment_list.segments[0].sequence_list = sl;
5550
5551 let v = App2E2021;
5552 let issues = v.validate_cpl(&cpl);
5553 assert!(
5554 issues
5555 .iter()
5556 .any(|i| i.code.contains("6.5.2/J2KExtendedCapabilities")),
5557 "Should flag missing J2KExtendedCapabilities for HTJ2K: {:#?}",
5558 issues
5559 );
5560 }
5561
5562 #[test]
5563 fn app2e_warns_j2k_sub_descriptor_missing() {
5564 let cpl = cpl_with_rgba_descriptor(ColorPrimaries::P3D65, TransferCharacteristic::PqSt2084);
5565
5566 let v = App2E2021;
5567 let issues = v.validate_cpl(&cpl);
5568
5569 assert!(
5570 issues.iter().any(|i| {
5571 i.code.contains("6.5.2/JPEG2000SubDescriptor") && i.severity == Severity::Warning
5572 }),
5573 "Should warn when JPEG2000SubDescriptor is missing: {:#?}",
5574 issues
5575 );
5576 }
5577
5578 #[test]
5579 fn app2e_sampled_x_offset_non_zero() {
5580 let mut cpl = cpl_with_cdci_descriptor(
5581 ColorPrimaries::Bt709,
5582 TransferCharacteristic::Bt709,
5583 CodingEquations::Bt709,
5584 10,
5585 );
5586 if let Some(ref mut edl) = cpl.essence_descriptor_list {
5587 for ed in &mut edl.essence_descriptors {
5588 if let Some(ref mut cdci) = ed.cdci_descriptor {
5589 cdci.sampled_x_offset = Some(1);
5590 }
5591 }
5592 }
5593 let v = App2E2021;
5594 let issues = v.validate_cpl(&cpl);
5595 assert!(
5596 issues
5597 .iter()
5598 .any(|i| i.code.contains("6.2.1/SampledXOffset")),
5599 "Should flag SampledXOffset != 0: {:#?}",
5600 issues
5601 );
5602 }
5603
5604 fn cpl_with_audio_and_segment_duration(
5609 audio_sample_rate: EditRate,
5610 composition_edit_rate: EditRate,
5611 segment_durations: &[u64],
5612 ) -> CompositionPlaylist {
5613 let mut cpl = minimal_cpl();
5614 cpl.edit_rate = Some(composition_edit_rate);
5615
5616 let audio_ed_id = uuid(20);
5618 let video_ed_id = uuid(10);
5619
5620 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
5621 essence_descriptors: vec![
5622 EssenceDescriptor {
5623 id: video_ed_id,
5624 rgba_descriptor: None,
5625 cdci_descriptor: None,
5626 wave_pcm_descriptor: None,
5627 dc_timed_text_descriptor: None,
5628 iab_essence_descriptor: None,
5629 isxd_data_essence_descriptor: None,
5630 },
5631 EssenceDescriptor {
5632 id: audio_ed_id,
5633 rgba_descriptor: None,
5634 cdci_descriptor: None,
5635 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
5636 instance_id: None,
5637 sample_rate: None,
5638 audio_sample_rate: Some(audio_sample_rate),
5639 channel_count: Some(2),
5640 quantization_bits: Some(24),
5641 linked_track_id: None,
5642 sub_descriptors: None,
5643 }),
5644 dc_timed_text_descriptor: None,
5645 iab_essence_descriptor: None,
5646 isxd_data_essence_descriptor: None,
5647 },
5648 ],
5649 });
5650
5651 let mut segments = Vec::new();
5652 for (i, &dur) in segment_durations.iter().enumerate() {
5653 let mut sl = empty_sequence_list();
5654 sl.main_image_sequences.push(MainImageSequence {
5655 id: uuid(30 + i as u8),
5656 track_id: uuid(40),
5657 resource_list: ResourceList {
5658 resources: vec![Resource {
5659 id: uuid(50 + i as u8),
5660 annotation: None,
5661 edit_rate: None,
5662 intrinsic_duration: dur,
5663 entry_point: None,
5664 source_duration: Some(dur),
5665 source_encoding: Some(video_ed_id),
5666 track_file_id: Some(uuid(60 + i as u8)),
5667 repeat_count: None,
5668 key_id: None,
5669 hash: None,
5670 markers: vec![],
5671 }],
5672 },
5673 });
5674 segments.push(Segment {
5675 id: uuid(70 + i as u8),
5676 sequence_list: sl,
5677 });
5678 }
5679 cpl.segment_list = SegmentList { segments };
5680
5681 cpl
5682 }
5683
5684 #[test]
5687 fn app2e_flags_segment_duration_not_multiple_of_5() {
5688 let cpl = cpl_with_audio_and_segment_duration(
5689 EditRate::new(48000, 1), EditRate::new(30000, 1001), &[7], );
5693 let v = App2E2021;
5694 let issues = v.validate_cpl(&cpl);
5695 assert!(
5696 issues.iter().any(|i| i.code.contains("7.4")),
5697 "Should flag segment duration not multiple of 5: {:#?}",
5698 issues
5699 );
5700 }
5701
5702 #[test]
5704 fn app2e_allows_segment_duration_multiple_of_5() {
5705 let cpl = cpl_with_audio_and_segment_duration(
5706 EditRate::new(48000, 1), EditRate::new(30000, 1001), &[10], );
5710 let v = App2E2021;
5711 let issues = v.validate_cpl(&cpl);
5712 assert!(
5713 !issues.iter().any(|i| i.code.contains("7.4")),
5714 "Should NOT flag segment duration that is a multiple of 5: {:#?}",
5715 issues
5716 );
5717 }
5718
5719 #[test]
5722 fn app2e_allows_any_duration_when_integer_samples() {
5723 let cpl = cpl_with_audio_and_segment_duration(
5724 EditRate::new(48000, 1), EditRate::new(24, 1), &[7], );
5728 let v = App2E2021;
5729 let issues = v.validate_cpl(&cpl);
5730 assert!(
5731 !issues.iter().any(|i| i.code.contains("7.4")),
5732 "Should NOT flag when audio samples per EU is integer: {:#?}",
5733 issues
5734 );
5735 }
5736
5737 #[test]
5739 fn app2e_flags_only_non_compliant_segments() {
5740 let cpl = cpl_with_audio_and_segment_duration(
5741 EditRate::new(48000, 1), EditRate::new(30000, 1001), &[10, 7, 15], );
5745 let v = App2E2021;
5746 let issues = v.validate_cpl(&cpl);
5747 let seg_issues: Vec<_> = issues.iter().filter(|i| i.code.contains("7.4")).collect();
5748 assert_eq!(
5749 seg_issues.len(),
5750 1,
5751 "Should flag exactly 1 segment: {:#?}",
5752 seg_issues
5753 );
5754 assert!(
5755 seg_issues[0].message.contains("Segment 2"),
5756 "Should flag segment 2 (the one with duration 7): {}",
5757 seg_issues[0].message
5758 );
5759 }
5760
5761 #[test]
5763 fn app2e_allows_any_duration_at_23_976fps() {
5764 let cpl = cpl_with_audio_and_segment_duration(
5765 EditRate::new(48000, 1), EditRate::new(24000, 1001), &[13], );
5769 let v = App2E2021;
5770 let issues = v.validate_cpl(&cpl);
5771 assert!(
5772 !issues.iter().any(|i| i.code.contains("7.4")),
5773 "Should NOT flag at 23.976 fps (integer samples/EU): {:#?}",
5774 issues
5775 );
5776 }
5777
5778 #[test]
5783 fn core_rejects_duplicate_segment_ids() {
5784 use crate::cpl::Segment;
5785
5786 let mut cpl = minimal_cpl();
5787 cpl.segment_list.segments.push(Segment {
5789 id: cpl.segment_list.segments[0].id,
5790 sequence_list: empty_sequence_list(),
5791 });
5792 let issues = CoreConstraints2020.validate_cpl(&cpl);
5793 assert!(
5794 issues.iter().any(|i| i.code.contains("UniqueSegmentId")),
5795 "Duplicate segment IDs should be flagged: {:#?}",
5796 issues,
5797 );
5798 }
5799
5800 #[test]
5801 fn core_rejects_zero_intrinsic_duration() {
5802 let mut cpl = minimal_cpl();
5803 let ed_id = uuid(10);
5804 cpl.segment_list.segments[0]
5805 .sequence_list
5806 .main_image_sequences
5807 .push(MainImageSequence {
5808 id: uuid(3),
5809 track_id: uuid(4),
5810 resource_list: ResourceList {
5811 resources: vec![Resource {
5812 id: uuid(99),
5813 annotation: None,
5814 edit_rate: None,
5815 intrinsic_duration: 0, entry_point: None,
5817 source_duration: None,
5818 source_encoding: Some(ed_id),
5819 track_file_id: Some(uuid(50)),
5820 repeat_count: None,
5821 key_id: None,
5822 hash: None,
5823 markers: vec![],
5824 }],
5825 },
5826 });
5827 let issues = CoreConstraints2020.validate_cpl(&cpl);
5828 assert!(
5829 issues.iter().any(|i| i.code.contains("IntrinsicDuration")),
5830 "Zero IntrinsicDuration should be flagged: {:#?}",
5831 issues,
5832 );
5833 }
5834
5835 #[test]
5836 fn core_rejects_entry_plus_duration_exceeds_intrinsic() {
5837 let mut cpl = minimal_cpl();
5838 let ed_id = uuid(10);
5839 cpl.segment_list.segments[0]
5840 .sequence_list
5841 .main_image_sequences
5842 .push(MainImageSequence {
5843 id: uuid(3),
5844 track_id: uuid(4),
5845 resource_list: ResourceList {
5846 resources: vec![Resource {
5847 id: uuid(99),
5848 annotation: None,
5849 edit_rate: None,
5850 intrinsic_duration: 100,
5851 entry_point: Some(50),
5852 source_duration: Some(60), source_encoding: Some(ed_id),
5854 track_file_id: Some(uuid(50)),
5855 repeat_count: None,
5856 key_id: None,
5857 hash: None,
5858 markers: vec![],
5859 }],
5860 },
5861 });
5862 let issues = CoreConstraints2020.validate_cpl(&cpl);
5863 assert!(
5864 issues.iter().any(|i| i.code.contains("ResourceDuration")),
5865 "EntryPoint + SourceDuration > IntrinsicDuration should be flagged: {:#?}",
5866 issues,
5867 );
5868 }
5869
5870 #[test]
5871 fn core_accepts_valid_entry_plus_duration() {
5872 let mut cpl = minimal_cpl();
5873 let ed_id = uuid(10);
5874 cpl.segment_list.segments[0]
5875 .sequence_list
5876 .main_image_sequences
5877 .push(MainImageSequence {
5878 id: uuid(3),
5879 track_id: uuid(4),
5880 resource_list: ResourceList {
5881 resources: vec![Resource {
5882 id: uuid(99),
5883 annotation: None,
5884 edit_rate: None,
5885 intrinsic_duration: 100,
5886 entry_point: Some(50),
5887 source_duration: Some(50), source_encoding: Some(ed_id),
5889 track_file_id: Some(uuid(50)),
5890 repeat_count: None,
5891 key_id: None,
5892 hash: None,
5893 markers: vec![],
5894 }],
5895 },
5896 });
5897 let issues = CoreConstraints2020.validate_cpl(&cpl);
5898 assert!(
5899 !issues.iter().any(|i| i.code.contains("ResourceDuration")),
5900 "Valid EntryPoint + SourceDuration should not be flagged: {:#?}",
5901 issues,
5902 );
5903 }
5904
5905 #[test]
5906 fn core_rejects_zero_repeat_count() {
5907 let mut cpl = minimal_cpl();
5908 let ed_id = uuid(10);
5909 cpl.segment_list.segments[0]
5910 .sequence_list
5911 .main_image_sequences
5912 .push(MainImageSequence {
5913 id: uuid(3),
5914 track_id: uuid(4),
5915 resource_list: ResourceList {
5916 resources: vec![Resource {
5917 id: uuid(99),
5918 annotation: None,
5919 edit_rate: None,
5920 intrinsic_duration: 100,
5921 entry_point: None,
5922 source_duration: None,
5923 source_encoding: Some(ed_id),
5924 track_file_id: Some(uuid(50)),
5925 repeat_count: Some(0), key_id: None,
5927 hash: None,
5928 markers: vec![],
5929 }],
5930 },
5931 });
5932 let issues = CoreConstraints2020.validate_cpl(&cpl);
5933 assert!(
5934 issues.iter().any(|i| i.code.contains("RepeatCount")),
5935 "Zero RepeatCount should be flagged: {:#?}",
5936 issues,
5937 );
5938 }
5939
5940 #[test]
5943 fn core_rejects_zero_source_duration() {
5944 let mut cpl = minimal_cpl();
5945 let ed_id = uuid(10);
5946 cpl.segment_list.segments[0]
5947 .sequence_list
5948 .main_image_sequences
5949 .push(MainImageSequence {
5950 id: uuid(3),
5951 track_id: uuid(4),
5952 resource_list: ResourceList {
5953 resources: vec![Resource {
5954 id: uuid(99),
5955 annotation: None,
5956 edit_rate: None,
5957 intrinsic_duration: 100,
5958 entry_point: None,
5959 source_duration: Some(0), source_encoding: Some(ed_id),
5961 track_file_id: Some(uuid(50)),
5962 repeat_count: None,
5963 key_id: None,
5964 hash: None,
5965 markers: vec![],
5966 }],
5967 },
5968 });
5969 let issues = CoreConstraints2020.validate_cpl(&cpl);
5970 assert!(
5971 issues.iter().any(|i| i.code.contains("SourceDuration")),
5972 "Zero SourceDuration should be flagged: {:#?}",
5973 issues,
5974 );
5975 }
5976
5977 #[test]
5978 fn core_accepts_nonzero_source_duration() {
5979 let mut cpl = minimal_cpl();
5980 let ed_id = uuid(10);
5981 cpl.segment_list.segments[0]
5982 .sequence_list
5983 .main_image_sequences
5984 .push(MainImageSequence {
5985 id: uuid(3),
5986 track_id: uuid(4),
5987 resource_list: ResourceList {
5988 resources: vec![Resource {
5989 id: uuid(99),
5990 annotation: None,
5991 edit_rate: None,
5992 intrinsic_duration: 100,
5993 entry_point: None,
5994 source_duration: Some(50),
5995 source_encoding: Some(ed_id),
5996 track_file_id: Some(uuid(50)),
5997 repeat_count: None,
5998 key_id: None,
5999 hash: None,
6000 markers: vec![],
6001 }],
6002 },
6003 });
6004 let issues = CoreConstraints2020.validate_cpl(&cpl);
6005 assert!(
6006 !issues.iter().any(|i| i.code.contains("SourceDuration")),
6007 "Valid SourceDuration should not be flagged: {:#?}",
6008 issues,
6009 );
6010 }
6011
6012 #[test]
6015 fn core_rejects_entry_point_gte_intrinsic_duration() {
6016 let mut cpl = minimal_cpl();
6017 let ed_id = uuid(10);
6018 cpl.segment_list.segments[0]
6019 .sequence_list
6020 .main_image_sequences
6021 .push(MainImageSequence {
6022 id: uuid(3),
6023 track_id: uuid(4),
6024 resource_list: ResourceList {
6025 resources: vec![Resource {
6026 id: uuid(99),
6027 annotation: None,
6028 edit_rate: None,
6029 intrinsic_duration: 100,
6030 entry_point: Some(100), source_duration: None,
6032 source_encoding: Some(ed_id),
6033 track_file_id: Some(uuid(50)),
6034 repeat_count: None,
6035 key_id: None,
6036 hash: None,
6037 markers: vec![],
6038 }],
6039 },
6040 });
6041 let issues = CoreConstraints2020.validate_cpl(&cpl);
6042 assert!(
6043 issues.iter().any(|i| i.code.contains("EntryPoint")),
6044 "EntryPoint >= IntrinsicDuration should be flagged: {:#?}",
6045 issues,
6046 );
6047 }
6048
6049 #[test]
6050 fn core_accepts_entry_point_less_than_intrinsic() {
6051 let mut cpl = minimal_cpl();
6052 let ed_id = uuid(10);
6053 cpl.segment_list.segments[0]
6054 .sequence_list
6055 .main_image_sequences
6056 .push(MainImageSequence {
6057 id: uuid(3),
6058 track_id: uuid(4),
6059 resource_list: ResourceList {
6060 resources: vec![Resource {
6061 id: uuid(99),
6062 annotation: None,
6063 edit_rate: None,
6064 intrinsic_duration: 100,
6065 entry_point: Some(99), source_duration: Some(1),
6067 source_encoding: Some(ed_id),
6068 track_file_id: Some(uuid(50)),
6069 repeat_count: None,
6070 key_id: None,
6071 hash: None,
6072 markers: vec![],
6073 }],
6074 },
6075 });
6076 let issues = CoreConstraints2020.validate_cpl(&cpl);
6077 assert!(
6078 !issues.iter().any(|i| i.code.contains("EntryPoint")),
6079 "Valid EntryPoint should not be flagged: {:#?}",
6080 issues,
6081 );
6082 }
6083
6084 #[test]
6087 fn core_rejects_duplicate_track_id_within_segment() {
6088 let mut cpl = minimal_cpl();
6089 let ed_id = uuid(10);
6090 let shared_track_id = uuid(4);
6091 let make_seq = |seq_id: u8, res_id: u8| MainImageSequence {
6092 id: uuid(seq_id),
6093 track_id: shared_track_id,
6094 resource_list: ResourceList {
6095 resources: vec![Resource {
6096 id: uuid(res_id),
6097 annotation: None,
6098 edit_rate: None,
6099 intrinsic_duration: 48,
6100 entry_point: None,
6101 source_duration: None,
6102 source_encoding: Some(ed_id),
6103 track_file_id: Some(uuid(50)),
6104 repeat_count: None,
6105 key_id: None,
6106 hash: None,
6107 markers: vec![],
6108 }],
6109 },
6110 };
6111 cpl.segment_list.segments[0]
6112 .sequence_list
6113 .main_image_sequences
6114 .push(make_seq(3, 99));
6115 cpl.segment_list.segments[0]
6116 .sequence_list
6117 .main_image_sequences
6118 .push(make_seq(5, 98));
6119 let issues = CoreConstraints2020.validate_cpl(&cpl);
6120 assert!(
6121 issues.iter().any(|i| i.code.contains("TrackIdNotUnique")),
6122 "Duplicate TrackId in same segment should be flagged: {:#?}",
6123 issues,
6124 );
6125 }
6126
6127 #[test]
6128 fn core_accepts_same_track_id_in_different_segments() {
6129 let mut cpl = minimal_cpl();
6130 let ed_id = uuid(10);
6131 let shared_track_id = uuid(4);
6132 let make_res = |res_id: u8| Resource {
6133 id: uuid(res_id),
6134 annotation: None,
6135 edit_rate: None,
6136 intrinsic_duration: 48,
6137 entry_point: None,
6138 source_duration: None,
6139 source_encoding: Some(ed_id),
6140 track_file_id: Some(uuid(50)),
6141 repeat_count: None,
6142 key_id: None,
6143 hash: None,
6144 markers: vec![],
6145 };
6146 let mut sl1 = empty_sequence_list();
6147 sl1.main_image_sequences.push(MainImageSequence {
6148 id: uuid(3),
6149 track_id: shared_track_id,
6150 resource_list: ResourceList {
6151 resources: vec![make_res(99)],
6152 },
6153 });
6154 let mut sl2 = empty_sequence_list();
6155 sl2.main_image_sequences.push(MainImageSequence {
6156 id: uuid(7),
6157 track_id: shared_track_id,
6158 resource_list: ResourceList {
6159 resources: vec![make_res(98)],
6160 },
6161 });
6162 cpl.segment_list.segments = vec![
6163 Segment {
6164 id: uuid(2),
6165 sequence_list: sl1,
6166 },
6167 Segment {
6168 id: uuid(6),
6169 sequence_list: sl2,
6170 },
6171 ];
6172 let issues = CoreConstraints2020.validate_cpl(&cpl);
6173 assert!(
6174 !issues.iter().any(|i| i.code.contains("TrackIdNotUnique")),
6175 "Same TrackId in different segments (virtual track) should not be flagged: {:#?}",
6176 issues,
6177 );
6178 }
6179
6180 #[test]
6183 fn core_warns_malformed_locale_language_tag() {
6184 let mut cpl = minimal_cpl();
6185 cpl.locale_list = Some(crate::cpl::LocaleList {
6186 locales: vec![crate::cpl::Locale {
6187 language_list: Some(crate::cpl::LanguageList {
6188 languages: vec![crate::cpl::LanguageTag::new("123invalid")],
6189 }),
6190 region_list: None,
6191 content_maturity_rating_list: None,
6192 }],
6193 });
6194 let issues = CoreConstraints2020.validate_cpl(&cpl);
6195 assert!(
6196 issues
6197 .iter()
6198 .any(|i| i.code.contains("LocaleLanguageTagInvalid")),
6199 "Malformed language tag should be flagged at core level: {:#?}",
6200 issues,
6201 );
6202 }
6203
6204 #[test]
6205 fn core_accepts_valid_locale() {
6206 let mut cpl = minimal_cpl();
6207 cpl.locale_list = Some(crate::cpl::LocaleList {
6208 locales: vec![crate::cpl::Locale {
6209 language_list: Some(crate::cpl::LanguageList {
6210 languages: vec![
6211 crate::cpl::LanguageTag::new("nl"),
6212 crate::cpl::LanguageTag::new("en"),
6213 ],
6214 }),
6215 region_list: Some(crate::cpl::RegionList {
6216 regions: vec!["NL".to_string(), "US".to_string()],
6217 }),
6218 content_maturity_rating_list: None,
6219 }],
6220 });
6221 let issues = CoreConstraints2020.validate_cpl(&cpl);
6222 assert!(
6223 !issues.iter().any(|i| i.code.contains("Locale")),
6224 "Valid locale should not be flagged: {:#?}",
6225 issues,
6226 );
6227 }
6228
6229 #[test]
6232 fn core_2016_requires_essence_descriptor_list() {
6233 let mut cpl = minimal_cpl();
6234 cpl.namespace = CplNamespace::Smpte2067_3_2016;
6235 cpl.essence_descriptor_list = None; let issues = CoreConstraints2016.validate_cpl(&cpl);
6237 assert!(
6238 issues
6239 .iter()
6240 .any(|i| i.code.contains("EssenceDescriptorList")),
6241 "ST 2067-2:2016 should require EssenceDescriptorList: {:#?}",
6242 issues,
6243 );
6244 }
6245
6246 #[test]
6247 fn core_2016_accepts_present_essence_descriptor_list() {
6248 let mut cpl = minimal_cpl();
6249 cpl.namespace = CplNamespace::Smpte2067_3_2016;
6250 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
6252 essence_descriptors: vec![EssenceDescriptor {
6253 id: uuid(99),
6254 rgba_descriptor: None,
6255 cdci_descriptor: None,
6256 wave_pcm_descriptor: None,
6257 dc_timed_text_descriptor: None,
6258 iab_essence_descriptor: None,
6259 isxd_data_essence_descriptor: None,
6260 }],
6261 });
6262 let issues = CoreConstraints2016.validate_cpl(&cpl);
6263 assert!(
6264 !issues
6265 .iter()
6266 .any(|i| i.code.contains("EssenceDescriptorList")),
6267 "Present non-empty EDL should not be flagged: {:#?}",
6268 issues,
6269 );
6270 }
6271
6272 #[test]
6275 fn core_flags_empty_content_version_id() {
6276 let mut cpl = minimal_cpl();
6277 cpl.content_version_list = Some(crate::cpl::ContentVersionList {
6278 content_versions: vec![crate::cpl::ContentVersion {
6279 id: "".to_string(),
6280 label_text: Some(crate::cpl::LanguageString {
6281 text: "Version 1".to_string(),
6282 language: None,
6283 }),
6284 }],
6285 });
6286 let issues = CoreConstraints2020.validate_cpl(&cpl);
6287 assert!(
6288 issues
6289 .iter()
6290 .any(|i| i.code.contains("ContentVersionIdInvalid")),
6291 "Empty ContentVersion Id should be flagged: {:#?}",
6292 issues,
6293 );
6294 }
6295
6296 #[test]
6297 fn core_accepts_nonempty_content_version_id() {
6298 let mut cpl = minimal_cpl();
6299 cpl.content_version_list = Some(crate::cpl::ContentVersionList {
6300 content_versions: vec![crate::cpl::ContentVersion {
6301 id: "urn:uuid:00000000-0000-0000-0000-000000000001".to_string(),
6302 label_text: Some(crate::cpl::LanguageString {
6303 text: "Version 1".to_string(),
6304 language: None,
6305 }),
6306 }],
6307 });
6308 let issues = CoreConstraints2020.validate_cpl(&cpl);
6309 assert!(
6310 !issues
6311 .iter()
6312 .any(|i| i.code.contains("ContentVersionIdInvalid")),
6313 "Non-empty ContentVersion Id should not be flagged: {:#?}",
6314 issues,
6315 );
6316 }
6317
6318 #[test]
6319 fn core_rejects_missing_track_file_id() {
6320 let mut cpl = minimal_cpl();
6321 cpl.segment_list.segments[0]
6322 .sequence_list
6323 .main_image_sequences
6324 .push(MainImageSequence {
6325 id: uuid(3),
6326 track_id: uuid(4),
6327 resource_list: ResourceList {
6328 resources: vec![Resource {
6329 id: uuid(99),
6330 annotation: None,
6331 edit_rate: None,
6332 intrinsic_duration: 100,
6333 entry_point: None,
6334 source_duration: None,
6335 source_encoding: Some(uuid(10)),
6336 track_file_id: None, repeat_count: None,
6338 key_id: None,
6339 hash: None,
6340 markers: vec![],
6341 }],
6342 },
6343 });
6344 let issues = CoreConstraints2020.validate_cpl(&cpl);
6345 assert!(
6346 issues.iter().any(|i| i.code.contains("TrackFileId")),
6347 "Missing TrackFileId should be flagged: {:#?}",
6348 issues,
6349 );
6350 }
6351
6352 #[test]
6353 fn core_detects_virtual_track_discontinuity() {
6354 use crate::cpl::Segment;
6355
6356 let mut cpl = minimal_cpl();
6357 let track_a = uuid(4);
6358 let track_b = uuid(5);
6359
6360 cpl.segment_list.segments[0]
6362 .sequence_list
6363 .main_image_sequences
6364 .push(MainImageSequence {
6365 id: uuid(10),
6366 track_id: track_a,
6367 resource_list: ResourceList {
6368 resources: vec![make_resource(Some(uuid(20)))],
6369 },
6370 });
6371 cpl.segment_list.segments[0]
6372 .sequence_list
6373 .main_audio_sequences
6374 .push(MainAudioSequence {
6375 id: uuid(11),
6376 track_id: track_b,
6377 resource_list: ResourceList {
6378 resources: vec![make_resource(Some(uuid(21)))],
6379 },
6380 });
6381
6382 cpl.segment_list.segments.push(Segment {
6384 id: uuid(30),
6385 sequence_list: SequenceList {
6386 main_image_sequences: vec![MainImageSequence {
6387 id: uuid(12),
6388 track_id: track_a,
6389 resource_list: ResourceList {
6390 resources: vec![make_resource(Some(uuid(22)))],
6391 },
6392 }],
6393 main_audio_sequences: vec![],
6394 subtitles_sequences: vec![],
6395 hearing_impaired_captions_sequences: vec![],
6396 forced_narrative_sequences: vec![],
6397 marker_sequences: vec![],
6398 iab_sequences: vec![],
6399 isxd_sequences: vec![],
6400 },
6401 });
6402
6403 let issues = CoreConstraints2020.validate_cpl(&cpl);
6404 assert!(
6405 issues
6406 .iter()
6407 .any(|i| i.code.contains("VirtualTrackContinuity")),
6408 "Missing virtual track in segment 2 should be flagged: {:#?}",
6409 issues,
6410 );
6411 }
6412
6413 #[test]
6414 fn core_accepts_continuous_virtual_tracks() {
6415 use crate::cpl::Segment;
6416
6417 let mut cpl = minimal_cpl();
6418 let track_a = uuid(4);
6419
6420 cpl.segment_list.segments[0]
6421 .sequence_list
6422 .main_image_sequences
6423 .push(MainImageSequence {
6424 id: uuid(10),
6425 track_id: track_a,
6426 resource_list: ResourceList {
6427 resources: vec![make_resource(Some(uuid(20)))],
6428 },
6429 });
6430
6431 cpl.segment_list.segments.push(Segment {
6433 id: uuid(30),
6434 sequence_list: SequenceList {
6435 main_image_sequences: vec![MainImageSequence {
6436 id: uuid(12),
6437 track_id: track_a,
6438 resource_list: ResourceList {
6439 resources: vec![make_resource(Some(uuid(22)))],
6440 },
6441 }],
6442 main_audio_sequences: vec![],
6443 subtitles_sequences: vec![],
6444 hearing_impaired_captions_sequences: vec![],
6445 forced_narrative_sequences: vec![],
6446 marker_sequences: vec![],
6447 iab_sequences: vec![],
6448 isxd_sequences: vec![],
6449 },
6450 });
6451
6452 let issues = CoreConstraints2020.validate_cpl(&cpl);
6453 assert!(
6454 !issues
6455 .iter()
6456 .any(|i| i.code.contains("VirtualTrackContinuity")),
6457 "Continuous virtual tracks should not be flagged: {:#?}",
6458 issues,
6459 );
6460 }
6461
6462 #[test]
6463 fn core_detects_edit_rate_mismatch_in_virtual_track() {
6464 use crate::cpl::Segment;
6465
6466 let mut cpl = minimal_cpl();
6467 let track_a = uuid(4);
6468
6469 let mut res1 = make_resource(Some(uuid(20)));
6471 res1.id = uuid(91);
6472 res1.edit_rate = Some(EditRate::new(24, 1));
6473
6474 cpl.segment_list.segments[0]
6475 .sequence_list
6476 .main_image_sequences
6477 .push(MainImageSequence {
6478 id: uuid(10),
6479 track_id: track_a,
6480 resource_list: ResourceList {
6481 resources: vec![res1],
6482 },
6483 });
6484
6485 let mut res2 = make_resource(Some(uuid(22)));
6487 res2.id = uuid(92);
6488 res2.edit_rate = Some(EditRate::new(25, 1));
6489
6490 cpl.segment_list.segments.push(Segment {
6491 id: uuid(30),
6492 sequence_list: SequenceList {
6493 main_image_sequences: vec![MainImageSequence {
6494 id: uuid(12),
6495 track_id: track_a,
6496 resource_list: ResourceList {
6497 resources: vec![res2],
6498 },
6499 }],
6500 main_audio_sequences: vec![],
6501 subtitles_sequences: vec![],
6502 hearing_impaired_captions_sequences: vec![],
6503 forced_narrative_sequences: vec![],
6504 marker_sequences: vec![],
6505 iab_sequences: vec![],
6506 isxd_sequences: vec![],
6507 },
6508 });
6509
6510 let issues = CoreConstraints2020.validate_cpl(&cpl);
6511 assert!(
6512 issues
6513 .iter()
6514 .any(|i| i.code.contains("VirtualTrackEditRate")),
6515 "Edit rate mismatch in virtual track should be flagged: {:#?}",
6516 issues,
6517 );
6518 }
6519
6520 #[test]
6525 fn core_accepts_absent_edit_rate_when_matches_cpl_rate() {
6526 use crate::cpl::Segment;
6527
6528 let mut cpl = minimal_cpl();
6529 cpl.edit_rate = Some(EditRate::new(24, 1));
6530 let track_a = uuid(4);
6531
6532 let mut res1 = make_resource(Some(uuid(20)));
6534 res1.id = uuid(91);
6535 res1.edit_rate = Some(EditRate::new(24, 1));
6536
6537 cpl.segment_list.segments[0]
6538 .sequence_list
6539 .main_image_sequences
6540 .push(MainImageSequence {
6541 id: uuid(10),
6542 track_id: track_a,
6543 resource_list: ResourceList {
6544 resources: vec![res1],
6545 },
6546 });
6547
6548 let mut res2 = make_resource(Some(uuid(22)));
6550 res2.id = uuid(92);
6551 res2.edit_rate = None;
6552
6553 cpl.segment_list.segments.push(Segment {
6554 id: uuid(30),
6555 sequence_list: SequenceList {
6556 main_image_sequences: vec![MainImageSequence {
6557 id: uuid(12),
6558 track_id: track_a,
6559 resource_list: ResourceList {
6560 resources: vec![res2],
6561 },
6562 }],
6563 main_audio_sequences: vec![],
6564 subtitles_sequences: vec![],
6565 hearing_impaired_captions_sequences: vec![],
6566 forced_narrative_sequences: vec![],
6567 marker_sequences: vec![],
6568 iab_sequences: vec![],
6569 isxd_sequences: vec![],
6570 },
6571 });
6572
6573 let issues = CoreConstraints2020.validate_cpl(&cpl);
6574 assert!(
6575 !issues
6576 .iter()
6577 .any(|i| i.code.contains("VirtualTrackEditRate")),
6578 "Absent EditRate matching CPL rate must not be flagged: {:#?}",
6579 issues,
6580 );
6581 }
6582
6583 #[test]
6589 fn core_accepts_equal_real_time_duration_across_edit_rates() {
6590 let mut cpl = minimal_cpl();
6591 let mut sl = empty_sequence_list();
6592 let er_video = EditRate {
6593 numerator: 24,
6594 denominator: 1,
6595 };
6596 let er_audio = EditRate {
6597 numerator: 48_000,
6598 denominator: 1,
6599 };
6600 sl.main_image_sequences.push(MainImageSequence {
6602 id: uuid(3),
6603 track_id: uuid(4),
6604 resource_list: ResourceList {
6605 resources: vec![Resource {
6606 id: uuid(20),
6607 annotation: None,
6608 edit_rate: Some(er_video),
6609 intrinsic_duration: 240,
6610 entry_point: None,
6611 source_duration: None,
6612 source_encoding: None,
6613 track_file_id: Some(uuid(50)),
6614 repeat_count: None,
6615 key_id: None,
6616 hash: None,
6617 markers: vec![],
6618 }],
6619 },
6620 });
6621 sl.main_audio_sequences.push(MainAudioSequence {
6623 id: uuid(5),
6624 track_id: uuid(6),
6625 resource_list: ResourceList {
6626 resources: vec![Resource {
6627 id: uuid(21),
6628 annotation: None,
6629 edit_rate: Some(er_audio),
6630 intrinsic_duration: 480_000,
6631 entry_point: None,
6632 source_duration: None,
6633 source_encoding: None,
6634 track_file_id: Some(uuid(51)),
6635 repeat_count: None,
6636 key_id: None,
6637 hash: None,
6638 markers: vec![],
6639 }],
6640 },
6641 });
6642 cpl.segment_list.segments[0].sequence_list = sl;
6643
6644 let issues = CoreConstraints2020.validate_cpl(&cpl);
6645 assert!(
6646 !issues.iter().any(|i| i.code.contains("SegmentDuration")),
6647 "Equal real-time duration across different edit rates must not be flagged: {:#?}",
6648 issues,
6649 );
6650 }
6651
6652 #[test]
6653 fn core_rejects_marker_offset_beyond_duration() {
6654 use crate::cpl::MarkerLabel;
6655 use crate::cpl::{MarkerInfo, MarkerLabelElement, MarkerSequence};
6656
6657 let mut cpl = minimal_cpl();
6658 cpl.segment_list.segments[0]
6659 .sequence_list
6660 .marker_sequences
6661 .push(MarkerSequence {
6662 id: uuid(60),
6663 track_id: uuid(61),
6664 resource_list: ResourceList {
6665 resources: vec![Resource {
6666 id: uuid(62),
6667 annotation: None,
6668 edit_rate: None,
6669 intrinsic_duration: 100,
6670 entry_point: Some(10),
6671 source_duration: Some(50), source_encoding: None,
6673 track_file_id: None,
6674 repeat_count: None,
6675 key_id: None,
6676 hash: None,
6677 markers: vec![MarkerInfo {
6678 annotation: None,
6679 label: MarkerLabelElement::from(MarkerLabel::Ffoc),
6680 offset: 60, }],
6682 }],
6683 },
6684 });
6685
6686 let issues = CoreConstraints2020.validate_cpl(&cpl);
6687 assert!(
6688 issues.iter().any(|i| i.code.contains("MarkerOffset")),
6689 "Marker offset beyond resource duration should be flagged: {:#?}",
6690 issues,
6691 );
6692 }
6693
6694 #[test]
6695 fn core_accepts_marker_at_valid_offset() {
6696 use crate::cpl::MarkerLabel;
6697 use crate::cpl::{MarkerInfo, MarkerLabelElement, MarkerSequence};
6698
6699 let mut cpl = minimal_cpl();
6700 cpl.segment_list.segments[0]
6701 .sequence_list
6702 .marker_sequences
6703 .push(MarkerSequence {
6704 id: uuid(60),
6705 track_id: uuid(61),
6706 resource_list: ResourceList {
6707 resources: vec![Resource {
6708 id: uuid(62),
6709 annotation: None,
6710 edit_rate: None,
6711 intrinsic_duration: 100,
6712 entry_point: None,
6713 source_duration: Some(100),
6714 source_encoding: None,
6715 track_file_id: None,
6716 repeat_count: None,
6717 key_id: None,
6718 hash: None,
6719 markers: vec![MarkerInfo {
6720 annotation: None,
6721 label: MarkerLabelElement::from(MarkerLabel::Ffoc),
6722 offset: 0, }],
6724 }],
6725 },
6726 });
6727
6728 let issues = CoreConstraints2020.validate_cpl(&cpl);
6729 assert!(
6730 !issues.iter().any(|i| i.code.contains("MarkerOffset")),
6731 "Marker at offset 0 should not be flagged: {:#?}",
6732 issues,
6733 );
6734 }
6735
6736 #[test]
6737 fn core_rejects_duplicate_essence_descriptor_ids() {
6738 let mut cpl = minimal_cpl();
6739 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
6740 essence_descriptors: vec![
6741 EssenceDescriptor {
6742 id: uuid(10),
6743 rgba_descriptor: None,
6744 cdci_descriptor: None,
6745 wave_pcm_descriptor: None,
6746 dc_timed_text_descriptor: None,
6747 iab_essence_descriptor: None,
6748 isxd_data_essence_descriptor: None,
6749 },
6750 EssenceDescriptor {
6751 id: uuid(10), rgba_descriptor: None,
6753 cdci_descriptor: None,
6754 wave_pcm_descriptor: None,
6755 dc_timed_text_descriptor: None,
6756 iab_essence_descriptor: None,
6757 isxd_data_essence_descriptor: None,
6758 },
6759 ],
6760 });
6761 let issues = CoreConstraints2020.validate_cpl(&cpl);
6762 assert!(
6763 issues
6764 .iter()
6765 .any(|i| i.code.contains("UniqueEssenceDescriptorId")),
6766 "Duplicate EssenceDescriptor IDs should be flagged: {:#?}",
6767 issues,
6768 );
6769 }
6770
6771 #[test]
6776 fn core_accepts_equal_track_durations_in_segment() {
6777 let mut cpl = minimal_cpl();
6778 let mut sl = empty_sequence_list();
6779 sl.main_image_sequences.push(MainImageSequence {
6781 id: uuid(3),
6782 track_id: uuid(4),
6783 resource_list: ResourceList {
6784 resources: vec![Resource {
6785 id: uuid(20),
6786 annotation: None,
6787 edit_rate: None,
6788 intrinsic_duration: 100,
6789 entry_point: None,
6790 source_duration: None,
6791 source_encoding: None,
6792 track_file_id: Some(uuid(50)),
6793 repeat_count: None,
6794 key_id: None,
6795 hash: None,
6796 markers: vec![],
6797 }],
6798 },
6799 });
6800 sl.main_audio_sequences.push(MainAudioSequence {
6802 id: uuid(5),
6803 track_id: uuid(6),
6804 resource_list: ResourceList {
6805 resources: vec![Resource {
6806 id: uuid(21),
6807 annotation: None,
6808 edit_rate: None,
6809 intrinsic_duration: 100,
6810 entry_point: None,
6811 source_duration: None,
6812 source_encoding: None,
6813 track_file_id: Some(uuid(51)),
6814 repeat_count: None,
6815 key_id: None,
6816 hash: None,
6817 markers: vec![],
6818 }],
6819 },
6820 });
6821 cpl.segment_list.segments[0].sequence_list = sl;
6822
6823 let issues = CoreConstraints2020.validate_cpl(&cpl);
6824 assert!(
6825 !issues.iter().any(|i| i.code.contains("SegmentDuration")),
6826 "Equal durations should not produce duration mismatch: {:#?}",
6827 issues,
6828 );
6829 }
6830
6831 #[test]
6832 fn core_detects_mismatched_track_durations_in_segment() {
6833 let mut cpl = minimal_cpl();
6834 let mut sl = empty_sequence_list();
6835 let er_video = EditRate {
6836 numerator: 24,
6837 denominator: 1,
6838 };
6839 let er_audio = EditRate {
6840 numerator: 48000,
6841 denominator: 1,
6842 };
6843 sl.main_image_sequences.push(MainImageSequence {
6845 id: uuid(3),
6846 track_id: uuid(4),
6847 resource_list: ResourceList {
6848 resources: vec![Resource {
6849 id: uuid(20),
6850 annotation: None,
6851 edit_rate: Some(er_video),
6852 intrinsic_duration: 100,
6853 entry_point: None,
6854 source_duration: None,
6855 source_encoding: None,
6856 track_file_id: Some(uuid(50)),
6857 repeat_count: None,
6858 key_id: None,
6859 hash: None,
6860 markers: vec![],
6861 }],
6862 },
6863 });
6864 sl.main_audio_sequences.push(MainAudioSequence {
6866 id: uuid(5),
6867 track_id: uuid(6),
6868 resource_list: ResourceList {
6869 resources: vec![Resource {
6870 id: uuid(21),
6871 annotation: None,
6872 edit_rate: Some(er_audio),
6873 intrinsic_duration: 96_000,
6874 entry_point: None,
6875 source_duration: None,
6876 source_encoding: None,
6877 track_file_id: Some(uuid(51)),
6878 repeat_count: None,
6879 key_id: None,
6880 hash: None,
6881 markers: vec![],
6882 }],
6883 },
6884 });
6885 cpl.segment_list.segments[0].sequence_list = sl;
6886
6887 let issues = CoreConstraints2020.validate_cpl(&cpl);
6888 assert!(
6889 issues.iter().any(|i| i.code.contains("SegmentDuration")),
6890 "Mismatched durations should be flagged: {:#?}",
6891 issues,
6892 );
6893 }
6894
6895 #[test]
6896 fn core_accounts_for_entry_point_and_source_duration() {
6897 let mut cpl = minimal_cpl();
6898 let mut sl = empty_sequence_list();
6899 sl.main_image_sequences.push(MainImageSequence {
6901 id: uuid(3),
6902 track_id: uuid(4),
6903 resource_list: ResourceList {
6904 resources: vec![Resource {
6905 id: uuid(20),
6906 annotation: None,
6907 edit_rate: None,
6908 intrinsic_duration: 200,
6909 entry_point: Some(50),
6910 source_duration: Some(100),
6911 source_encoding: None,
6912 track_file_id: Some(uuid(50)),
6913 repeat_count: None,
6914 key_id: None,
6915 hash: None,
6916 markers: vec![],
6917 }],
6918 },
6919 });
6920 sl.main_audio_sequences.push(MainAudioSequence {
6922 id: uuid(5),
6923 track_id: uuid(6),
6924 resource_list: ResourceList {
6925 resources: vec![Resource {
6926 id: uuid(21),
6927 annotation: None,
6928 edit_rate: None,
6929 intrinsic_duration: 100,
6930 entry_point: None,
6931 source_duration: None,
6932 source_encoding: None,
6933 track_file_id: Some(uuid(51)),
6934 repeat_count: None,
6935 key_id: None,
6936 hash: None,
6937 markers: vec![],
6938 }],
6939 },
6940 });
6941 cpl.segment_list.segments[0].sequence_list = sl;
6942
6943 let issues = CoreConstraints2020.validate_cpl(&cpl);
6944 assert!(
6945 !issues.iter().any(|i| i.code.contains("SegmentDuration")),
6946 "Entry point + source duration should compute correct effective duration: {:#?}",
6947 issues,
6948 );
6949 }
6950
6951 #[test]
6952 fn core_accounts_for_repeat_count_in_duration() {
6953 let mut cpl = minimal_cpl();
6954 let mut sl = empty_sequence_list();
6955 sl.main_image_sequences.push(MainImageSequence {
6957 id: uuid(3),
6958 track_id: uuid(4),
6959 resource_list: ResourceList {
6960 resources: vec![Resource {
6961 id: uuid(20),
6962 annotation: None,
6963 edit_rate: None,
6964 intrinsic_duration: 50,
6965 entry_point: None,
6966 source_duration: None,
6967 source_encoding: None,
6968 track_file_id: Some(uuid(50)),
6969 repeat_count: Some(2),
6970 key_id: None,
6971 hash: None,
6972 markers: vec![],
6973 }],
6974 },
6975 });
6976 sl.main_audio_sequences.push(MainAudioSequence {
6978 id: uuid(5),
6979 track_id: uuid(6),
6980 resource_list: ResourceList {
6981 resources: vec![Resource {
6982 id: uuid(21),
6983 annotation: None,
6984 edit_rate: None,
6985 intrinsic_duration: 100,
6986 entry_point: None,
6987 source_duration: None,
6988 source_encoding: None,
6989 track_file_id: Some(uuid(51)),
6990 repeat_count: None,
6991 key_id: None,
6992 hash: None,
6993 markers: vec![],
6994 }],
6995 },
6996 });
6997 cpl.segment_list.segments[0].sequence_list = sl;
6998
6999 let issues = CoreConstraints2020.validate_cpl(&cpl);
7000 assert!(
7001 !issues.iter().any(|i| i.code.contains("SegmentDuration")),
7002 "RepeatCount should be factored into duration: {:#?}",
7003 issues,
7004 );
7005 }
7006
7007 fn cpl_with_audio(wave: WAVEPCMDescriptor) -> CompositionPlaylist {
7012 let mut cpl = minimal_cpl();
7013 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
7014 essence_descriptors: vec![EssenceDescriptor {
7015 id: uuid(10),
7016 rgba_descriptor: None,
7017 cdci_descriptor: None,
7018 wave_pcm_descriptor: Some(wave),
7019 dc_timed_text_descriptor: None,
7020 iab_essence_descriptor: None,
7021 isxd_data_essence_descriptor: None,
7022 }],
7023 });
7024 cpl
7025 }
7026
7027 #[test]
7028 fn audio_warns_missing_mca_sub_descriptors() {
7029 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7030 instance_id: None,
7031 sample_rate: None,
7032 audio_sample_rate: Some(EditRate::new(48000, 1)),
7033 channel_count: Some(6),
7034 quantization_bits: Some(24),
7035 linked_track_id: None,
7036 sub_descriptors: None, });
7038 let issues = CoreConstraints2020.validate_cpl(&cpl);
7039 assert!(
7040 issues.iter().any(|i| i.code.contains("MCASubDescriptors")),
7041 "Missing MCA sub-descriptors should produce warning: {:#?}",
7042 issues,
7043 );
7044 }
7045
7046 #[test]
7047 fn audio_warns_missing_soundfield_group() {
7048 use crate::cpl::AudioSubDescriptors;
7049
7050 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7051 instance_id: None,
7052 sample_rate: None,
7053 audio_sample_rate: Some(EditRate::new(48000, 1)),
7054 channel_count: Some(6),
7055 quantization_bits: Some(24),
7056 linked_track_id: None,
7057 sub_descriptors: Some(AudioSubDescriptors {
7058 soundfield_group_label_sub_descriptor: None, }),
7060 });
7061 let issues = CoreConstraints2020.validate_cpl(&cpl);
7062 assert!(
7063 issues.iter().any(|i| i.code.contains("SoundfieldGroup")),
7064 "Missing soundfield group should produce warning: {:#?}",
7065 issues,
7066 );
7067 }
7068
7069 #[test]
7070 fn audio_flags_channel_count_mismatch() {
7071 use crate::cpl::McaTagSymbol;
7072 use crate::cpl::{AudioSubDescriptors, SoundfieldGroupLabelSubDescriptor};
7073
7074 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7075 instance_id: None,
7076 sample_rate: None,
7077 audio_sample_rate: Some(EditRate::new(48000, 1)),
7078 channel_count: Some(2), quantization_bits: Some(24),
7080 linked_track_id: None,
7081 sub_descriptors: Some(AudioSubDescriptors {
7082 soundfield_group_label_sub_descriptor: Some(SoundfieldGroupLabelSubDescriptor {
7083 mca_tag_symbol: Some(McaTagSymbol::Sg51), mca_tag_name: Some("5.1".to_string()),
7085 mca_audio_content_kind: None,
7086 rfc5646_spoken_language: None,
7087 }),
7088 }),
7089 });
7090 let issues = CoreConstraints2020.validate_cpl(&cpl);
7091 assert!(
7092 issues
7093 .iter()
7094 .any(|i| i.code.contains("SoundfieldChannelCount")),
7095 "Channel count mismatch with soundfield should be flagged: {:#?}",
7096 issues,
7097 );
7098 }
7099
7100 #[test]
7101 fn audio_accepts_correct_51_channel_count() {
7102 use crate::cpl::McaTagSymbol;
7103 use crate::cpl::{AudioSubDescriptors, SoundfieldGroupLabelSubDescriptor};
7104
7105 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7106 instance_id: None,
7107 sample_rate: None,
7108 audio_sample_rate: Some(EditRate::new(48000, 1)),
7109 channel_count: Some(6), quantization_bits: Some(24),
7111 linked_track_id: None,
7112 sub_descriptors: Some(AudioSubDescriptors {
7113 soundfield_group_label_sub_descriptor: Some(SoundfieldGroupLabelSubDescriptor {
7114 mca_tag_symbol: Some(McaTagSymbol::Sg51),
7115 mca_tag_name: Some("5.1".to_string()),
7116 mca_audio_content_kind: None,
7117 rfc5646_spoken_language: None,
7118 }),
7119 }),
7120 });
7121 let issues = CoreConstraints2020.validate_cpl(&cpl);
7122 assert!(
7123 !issues
7124 .iter()
7125 .any(|i| i.code.contains("SoundfieldChannelCount")),
7126 "Correct 5.1 channel count should not be flagged: {:#?}",
7127 issues,
7128 );
7129 }
7130
7131 #[test]
7132 fn audio_flags_zero_channel_count() {
7133 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7134 instance_id: None,
7135 sample_rate: None,
7136 audio_sample_rate: Some(EditRate::new(48000, 1)),
7137 channel_count: Some(0), quantization_bits: Some(24),
7139 linked_track_id: None,
7140 sub_descriptors: None,
7141 });
7142 let issues = CoreConstraints2020.validate_cpl(&cpl);
7143 assert!(
7144 issues
7145 .iter()
7146 .any(|i| i.code.contains("ChannelCount") && i.severity == Severity::Error),
7147 "Zero ChannelCount should be an error: {:#?}",
7148 issues,
7149 );
7150 }
7151
7152 #[test]
7153 fn audio_accepts_stereo_with_correct_channel_count() {
7154 use crate::cpl::McaTagSymbol;
7155 use crate::cpl::{AudioSubDescriptors, SoundfieldGroupLabelSubDescriptor};
7156
7157 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7158 instance_id: None,
7159 sample_rate: None,
7160 audio_sample_rate: Some(EditRate::new(48000, 1)),
7161 channel_count: Some(2),
7162 quantization_bits: Some(24),
7163 linked_track_id: None,
7164 sub_descriptors: Some(AudioSubDescriptors {
7165 soundfield_group_label_sub_descriptor: Some(SoundfieldGroupLabelSubDescriptor {
7166 mca_tag_symbol: Some(McaTagSymbol::SgSt),
7167 mca_tag_name: Some("Stereo".to_string()),
7168 mca_audio_content_kind: None,
7169 rfc5646_spoken_language: None,
7170 }),
7171 }),
7172 });
7173 let issues = CoreConstraints2020.validate_cpl(&cpl);
7174 assert!(
7175 !issues
7176 .iter()
7177 .any(|i| i.code.contains("SoundfieldChannelCount")),
7178 "Correct Stereo channel count should not be flagged: {:#?}",
7179 issues,
7180 );
7181 }
7182
7183 #[test]
7188 fn app2e_accepts_24bit_audio() {
7189 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7190 instance_id: None,
7191 sample_rate: None,
7192 audio_sample_rate: Some(EditRate::new(48000, 1)),
7193 channel_count: Some(2),
7194 quantization_bits: Some(24),
7195 linked_track_id: None,
7196 sub_descriptors: None,
7197 });
7198 let issues = App2E2021.validate_cpl(&cpl);
7199 assert!(
7200 !issues.iter().any(|i| i.code.contains("QuantizationBits")),
7201 "24-bit audio should be accepted: {:#?}",
7202 issues,
7203 );
7204 }
7205
7206 #[test]
7207 fn app2e_accepts_16bit_audio() {
7208 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7209 instance_id: None,
7210 sample_rate: None,
7211 audio_sample_rate: Some(EditRate::new(48000, 1)),
7212 channel_count: Some(2),
7213 quantization_bits: Some(16),
7214 linked_track_id: None,
7215 sub_descriptors: None,
7216 });
7217 let issues = App2E2021.validate_cpl(&cpl);
7218 assert!(
7219 !issues.iter().any(|i| i.code.contains("QuantizationBits")),
7220 "16-bit audio should be accepted: {:#?}",
7221 issues,
7222 );
7223 }
7224
7225 #[test]
7226 fn app2e_rejects_8bit_audio() {
7227 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7228 instance_id: None,
7229 sample_rate: None,
7230 audio_sample_rate: Some(EditRate::new(48000, 1)),
7231 channel_count: Some(2),
7232 quantization_bits: Some(8), linked_track_id: None,
7234 sub_descriptors: None,
7235 });
7236 let issues = App2E2021.validate_cpl(&cpl);
7237 assert!(
7238 issues.iter().any(|i| i.code.contains("QuantizationBits")),
7239 "8-bit audio should be rejected: {:#?}",
7240 issues,
7241 );
7242 }
7243
7244 #[test]
7245 fn app2e_rejects_32bit_audio() {
7246 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7247 instance_id: None,
7248 sample_rate: None,
7249 audio_sample_rate: Some(EditRate::new(48000, 1)),
7250 channel_count: Some(2),
7251 quantization_bits: Some(32), linked_track_id: None,
7253 sub_descriptors: None,
7254 });
7255 let issues = App2E2021.validate_cpl(&cpl);
7256 assert!(
7257 issues.iter().any(|i| i.code.contains("QuantizationBits")),
7258 "32-bit audio should be rejected: {:#?}",
7259 issues,
7260 );
7261 }
7262
7263 fn cpl_with_image_resolution(
7266 width: u32,
7267 height: u32,
7268 rate: Option<EditRate>,
7269 ) -> CompositionPlaylist {
7270 let ed_id = uuid(10);
7271 let mut cpl = minimal_cpl();
7272 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
7273 essence_descriptors: vec![EssenceDescriptor {
7274 id: ed_id,
7275 rgba_descriptor: None,
7276 cdci_descriptor: Some(CDCIDescriptor {
7277 instance_id: None,
7278 stored_width: Some(width),
7279 stored_height: Some(height),
7280 display_width: Some(width),
7281 display_height: Some(height),
7282 sample_rate: rate,
7283 image_aspect_ratio: None,
7284 color_primaries: Some(ColorPrimaries::Bt709),
7285 transfer_characteristic: Some(TransferCharacteristic::Bt709),
7286 coding_equations: Some(CodingEquations::Bt709),
7287 picture_compression: Some(VideoCodec::Jpeg2000),
7288 component_depth: Some(10),
7289 frame_layout: Some("FullFrame".to_string()),
7290 display_f2_offset: None,
7291 horizontal_subsampling: Some(2),
7292 vertical_subsampling: Some(1),
7293 color_siting: Some(0),
7294 black_ref_level: Some(64),
7295 white_ref_level: Some(940),
7296 color_range: Some(897),
7297 stored_f2_offset: None,
7298 sampled_width: None,
7299 sampled_height: None,
7300 sampled_x_offset: None,
7301 sampled_y_offset: None,
7302 alpha_transparency: None,
7303 image_alignment_offset: None,
7304 image_start_offset: None,
7305 image_end_offset: None,
7306 field_dominance: None,
7307 reversed_byte_order: None,
7308 padding_bits: None,
7309 alpha_sample_depth: None,
7310 linked_track_id: None,
7311 active_width: None,
7312 active_height: None,
7313 sub_descriptors: None,
7314 }),
7315 wave_pcm_descriptor: None,
7316 dc_timed_text_descriptor: None,
7317 iab_essence_descriptor: None,
7318 isxd_data_essence_descriptor: None,
7319 }],
7320 });
7321 let mut sl = empty_sequence_list();
7322 sl.main_image_sequences.push(MainImageSequence {
7323 id: uuid(3),
7324 track_id: uuid(4),
7325 resource_list: ResourceList {
7326 resources: vec![make_resource(Some(ed_id))],
7327 },
7328 });
7329 cpl.segment_list.segments[0].sequence_list = sl;
7330 cpl
7331 }
7332
7333 #[test]
7334 fn app2e_accepts_1920x1080() {
7335 let cpl = cpl_with_image_resolution(1920, 1080, Some(EditRate::new(24, 1)));
7336 let issues = App2E2021.validate_cpl(&cpl);
7337 assert!(
7338 !issues.iter().any(|i| i.code.contains("Resolution")),
7339 "1920x1080 should be accepted: {:#?}",
7340 issues,
7341 );
7342 }
7343
7344 #[test]
7345 fn app2e_accepts_2048x1080() {
7346 let cpl = cpl_with_image_resolution(2048, 1080, Some(EditRate::new(25, 1)));
7347 let issues = App2E2021.validate_cpl(&cpl);
7348 assert!(
7349 !issues.iter().any(|i| i.code.contains("Resolution")),
7350 "2048x1080 should be accepted: {:#?}",
7351 issues,
7352 );
7353 }
7354
7355 #[test]
7356 fn app2e_accepts_3840x2160() {
7357 let cpl = cpl_with_image_resolution(3840, 2160, Some(EditRate::new(24, 1)));
7358 let issues = App2E2021.validate_cpl(&cpl);
7359 assert!(
7360 !issues.iter().any(|i| i.code.contains("Resolution")),
7361 "3840x2160 should be accepted: {:#?}",
7362 issues,
7363 );
7364 }
7365
7366 #[test]
7367 fn app2e_accepts_4096x2160() {
7368 let cpl = cpl_with_image_resolution(4096, 2160, Some(EditRate::new(24000, 1001)));
7369 let issues = App2E2021.validate_cpl(&cpl);
7370 assert!(
7371 !issues.iter().any(|i| i.code.contains("Resolution")),
7372 "4096x2160 should be accepted: {:#?}",
7373 issues,
7374 );
7375 }
7376
7377 #[test]
7378 fn app2e_accepts_7680x4320() {
7379 let cpl = cpl_with_image_resolution(7680, 4320, Some(EditRate::new(24, 1)));
7380 let issues = App2E2021.validate_cpl(&cpl);
7381 assert!(
7382 !issues.iter().any(|i| i.code.contains("Resolution")),
7383 "7680x4320 should be accepted: {:#?}",
7384 issues,
7385 );
7386 }
7387
7388 #[test]
7389 fn app2e_rejects_1280x720() {
7390 let cpl = cpl_with_image_resolution(1280, 720, Some(EditRate::new(24, 1)));
7391 let issues = App2E2021.validate_cpl(&cpl);
7392 assert!(
7393 issues.iter().any(|i| i.code.contains("Resolution")),
7394 "1280x720 should be rejected: {:#?}",
7395 issues,
7396 );
7397 }
7398
7399 #[test]
7400 fn app2e_rejects_1920x800() {
7401 let cpl = cpl_with_image_resolution(1920, 800, Some(EditRate::new(24, 1)));
7402 let issues = App2E2021.validate_cpl(&cpl);
7403 assert!(
7404 issues.iter().any(|i| i.code.contains("Resolution")),
7405 "1920x800 should be rejected: {:#?}",
7406 issues,
7407 );
7408 }
7409
7410 #[test]
7413 fn app2e_accepts_24fps() {
7414 let cpl = cpl_with_image_resolution(1920, 1080, Some(EditRate::new(24, 1)));
7415 let issues = App2E2021.validate_cpl(&cpl);
7416 assert!(
7417 !issues.iter().any(|i| i.code.contains("FrameRate")),
7418 "24 fps should be accepted: {:#?}",
7419 issues,
7420 );
7421 }
7422
7423 #[test]
7424 fn app2e_accepts_23976fps() {
7425 let cpl = cpl_with_image_resolution(1920, 1080, Some(EditRate::new(24000, 1001)));
7426 let issues = App2E2021.validate_cpl(&cpl);
7427 assert!(
7428 !issues.iter().any(|i| i.code.contains("FrameRate")),
7429 "23.976 fps should be accepted: {:#?}",
7430 issues,
7431 );
7432 }
7433
7434 #[test]
7435 fn app2e_accepts_25fps() {
7436 let cpl = cpl_with_image_resolution(1920, 1080, Some(EditRate::new(25, 1)));
7437 let issues = App2E2021.validate_cpl(&cpl);
7438 assert!(
7439 !issues.iter().any(|i| i.code.contains("FrameRate")),
7440 "25 fps should be accepted: {:#?}",
7441 issues,
7442 );
7443 }
7444
7445 #[test]
7446 fn app2e_accepts_60fps() {
7447 let cpl = cpl_with_image_resolution(3840, 2160, Some(EditRate::new(60, 1)));
7448 let issues = App2E2021.validate_cpl(&cpl);
7449 assert!(
7450 !issues.iter().any(|i| i.code.contains("FrameRate")),
7451 "60 fps should be accepted: {:#?}",
7452 issues,
7453 );
7454 }
7455
7456 #[test]
7457 fn app2e_accepts_5994fps() {
7458 let cpl = cpl_with_image_resolution(3840, 2160, Some(EditRate::new(60000, 1001)));
7459 let issues = App2E2021.validate_cpl(&cpl);
7460 assert!(
7461 !issues.iter().any(|i| i.code.contains("FrameRate")),
7462 "59.94 fps should be accepted: {:#?}",
7463 issues,
7464 );
7465 }
7466
7467 #[test]
7468 fn app2e_rejects_120fps() {
7469 let cpl = cpl_with_image_resolution(1920, 1080, Some(EditRate::new(120, 1)));
7470 let issues = App2E2021.validate_cpl(&cpl);
7471 assert!(
7472 issues.iter().any(|i| i.code.contains("FrameRate")),
7473 "120 fps should be rejected: {:#?}",
7474 issues,
7475 );
7476 }
7477
7478 #[test]
7479 fn app2e_rejects_15fps() {
7480 let cpl = cpl_with_image_resolution(1920, 1080, Some(EditRate::new(15, 1)));
7481 let issues = App2E2021.validate_cpl(&cpl);
7482 assert!(
7483 issues.iter().any(|i| i.code.contains("FrameRate")),
7484 "15 fps should be rejected: {:#?}",
7485 issues,
7486 );
7487 }
7488
7489 #[test]
7492 fn app2e_accepts_48000hz_audio() {
7493 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7494 instance_id: None,
7495 sample_rate: None,
7496 audio_sample_rate: Some(EditRate::new(48000, 1)),
7497 channel_count: Some(2),
7498 quantization_bits: Some(24),
7499 linked_track_id: None,
7500 sub_descriptors: None,
7501 });
7502 let issues = App2E2021.validate_cpl(&cpl);
7503 assert!(
7504 !issues.iter().any(|i| i.code.contains("AudioSampleRate")),
7505 "48000 Hz should be accepted: {:#?}",
7506 issues,
7507 );
7508 }
7509
7510 #[test]
7511 fn app2e_rejects_44100hz_audio() {
7512 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7513 instance_id: None,
7514 sample_rate: None,
7515 audio_sample_rate: Some(EditRate::new(44100, 1)),
7516 channel_count: Some(2),
7517 quantization_bits: Some(24),
7518 linked_track_id: None,
7519 sub_descriptors: None,
7520 });
7521 let issues = App2E2021.validate_cpl(&cpl);
7522 assert!(
7523 issues.iter().any(|i| i.code.contains("AudioSampleRate")),
7524 "44100 Hz should be rejected: {:#?}",
7525 issues,
7526 );
7527 }
7528
7529 #[test]
7530 fn app2e_rejects_96000hz_audio() {
7531 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7532 instance_id: None,
7533 sample_rate: None,
7534 audio_sample_rate: Some(EditRate::new(96000, 1)),
7535 channel_count: Some(2),
7536 quantization_bits: Some(24),
7537 linked_track_id: None,
7538 sub_descriptors: None,
7539 });
7540 let issues = App2E2021.validate_cpl(&cpl);
7541 assert!(
7542 issues.iter().any(|i| i.code.contains("AudioSampleRate")),
7543 "96000 Hz should be rejected: {:#?}",
7544 issues,
7545 );
7546 }
7547
7548 #[test]
7553 fn app2e_flags_cdci_missing_sample_rate() {
7554 let ed_id = uuid(10);
7555 let mut cpl = minimal_cpl();
7556 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
7557 essence_descriptors: vec![EssenceDescriptor {
7558 id: ed_id,
7559 rgba_descriptor: None,
7560 cdci_descriptor: Some(CDCIDescriptor {
7561 instance_id: None,
7562 stored_width: Some(1920),
7563 stored_height: Some(1080),
7564 sample_rate: None, image_aspect_ratio: None,
7566 color_primaries: Some(ColorPrimaries::Bt709),
7567 transfer_characteristic: Some(TransferCharacteristic::Bt709),
7568 coding_equations: Some(CodingEquations::Bt709),
7569 picture_compression: Some(VideoCodec::Jpeg2000),
7570 component_depth: Some(10),
7571 frame_layout: Some("FullFrame".to_string()),
7572 display_width: None,
7573 display_height: None,
7574 display_f2_offset: None,
7575 horizontal_subsampling: Some(2),
7576 vertical_subsampling: Some(1),
7577 color_siting: Some(0),
7578 black_ref_level: Some(64),
7579 white_ref_level: Some(940),
7580 color_range: Some(897),
7581 stored_f2_offset: None,
7582 sampled_width: None,
7583 sampled_height: None,
7584 sampled_x_offset: None,
7585 sampled_y_offset: None,
7586 alpha_transparency: None,
7587 image_alignment_offset: None,
7588 image_start_offset: None,
7589 image_end_offset: None,
7590 field_dominance: None,
7591 reversed_byte_order: None,
7592 padding_bits: None,
7593 alpha_sample_depth: None,
7594 linked_track_id: None,
7595 active_width: None,
7596 active_height: None,
7597 sub_descriptors: None,
7598 }),
7599 wave_pcm_descriptor: None,
7600 dc_timed_text_descriptor: None,
7601 iab_essence_descriptor: None,
7602 isxd_data_essence_descriptor: None,
7603 }],
7604 });
7605
7606 let issues = App2E2021.validate_cpl(&cpl);
7607 assert!(
7608 issues
7609 .iter()
7610 .any(|i| i.code == St2067_21_2023::RequiredSampleRate.code()),
7611 "Missing SampleRate should be flagged: {:#?}",
7612 issues,
7613 );
7614 }
7615
7616 #[test]
7617 fn app2e_flags_wave_missing_channel_count() {
7618 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7619 instance_id: None,
7620 sample_rate: None,
7621 audio_sample_rate: Some(EditRate::new(48000, 1)),
7622 channel_count: None, quantization_bits: Some(24),
7624 linked_track_id: None,
7625 sub_descriptors: None,
7626 });
7627 let issues = App2E2021.validate_cpl(&cpl);
7628 assert!(
7629 issues
7630 .iter()
7631 .any(|i| i.code == St2067_21_2023::RequiredChannelCount.code()),
7632 "Missing ChannelCount should be flagged: {:#?}",
7633 issues,
7634 );
7635 }
7636
7637 #[test]
7638 fn app2e_flags_wave_missing_quantization_bits() {
7639 let cpl = cpl_with_audio(WAVEPCMDescriptor {
7640 instance_id: None,
7641 sample_rate: None,
7642 audio_sample_rate: Some(EditRate::new(48000, 1)),
7643 channel_count: Some(2),
7644 quantization_bits: None, linked_track_id: None,
7646 sub_descriptors: None,
7647 });
7648 let issues = App2E2021.validate_cpl(&cpl);
7649 assert!(
7650 issues
7651 .iter()
7652 .any(|i| i.code == St2067_21_2023::RequiredQuantizationBits.code()),
7653 "Missing QuantizationBits should be flagged: {:#?}",
7654 issues,
7655 );
7656 }
7657
7658 #[test]
7672 fn core_flags_missing_edit_rate() {
7673 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
7674 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
7675 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
7676 <ContentTitle>Test</ContentTitle>
7677 <SegmentList><Segment>
7678 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
7679 <SequenceList/>
7680 </Segment></SegmentList>
7681 </CompositionPlaylist>"#;
7682 let cpl = crate::cpl::parse_cpl(xml).expect("should parse despite missing EditRate");
7683 let issues = CoreConstraints2013.validate_cpl(&cpl);
7684 assert!(
7685 issues.iter().any(|i| i.code.contains("EditRate")),
7686 "Missing EditRate should be flagged: {:#?}",
7687 issues,
7688 );
7689 }
7690
7691 #[test]
7693 fn core_accepts_present_edit_rate() {
7694 let mut cpl = minimal_cpl();
7695 cpl.edit_rate = Some(EditRate::new(24, 1));
7696 cpl.segment_list.segments[0]
7698 .sequence_list
7699 .main_image_sequences
7700 .push(MainImageSequence {
7701 id: uuid(3),
7702 track_id: uuid(4),
7703 resource_list: ResourceList {
7704 resources: vec![make_resource(None)],
7705 },
7706 });
7707 let v = CoreConstraints2020;
7708 let issues = v.validate_cpl(&cpl);
7709 assert!(
7710 !issues.iter().any(|i| i.code.contains("-EditRate")),
7711 "Present EditRate should not be flagged: {:#?}",
7712 issues,
7713 );
7714 }
7715
7716 #[test]
7719 fn core_warns_invalid_issue_date_format() {
7720 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
7721 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
7722 <IssueDate>not-a-date</IssueDate>
7723 <ContentTitle>Test</ContentTitle>
7724 <EditRate>24 1</EditRate>
7725 <SegmentList><Segment>
7726 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
7727 <SequenceList/>
7728 </Segment></SegmentList>
7729 </CompositionPlaylist>"#;
7730 let cpl = crate::cpl::parse_cpl(xml).expect("should parse despite invalid IssueDate");
7731 let issues = CoreConstraints2013.validate_cpl(&cpl);
7732 assert!(
7733 issues.iter().any(|i| i.code.contains("IssueDate")),
7734 "Invalid IssueDate format should be flagged: {:#?}",
7735 issues,
7736 );
7737 }
7738
7739 #[test]
7741 fn core_flags_empty_issue_date() {
7742 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
7743 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
7744 <IssueDate></IssueDate>
7745 <ContentTitle>Test</ContentTitle>
7746 <EditRate>24 1</EditRate>
7747 <SegmentList><Segment>
7748 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
7749 <SequenceList/>
7750 </Segment></SegmentList>
7751 </CompositionPlaylist>"#;
7752 let cpl = crate::cpl::parse_cpl(xml).expect("should parse despite empty IssueDate");
7753 let issues = CoreConstraints2013.validate_cpl(&cpl);
7754 assert!(
7755 issues.iter().any(|i| i.code.contains("IssueDate")),
7756 "Empty IssueDate should be flagged: {:#?}",
7757 issues,
7758 );
7759 }
7760
7761 #[test]
7767 fn core_flags_incomplete_composition_timecode() {
7768 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
7769 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
7770 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
7771 <ContentTitle>Test</ContentTitle>
7772 <CompositionTimecode>
7773 <TimecodeRate>24</TimecodeRate>
7774 </CompositionTimecode>
7775 <EditRate>24 1</EditRate>
7776 <SegmentList><Segment>
7777 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
7778 <SequenceList/>
7779 </Segment></SegmentList>
7780 </CompositionPlaylist>"#;
7781 let cpl = crate::cpl::parse_cpl(xml)
7782 .expect("should parse despite incomplete CompositionTimecode");
7783 let issues = CoreConstraints2013.validate_cpl(&cpl);
7784 assert!(
7785 issues
7786 .iter()
7787 .any(|i| i.code.contains("TimecodeDropFrame")
7788 || i.code.contains("TimecodeStartAddress")),
7789 "Missing TimecodeDropFrame/StartAddress should be flagged: {:#?}",
7790 issues,
7791 );
7792 }
7793
7794 #[test]
7800 fn core_flags_empty_resource_list() {
7801 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
7802 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
7803 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
7804 <ContentTitle>Test</ContentTitle>
7805 <EditRate>24 1</EditRate>
7806 <SegmentList><Segment>
7807 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
7808 <SequenceList>
7809 <MarkerSequence>
7810 <Id>urn:uuid:00000000-0000-0000-0000-000000000003</Id>
7811 <TrackId>urn:uuid:00000000-0000-0000-0000-000000000004</TrackId>
7812 <ResourceList/>
7813 </MarkerSequence>
7814 </SequenceList>
7815 </Segment></SegmentList>
7816 </CompositionPlaylist>"#;
7817 let cpl = crate::cpl::parse_cpl(xml).expect("should parse despite empty ResourceList");
7818 let issues = CoreConstraints2013.validate_cpl(&cpl);
7819 assert!(
7820 issues.iter().any(|i| i.code.contains("Resource")),
7821 "Empty ResourceList should be flagged: {:#?}",
7822 issues,
7823 );
7824 }
7825
7826 #[test]
7830 fn core_accepts_content_version_with_label_text() {
7831 let mut cpl = minimal_cpl();
7832 cpl.content_version_list = Some(ContentVersionList {
7833 content_versions: vec![ContentVersion {
7834 id: "urn:uuid:00000000-0000-0000-0000-000000000099".to_string(),
7835 label_text: Some(LanguageString {
7836 text: "Version 1".to_string(),
7837 language: Some(LanguageTag::new("en")),
7838 }),
7839 }],
7840 });
7841 let v = CoreConstraints2020;
7842 let issues = v.validate_cpl(&cpl);
7843 assert!(
7844 !issues
7845 .iter()
7846 .any(|i| i.code.contains("ContentVersionLabelTextMissing")),
7847 "ContentVersion with LabelText should not be flagged: {:#?}",
7848 issues,
7849 );
7850 }
7851
7852 #[test]
7854 fn core_flags_content_version_missing_label_text() {
7855 let mut cpl = minimal_cpl();
7856 cpl.content_version_list = Some(ContentVersionList {
7857 content_versions: vec![ContentVersion {
7858 id: "urn:uuid:00000000-0000-0000-0000-000000000099".to_string(),
7859 label_text: None,
7860 }],
7861 });
7862 let v = CoreConstraints2020;
7863 let issues = v.validate_cpl(&cpl);
7864 assert!(
7865 issues
7866 .iter()
7867 .any(|i| i.code.contains("ContentVersionLabelTextMissing")),
7868 "Missing LabelText should be flagged: {:#?}",
7869 issues,
7870 );
7871 }
7872
7873 #[test]
7877 fn core_accepts_recognized_marker_labels() {
7878 let mut cpl = minimal_cpl();
7879 cpl.segment_list.segments[0]
7880 .sequence_list
7881 .marker_sequences
7882 .push(MarkerSequence {
7883 id: uuid(20),
7884 track_id: uuid(21),
7885 resource_list: ResourceList {
7886 resources: vec![Resource {
7887 id: uuid(22),
7888 annotation: None,
7889 edit_rate: None,
7890 intrinsic_duration: 100,
7891 entry_point: None,
7892 source_duration: None,
7893 source_encoding: None,
7894 track_file_id: None,
7895 repeat_count: None,
7896 key_id: None,
7897 hash: None,
7898 markers: vec![
7899 MarkerInfo {
7900 annotation: None,
7901 label: MarkerLabelElement::from(MarkerLabel::Ffoc),
7902 offset: 0,
7903 },
7904 MarkerInfo {
7905 annotation: None,
7906 label: MarkerLabelElement::from(MarkerLabel::Lfoc),
7907 offset: 99,
7908 },
7909 ],
7910 }],
7911 },
7912 });
7913 let v = CoreConstraints2020;
7914 let issues = v.validate_cpl(&cpl);
7915 assert!(
7916 !issues.iter().any(|i| i.code.contains("MarkerLabelUnknown")),
7917 "Recognized marker labels should not be flagged: {:#?}",
7918 issues,
7919 );
7920 }
7921
7922 #[test]
7924 fn core_flags_unrecognized_marker_label_under_smpte_scope() {
7925 let mut cpl = minimal_cpl();
7926 cpl.segment_list.segments[0]
7927 .sequence_list
7928 .marker_sequences
7929 .push(MarkerSequence {
7930 id: uuid(20),
7931 track_id: uuid(21),
7932 resource_list: ResourceList {
7933 resources: vec![Resource {
7934 id: uuid(22),
7935 annotation: None,
7936 edit_rate: None,
7937 intrinsic_duration: 100,
7938 entry_point: None,
7939 source_duration: None,
7940 source_encoding: None,
7941 track_file_id: None,
7942 repeat_count: None,
7943 key_id: None,
7944 hash: None,
7945 markers: vec![MarkerInfo {
7946 annotation: None,
7947 label: MarkerLabelElement::from(MarkerLabel::Other(
7948 "CUSTOM_MARKER".to_string(),
7949 )),
7950 offset: 0,
7951 }],
7952 }],
7953 },
7954 });
7955 let v = CoreConstraints2020;
7956 let issues = v.validate_cpl(&cpl);
7957 assert!(
7958 issues.iter().any(|i| i.code.contains("MarkerLabelUnknown")),
7959 "Unrecognized marker label under SMPTE scope should be flagged: {:#?}",
7960 issues,
7961 );
7962 }
7963
7964 #[test]
7966 fn core_accepts_custom_marker_label_under_custom_scope() {
7967 let mut cpl = minimal_cpl();
7968 cpl.segment_list.segments[0]
7969 .sequence_list
7970 .marker_sequences
7971 .push(MarkerSequence {
7972 id: uuid(20),
7973 track_id: uuid(21),
7974 resource_list: ResourceList {
7975 resources: vec![Resource {
7976 id: uuid(22),
7977 annotation: None,
7978 edit_rate: None,
7979 intrinsic_duration: 100,
7980 entry_point: None,
7981 source_duration: None,
7982 source_encoding: None,
7983 track_file_id: None,
7984 repeat_count: None,
7985 key_id: None,
7986 hash: None,
7987 markers: vec![MarkerInfo {
7988 annotation: None,
7989 label: MarkerLabelElement {
7990 label: MarkerLabel::Other("VENDOR_MARKER".to_string()),
7991 scope: Some("http://example.com/markers".to_string()),
7992 },
7993 offset: 0,
7994 }],
7995 }],
7996 },
7997 });
7998 let v = CoreConstraints2020;
7999 let issues = v.validate_cpl(&cpl);
8000 assert!(
8001 !issues.iter().any(|i| i.code.contains("MarkerLabelUnknown")),
8002 "Custom marker under custom scope should not be flagged: {:#?}",
8003 issues,
8004 );
8005 }
8006
8007 #[test]
8011 fn app2e_accepts_progressive_frame_layout() {
8012 let cpl = cpl_with_cdci_descriptor(
8013 ColorPrimaries::Bt709,
8014 TransferCharacteristic::Bt709,
8015 CodingEquations::Bt709,
8016 10,
8017 );
8018 let v = App2E2021;
8019 let issues = v.validate_cpl(&cpl);
8020 assert!(
8021 !issues
8022 .iter()
8023 .any(|i| i.code == St2067_21_2023::FrameLayoutInterlaced.code()),
8024 "FullFrame FrameLayout should not be flagged: {:#?}",
8025 issues,
8026 );
8027 }
8028
8029 #[test]
8031 fn app2e_flags_interlaced_frame_layout() {
8032 let ed_id = uuid(10);
8033 let mut cpl = minimal_cpl();
8034 cpl.edit_rate = Some(EditRate::new(24, 1));
8035 cpl.extension_properties = Some(ExtensionProperties {
8036 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8037 max_cll: None,
8038 max_fall: None,
8039 });
8040 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8041 essence_descriptors: vec![EssenceDescriptor {
8042 id: ed_id,
8043 rgba_descriptor: None,
8044 cdci_descriptor: Some(CDCIDescriptor {
8045 instance_id: None,
8046 stored_width: Some(1920),
8047 stored_height: Some(1080),
8048 display_width: Some(1920),
8049 display_height: Some(1080),
8050 sample_rate: Some(EditRate::new(24, 1)),
8051 image_aspect_ratio: None,
8052 color_primaries: Some(ColorPrimaries::Bt709),
8053 transfer_characteristic: Some(TransferCharacteristic::Bt709),
8054 coding_equations: Some(CodingEquations::Bt709),
8055 picture_compression: Some(VideoCodec::Jpeg2000),
8056 component_depth: Some(10),
8057 frame_layout: Some("SeparateFields".to_string()),
8058 display_f2_offset: None,
8059 horizontal_subsampling: Some(2),
8060 vertical_subsampling: Some(1),
8061 color_siting: Some(0),
8062 black_ref_level: Some(64),
8063 white_ref_level: Some(940),
8064 color_range: Some(897),
8065 stored_f2_offset: None,
8066 sampled_width: None,
8067 sampled_height: None,
8068 sampled_x_offset: None,
8069 sampled_y_offset: None,
8070 alpha_transparency: None,
8071 image_alignment_offset: None,
8072 image_start_offset: None,
8073 image_end_offset: None,
8074 field_dominance: Some(1),
8075 reversed_byte_order: None,
8076 padding_bits: None,
8077 alpha_sample_depth: None,
8078 linked_track_id: None,
8079 active_width: None,
8080 active_height: None,
8081 sub_descriptors: None,
8082 }),
8083 wave_pcm_descriptor: None,
8084 iab_essence_descriptor: None,
8085 dc_timed_text_descriptor: None,
8086 isxd_data_essence_descriptor: None,
8087 }],
8088 });
8089 let v = App2E2021;
8090 let issues = v.validate_cpl(&cpl);
8091 assert!(
8092 issues
8093 .iter()
8094 .any(|i| i.code == St2067_21_2023::FrameLayoutInterlaced.code()),
8095 "SeparateFields FrameLayout should be flagged for App2E: {:#?}",
8096 issues,
8097 );
8098 }
8099
8100 #[test]
8104 fn core_accepts_matching_timecode_rate() {
8105 let mut cpl = minimal_cpl();
8106 cpl.edit_rate = Some(EditRate::new(24, 1));
8107 cpl.composition_timecode = Some(CompositionTimecode {
8108 timecode_drop_frame: Some(false),
8109 timecode_rate: Some(24),
8110 timecode_start_address: Some("00:00:00:00".to_string()),
8111 });
8112 let v = CoreConstraints2020;
8113 let issues = v.validate_cpl(&cpl);
8114 assert!(
8115 !issues.iter().any(|i| i.code.contains("Rate-Mismatch")),
8116 "Matching TimecodeRate and EditRate should not be flagged: {:#?}",
8117 issues,
8118 );
8119 }
8120
8121 #[test]
8123 fn core_flags_mismatched_timecode_rate() {
8124 let mut cpl = minimal_cpl();
8125 cpl.edit_rate = Some(EditRate::new(24, 1));
8126 cpl.composition_timecode = Some(CompositionTimecode {
8127 timecode_drop_frame: Some(false),
8128 timecode_rate: Some(25), timecode_start_address: Some("00:00:00:00".to_string()),
8130 });
8131 let v = CoreConstraints2020;
8132 let issues = v.validate_cpl(&cpl);
8133 assert!(
8134 issues.iter().any(|i| i.code.contains("Rate-Mismatch")),
8135 "Mismatched TimecodeRate and EditRate should be flagged: {:#?}",
8136 issues,
8137 );
8138 }
8139
8140 #[test]
8142 fn core_accepts_timecode_rate_for_23976fps() {
8143 let mut cpl = minimal_cpl();
8144 cpl.edit_rate = Some(EditRate::new(24000, 1001)); cpl.composition_timecode = Some(CompositionTimecode {
8146 timecode_drop_frame: Some(true),
8147 timecode_rate: Some(24), timecode_start_address: Some("00:00:00;00".to_string()),
8149 });
8150 let v = CoreConstraints2020;
8151 let issues = v.validate_cpl(&cpl);
8152 assert!(
8153 !issues.iter().any(|i| i.code.contains("Rate-Mismatch")),
8154 "TimecodeRate 24 should match EditRate 24000/1001 (23.976 fps): {:#?}",
8155 issues,
8156 );
8157 }
8158
8159 #[test]
8163 fn core_accepts_valid_source_encoding_ref() {
8164 let ed_id = uuid(10);
8165 let mut cpl = minimal_cpl();
8166 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8167 essence_descriptors: vec![EssenceDescriptor {
8168 id: ed_id,
8169 rgba_descriptor: None,
8170 cdci_descriptor: None,
8171 wave_pcm_descriptor: None,
8172 iab_essence_descriptor: None,
8173 dc_timed_text_descriptor: Some(DCTimedTextDescriptor {
8174 instance_id: None,
8175 linked_track_id: None,
8176 sample_rate: None,
8177 namespace_uri: Some("http://www.w3.org/ns/ttml".to_string()),
8178 rfc5646_language_tag_list: vec![],
8179 }),
8180 isxd_data_essence_descriptor: None,
8181 }],
8182 });
8183 cpl.segment_list.segments[0]
8184 .sequence_list
8185 .subtitles_sequences
8186 .push(SubtitlesSequence {
8187 id: uuid(20),
8188 track_id: uuid(21),
8189 resource_list: ResourceList {
8190 resources: vec![Resource {
8191 id: uuid(22),
8192 annotation: None,
8193 edit_rate: None,
8194 intrinsic_duration: 100,
8195 entry_point: None,
8196 source_duration: None,
8197 source_encoding: Some(ed_id),
8198 track_file_id: Some(uuid(50)),
8199 repeat_count: None,
8200 key_id: None,
8201 hash: None,
8202 markers: vec![],
8203 }],
8204 },
8205 });
8206 let v = CoreConstraints2020;
8207 let issues = v.validate_cpl(&cpl);
8208 assert!(
8209 !issues.iter().any(|i| i.code.contains("SourceEncoding")),
8210 "Valid SourceEncoding ref should not be flagged: {:#?}",
8211 issues,
8212 );
8213 }
8214
8215 #[test]
8217 fn core_flags_unresolved_source_encoding_ref() {
8218 let ed_id = uuid(10);
8219 let bad_ref = uuid(99); let mut cpl = minimal_cpl();
8221 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8222 essence_descriptors: vec![EssenceDescriptor {
8223 id: ed_id,
8224 rgba_descriptor: None,
8225 cdci_descriptor: None,
8226 wave_pcm_descriptor: None,
8227 iab_essence_descriptor: None,
8228 dc_timed_text_descriptor: None,
8229 isxd_data_essence_descriptor: None,
8230 }],
8231 });
8232 cpl.segment_list.segments[0]
8233 .sequence_list
8234 .main_image_sequences
8235 .push(MainImageSequence {
8236 id: uuid(20),
8237 track_id: uuid(21),
8238 resource_list: ResourceList {
8239 resources: vec![Resource {
8240 id: uuid(22),
8241 annotation: None,
8242 edit_rate: None,
8243 intrinsic_duration: 100,
8244 entry_point: None,
8245 source_duration: None,
8246 source_encoding: Some(bad_ref),
8247 track_file_id: Some(uuid(50)),
8248 repeat_count: None,
8249 key_id: None,
8250 hash: None,
8251 markers: vec![],
8252 }],
8253 },
8254 });
8255 let v = CoreConstraints2020;
8256 let issues = v.validate_cpl(&cpl);
8257 assert!(
8258 issues
8259 .iter()
8260 .any(|i| i.code.contains("SourceEncodingUnresolved")),
8261 "Unresolved SourceEncoding should be flagged: {:#?}",
8262 issues,
8263 );
8264 }
8265
8266 #[test]
8270 fn core_accepts_known_content_kind() {
8271 let mut cpl = minimal_cpl();
8272 cpl.content_kind = ContentKindElement::from(ContentKind::Feature);
8273 let v = CoreConstraints2020;
8274 let issues = v.validate_cpl(&cpl);
8275 assert!(
8276 !issues.iter().any(|i| i.code.contains("ContentKindUnknown")),
8277 "Known ContentKind should not be flagged: {:#?}",
8278 issues,
8279 );
8280 }
8281
8282 #[test]
8284 fn core_flags_unknown_content_kind_under_smpte_scope() {
8285 let mut cpl = minimal_cpl();
8286 cpl.content_kind =
8287 ContentKindElement::from(ContentKind::Other("SomeFutureKind".to_string()));
8288 let v = CoreConstraints2020;
8289 let issues = v.validate_cpl(&cpl);
8290 assert!(
8291 issues.iter().any(|i| i.code.contains("ContentKindUnknown")),
8292 "Unknown ContentKind under SMPTE scope should be flagged: {:#?}",
8293 issues,
8294 );
8295 }
8296
8297 #[test]
8299 fn core_accepts_custom_content_kind_under_custom_scope() {
8300 let mut cpl = minimal_cpl();
8301 cpl.content_kind = ContentKindElement {
8302 kind: ContentKind::Other("VendorSpecific".to_string()),
8303 scope: Some("http://example.com/content-kinds".to_string()),
8304 };
8305 let v = CoreConstraints2020;
8306 let issues = v.validate_cpl(&cpl);
8307 assert!(
8308 !issues.iter().any(|i| i.code.contains("ContentKindUnknown")),
8309 "Custom ContentKind under custom scope should not be flagged: {:#?}",
8310 issues,
8311 );
8312 }
8313
8314 #[test]
8318 fn app2e_accepts_homogeneous_audio_channels() {
8319 let mut cpl = minimal_cpl();
8320 cpl.edit_rate = Some(EditRate::new(24, 1));
8321 cpl.extension_properties = Some(ExtensionProperties {
8322 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8323 max_cll: None,
8324 max_fall: None,
8325 });
8326 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8327 essence_descriptors: vec![
8328 EssenceDescriptor {
8329 id: uuid(10),
8330 rgba_descriptor: None,
8331 cdci_descriptor: None,
8332 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
8333 instance_id: None,
8334 sample_rate: Some(EditRate::new(48000, 1)),
8335 audio_sample_rate: None,
8336 quantization_bits: Some(24),
8337 channel_count: Some(6),
8338 linked_track_id: None,
8339 sub_descriptors: None,
8340 }),
8341 iab_essence_descriptor: None,
8342 dc_timed_text_descriptor: None,
8343 isxd_data_essence_descriptor: None,
8344 },
8345 EssenceDescriptor {
8346 id: uuid(11),
8347 rgba_descriptor: None,
8348 cdci_descriptor: None,
8349 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
8350 instance_id: None,
8351 sample_rate: Some(EditRate::new(48000, 1)),
8352 audio_sample_rate: None,
8353 quantization_bits: Some(24),
8354 channel_count: Some(6),
8355 linked_track_id: None,
8356 sub_descriptors: None,
8357 }),
8358 iab_essence_descriptor: None,
8359 dc_timed_text_descriptor: None,
8360 isxd_data_essence_descriptor: None,
8361 },
8362 ],
8363 });
8364 let v = App2E2021;
8365 let issues = v.validate_cpl(&cpl);
8366 assert!(
8367 !issues.iter().any(|i| i.code.contains("AudioHomogeneity")),
8368 "Homogeneous audio channels should not be flagged: {:#?}",
8369 issues,
8370 );
8371 }
8372
8373 #[test]
8376 fn app2e_accepts_mixed_channel_count_across_tracks() {
8377 let mut cpl = minimal_cpl();
8378 cpl.edit_rate = Some(EditRate::new(24, 1));
8379 cpl.extension_properties = Some(ExtensionProperties {
8380 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8381 max_cll: None,
8382 max_fall: None,
8383 });
8384 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8385 essence_descriptors: vec![
8386 EssenceDescriptor {
8387 id: uuid(10),
8388 rgba_descriptor: None,
8389 cdci_descriptor: None,
8390 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
8391 instance_id: None,
8392 sample_rate: Some(EditRate::new(48000, 1)),
8393 audio_sample_rate: None,
8394 quantization_bits: Some(24),
8395 channel_count: Some(6), linked_track_id: None,
8397 sub_descriptors: None,
8398 }),
8399 iab_essence_descriptor: None,
8400 dc_timed_text_descriptor: None,
8401 isxd_data_essence_descriptor: None,
8402 },
8403 EssenceDescriptor {
8404 id: uuid(11),
8405 rgba_descriptor: None,
8406 cdci_descriptor: None,
8407 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
8408 instance_id: None,
8409 sample_rate: Some(EditRate::new(48000, 1)),
8410 audio_sample_rate: None,
8411 quantization_bits: Some(24),
8412 channel_count: Some(2), linked_track_id: None,
8414 sub_descriptors: None,
8415 }),
8416 iab_essence_descriptor: None,
8417 dc_timed_text_descriptor: None,
8418 isxd_data_essence_descriptor: None,
8419 },
8420 ],
8421 });
8422 let v = App2E2021;
8423 let issues = v.validate_cpl(&cpl);
8424 assert!(
8425 !issues.iter().any(|i| i.code.contains("AudioHomogeneity")),
8426 "Mixed channel counts across separate audio tracks must not be flagged: {:#?}",
8427 issues,
8428 );
8429 }
8430
8431 #[test]
8435 fn app2e_accepts_hic_with_timed_text() {
8436 let ed_id = uuid(10);
8437 let mut cpl = minimal_cpl();
8438 cpl.edit_rate = Some(EditRate::new(24, 1));
8439 cpl.extension_properties = Some(ExtensionProperties {
8440 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8441 max_cll: None,
8442 max_fall: None,
8443 });
8444 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8445 essence_descriptors: vec![EssenceDescriptor {
8446 id: ed_id,
8447 rgba_descriptor: None,
8448 cdci_descriptor: None,
8449 wave_pcm_descriptor: None,
8450 iab_essence_descriptor: None,
8451 dc_timed_text_descriptor: Some(DCTimedTextDescriptor {
8452 instance_id: None,
8453 linked_track_id: None,
8454 sample_rate: None,
8455 namespace_uri: Some("http://www.w3.org/ns/ttml".to_string()),
8456 rfc5646_language_tag_list: vec![LanguageTag::new("en")],
8457 }),
8458 isxd_data_essence_descriptor: None,
8459 }],
8460 });
8461 cpl.segment_list.segments[0]
8462 .sequence_list
8463 .hearing_impaired_captions_sequences
8464 .push(HearingImpairedCaptionsSequence {
8465 id: uuid(20),
8466 track_id: uuid(21),
8467 resource_list: ResourceList {
8468 resources: vec![Resource {
8469 id: uuid(22),
8470 annotation: None,
8471 edit_rate: None,
8472 intrinsic_duration: 100,
8473 entry_point: None,
8474 source_duration: None,
8475 source_encoding: Some(ed_id),
8476 track_file_id: Some(uuid(50)),
8477 repeat_count: None,
8478 key_id: None,
8479 hash: None,
8480 markers: vec![],
8481 }],
8482 },
8483 });
8484 let v = App2E2021;
8485 let issues = v.validate_cpl(&cpl);
8486 assert!(
8487 !issues.iter().any(|i| i.code.contains("5.6-HIC")),
8488 "HIC with timed text should not be flagged: {:#?}",
8489 issues,
8490 );
8491 }
8492
8493 #[test]
8495 fn app2e_flags_hic_without_timed_text() {
8496 let ed_id = uuid(10);
8497 let mut cpl = minimal_cpl();
8498 cpl.edit_rate = Some(EditRate::new(24, 1));
8499 cpl.extension_properties = Some(ExtensionProperties {
8500 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8501 max_cll: None,
8502 max_fall: None,
8503 });
8504 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8505 essence_descriptors: vec![EssenceDescriptor {
8506 id: ed_id,
8507 rgba_descriptor: None,
8508 cdci_descriptor: None,
8509 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
8510 instance_id: None,
8511 sample_rate: Some(EditRate::new(48000, 1)),
8512 quantization_bits: Some(24),
8513 channel_count: Some(2),
8514 audio_sample_rate: None,
8515 linked_track_id: None,
8516 sub_descriptors: None,
8517 }),
8518 iab_essence_descriptor: None,
8519 dc_timed_text_descriptor: None, isxd_data_essence_descriptor: None,
8521 }],
8522 });
8523 cpl.segment_list.segments[0]
8524 .sequence_list
8525 .hearing_impaired_captions_sequences
8526 .push(HearingImpairedCaptionsSequence {
8527 id: uuid(20),
8528 track_id: uuid(21),
8529 resource_list: ResourceList {
8530 resources: vec![Resource {
8531 id: uuid(22),
8532 annotation: None,
8533 edit_rate: None,
8534 intrinsic_duration: 100,
8535 entry_point: None,
8536 source_duration: None,
8537 source_encoding: Some(ed_id),
8538 track_file_id: Some(uuid(50)),
8539 repeat_count: None,
8540 key_id: None,
8541 hash: None,
8542 markers: vec![],
8543 }],
8544 },
8545 });
8546 let v = App2E2021;
8547 let issues = v.validate_cpl(&cpl);
8548 assert!(
8549 issues.iter().any(|i| i.code.contains("5.6/HICTimedText")),
8550 "HIC without timed text should be flagged: {:#?}",
8551 issues,
8552 );
8553 }
8554
8555 #[test]
8557 fn app2e_flags_fn_without_timed_text() {
8558 let ed_id = uuid(10);
8559 let mut cpl = minimal_cpl();
8560 cpl.edit_rate = Some(EditRate::new(24, 1));
8561 cpl.extension_properties = Some(ExtensionProperties {
8562 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8563 max_cll: None,
8564 max_fall: None,
8565 });
8566 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8567 essence_descriptors: vec![EssenceDescriptor {
8568 id: ed_id,
8569 rgba_descriptor: None,
8570 cdci_descriptor: None,
8571 wave_pcm_descriptor: Some(WAVEPCMDescriptor {
8572 instance_id: None,
8573 sample_rate: Some(EditRate::new(48000, 1)),
8574 quantization_bits: Some(24),
8575 channel_count: Some(2),
8576 linked_track_id: None,
8577 sub_descriptors: None,
8578 audio_sample_rate: None,
8579 }),
8580 iab_essence_descriptor: None,
8581 dc_timed_text_descriptor: None,
8582 isxd_data_essence_descriptor: None,
8583 }],
8584 });
8585 cpl.segment_list.segments[0]
8586 .sequence_list
8587 .forced_narrative_sequences
8588 .push(ForcedNarrativeSequence {
8589 id: uuid(20),
8590 track_id: uuid(21),
8591 resource_list: ResourceList {
8592 resources: vec![Resource {
8593 id: uuid(22),
8594 annotation: None,
8595 edit_rate: None,
8596 intrinsic_duration: 100,
8597 entry_point: None,
8598 source_duration: None,
8599 source_encoding: Some(ed_id),
8600 track_file_id: Some(uuid(50)),
8601 repeat_count: None,
8602 key_id: None,
8603 hash: None,
8604 markers: vec![],
8605 }],
8606 },
8607 });
8608 let v = App2E2021;
8609 let issues = v.validate_cpl(&cpl);
8610 assert!(
8611 issues.iter().any(|i| i.code.contains("5.6/FNTimedText")),
8612 "ForcedNarrative without timed text should be flagged: {:#?}",
8613 issues,
8614 );
8615 }
8616
8617 #[test]
8621 fn app2e_flags_missing_application_identification() {
8622 let mut cpl = minimal_cpl();
8623 cpl.edit_rate = Some(EditRate::new(24, 1));
8624 cpl.extension_properties = Some(ExtensionProperties {
8625 application_identification: None,
8626 max_cll: None,
8627 max_fall: None,
8628 });
8629 let v = App2E2021;
8630 let issues = v.validate_cpl(&cpl);
8631 assert!(
8632 issues
8633 .iter()
8634 .any(|i| i.code.contains("7.1/ApplicationIdentification")),
8635 "Missing ApplicationIdentification should be flagged: {:#?}",
8636 issues,
8637 );
8638 }
8639
8640 #[test]
8644 fn app2e_accepts_valid_locale_list() {
8645 let mut cpl = minimal_cpl();
8646 cpl.edit_rate = Some(EditRate::new(24, 1));
8647 cpl.extension_properties = Some(ExtensionProperties {
8648 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8649 max_cll: None,
8650 max_fall: None,
8651 });
8652 cpl.locale_list = Some(LocaleList {
8653 locales: vec![Locale {
8654 language_list: Some(LanguageList {
8655 languages: vec![LanguageTag::new("en"), LanguageTag::new("fr")],
8656 }),
8657 content_maturity_rating_list: None,
8658 region_list: Some(RegionList {
8659 regions: vec!["US".to_string(), "FR".to_string()],
8660 }),
8661 }],
8662 });
8663 let v = App2E2021;
8664 let issues = v.validate_cpl(&cpl);
8665 assert!(
8666 !issues.iter().any(|i| i.code.contains("5.3/")),
8667 "Valid locale should not be flagged: {:#?}",
8668 issues,
8669 );
8670 }
8671
8672 #[test]
8674 fn app2e_flags_invalid_region_code() {
8675 let mut cpl = minimal_cpl();
8676 cpl.edit_rate = Some(EditRate::new(24, 1));
8677 cpl.extension_properties = Some(ExtensionProperties {
8678 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8679 max_cll: None,
8680 max_fall: None,
8681 });
8682 cpl.locale_list = Some(LocaleList {
8683 locales: vec![Locale {
8684 language_list: None,
8685 region_list: Some(RegionList {
8686 regions: vec!["us".to_string()], }),
8688 content_maturity_rating_list: None,
8689 }],
8690 });
8691 let v = App2E2021;
8692 let issues = v.validate_cpl(&cpl);
8693 assert!(
8694 issues.iter().any(|i| i.code.contains("5.3/RegionCode")),
8695 "Invalid region code should be flagged: {:#?}",
8696 issues,
8697 );
8698 }
8699
8700 #[test]
8702 fn app2e_flags_empty_locale_language_tag() {
8703 let mut cpl = minimal_cpl();
8704 cpl.edit_rate = Some(EditRate::new(24, 1));
8705 cpl.extension_properties = Some(ExtensionProperties {
8706 application_identification: Some(APP2E_APPLICATION_IDENTIFICATION.to_string()),
8707 max_cll: None,
8708 max_fall: None,
8709 });
8710 cpl.locale_list = Some(LocaleList {
8711 locales: vec![Locale {
8712 language_list: Some(LanguageList {
8713 languages: vec![LanguageTag::new("")],
8714 }),
8715 region_list: None,
8716 content_maturity_rating_list: None,
8717 }],
8718 });
8719 let v = App2E2021;
8720 let issues = v.validate_cpl(&cpl);
8721 assert!(
8722 issues
8723 .iter()
8724 .any(|i| i.code.contains("5.3/EmptyLanguageTag")),
8725 "Empty language tag should be flagged: {:#?}",
8726 issues,
8727 );
8728 }
8729
8730 #[test]
8734 fn core_warns_timed_text_missing_sample_rate() {
8735 let mut cpl = minimal_cpl();
8736 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8737 essence_descriptors: vec![EssenceDescriptor {
8738 id: uuid(10),
8739 rgba_descriptor: None,
8740 cdci_descriptor: None,
8741 wave_pcm_descriptor: None,
8742 iab_essence_descriptor: None,
8743 dc_timed_text_descriptor: Some(DCTimedTextDescriptor {
8744 instance_id: None,
8745 linked_track_id: None,
8746 sample_rate: None, namespace_uri: Some("http://www.w3.org/ns/ttml".to_string()),
8748 rfc5646_language_tag_list: vec![LanguageTag::new("en")],
8749 }),
8750 isxd_data_essence_descriptor: None,
8751 }],
8752 });
8753 let v = CoreConstraints2020;
8754 let issues = v.validate_cpl(&cpl);
8755 assert!(
8756 issues
8757 .iter()
8758 .any(|i| i.code.contains("TimedText-SampleRate")),
8759 "Missing timed text SampleRate should be warned: {:#?}",
8760 issues,
8761 );
8762 }
8763
8764 #[test]
8766 fn core_accepts_timed_text_with_sample_rate() {
8767 let mut cpl = minimal_cpl();
8768 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8769 essence_descriptors: vec![EssenceDescriptor {
8770 id: uuid(10),
8771 rgba_descriptor: None,
8772 cdci_descriptor: None,
8773 wave_pcm_descriptor: None,
8774 iab_essence_descriptor: None,
8775 dc_timed_text_descriptor: Some(DCTimedTextDescriptor {
8776 instance_id: None,
8777 linked_track_id: None,
8778 sample_rate: Some(EditRate::new(24, 1)),
8779 namespace_uri: Some("http://www.w3.org/ns/ttml".to_string()),
8780 rfc5646_language_tag_list: vec![LanguageTag::new("en")],
8781 }),
8782 isxd_data_essence_descriptor: None,
8783 }],
8784 });
8785 let v = CoreConstraints2020;
8786 let issues = v.validate_cpl(&cpl);
8787 assert!(
8788 !issues
8789 .iter()
8790 .any(|i| i.code.contains("TimedText-SampleRate")),
8791 "Valid timed text SampleRate should not be warned: {:#?}",
8792 issues,
8793 );
8794 }
8795
8796 #[test]
8798 fn core_warns_timed_text_empty_language_tag() {
8799 let mut cpl = minimal_cpl();
8800 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
8801 essence_descriptors: vec![EssenceDescriptor {
8802 id: uuid(10),
8803 rgba_descriptor: None,
8804 cdci_descriptor: None,
8805 wave_pcm_descriptor: None,
8806 iab_essence_descriptor: None,
8807 dc_timed_text_descriptor: Some(DCTimedTextDescriptor {
8808 instance_id: None,
8809 linked_track_id: None,
8810 sample_rate: Some(EditRate::new(24, 1)),
8811 namespace_uri: Some("http://www.w3.org/ns/ttml".to_string()),
8812 rfc5646_language_tag_list: vec![LanguageTag::new("")],
8813 }),
8814 isxd_data_essence_descriptor: None,
8815 }],
8816 });
8817 let v = CoreConstraints2020;
8818 let issues = v.validate_cpl(&cpl);
8819 assert!(
8820 issues
8821 .iter()
8822 .any(|i| i.code.contains("TimedText-EmptyLanguageTag")),
8823 "Empty language tag should be warned: {:#?}",
8824 issues,
8825 );
8826 }
8827
8828 #[test]
8832 fn core_emits_digital_signature_notice_for_modern_cpl() {
8833 let cpl = minimal_cpl(); let v = CoreConstraints2020;
8835 let issues = v.validate_cpl(&cpl);
8836 assert!(
8837 issues
8838 .iter()
8839 .any(|i| i.code.contains("DigitalSignature") && i.severity == Severity::Info),
8840 "2020 CPL should emit digital signature info notice: {:#?}",
8841 issues,
8842 );
8843 }
8844
8845 #[test]
8847 fn core_no_digital_signature_notice_for_2013_cpl() {
8848 let mut cpl = minimal_cpl();
8849 cpl.namespace = CplNamespace::Smpte2067_3_2013;
8850 let v = CoreConstraints2013;
8851 let issues = v.validate_cpl(&cpl);
8852 assert!(
8853 !issues.iter().any(|i| i.code.contains("DigitalSignature")),
8854 "2013 CPL should not emit digital signature notice: {:#?}",
8855 issues,
8856 );
8857 }
8858
8859 #[test]
8865 fn app2e_flags_cdci_sampled_height_mismatch() {
8866 let mut cpl = cpl_with_cdci_descriptor(
8867 ColorPrimaries::Bt709,
8868 TransferCharacteristic::Bt709,
8869 CodingEquations::Bt709,
8870 10,
8871 );
8872 if let Some(ref mut edl) = cpl.essence_descriptor_list {
8873 for ed in &mut edl.essence_descriptors {
8874 if let Some(ref mut cdci) = ed.cdci_descriptor {
8875 cdci.sampled_height = Some(720); }
8877 }
8878 }
8879 let v = App2E2021;
8880 let issues = v.validate_cpl(&cpl);
8881 assert!(
8882 issues
8883 .iter()
8884 .any(|i| i.code.contains("6.2.1/SampledHeight")),
8885 "Should flag SampledHeight ≠ StoredHeight: {:#?}",
8886 issues,
8887 );
8888 }
8889
8890 #[test]
8892 fn app2e_flags_cdci_sampled_y_offset_nonzero() {
8893 let mut cpl = cpl_with_cdci_descriptor(
8894 ColorPrimaries::Bt709,
8895 TransferCharacteristic::Bt709,
8896 CodingEquations::Bt709,
8897 10,
8898 );
8899 if let Some(ref mut edl) = cpl.essence_descriptor_list {
8900 for ed in &mut edl.essence_descriptors {
8901 if let Some(ref mut cdci) = ed.cdci_descriptor {
8902 cdci.sampled_y_offset = Some(1);
8903 }
8904 }
8905 }
8906 let v = App2E2021;
8907 let issues = v.validate_cpl(&cpl);
8908 assert!(
8909 issues
8910 .iter()
8911 .any(|i| i.code.contains("6.2.1/SampledYOffset")),
8912 "Should flag SampledYOffset ≠ 0: {:#?}",
8913 issues,
8914 );
8915 }
8916
8917 #[test]
8919 fn app2e_flags_cdci_sampled_x_offset_nonzero() {
8920 let mut cpl = cpl_with_cdci_descriptor(
8921 ColorPrimaries::Bt709,
8922 TransferCharacteristic::Bt709,
8923 CodingEquations::Bt709,
8924 10,
8925 );
8926 if let Some(ref mut edl) = cpl.essence_descriptor_list {
8927 for ed in &mut edl.essence_descriptors {
8928 if let Some(ref mut cdci) = ed.cdci_descriptor {
8929 cdci.sampled_x_offset = Some(5);
8930 }
8931 }
8932 }
8933 let v = App2E2021;
8934 let issues = v.validate_cpl(&cpl);
8935 assert!(
8936 issues
8937 .iter()
8938 .any(|i| i.code.contains("6.2.1/SampledXOffset")),
8939 "Should flag SampledXOffset ≠ 0: {:#?}",
8940 issues,
8941 );
8942 }
8943
8944 #[test]
8946 fn app2e_flags_cdci_coding_equations_missing() {
8947 let mut cpl = cpl_with_cdci_descriptor(
8948 ColorPrimaries::Bt709,
8949 TransferCharacteristic::Bt709,
8950 CodingEquations::Bt709,
8951 10,
8952 );
8953 if let Some(ref mut edl) = cpl.essence_descriptor_list {
8954 for ed in &mut edl.essence_descriptors {
8955 if let Some(ref mut cdci) = ed.cdci_descriptor {
8956 cdci.coding_equations = None;
8957 }
8958 }
8959 }
8960 let v = App2E2021;
8961 let issues = v.validate_cpl(&cpl);
8962 assert!(
8963 issues
8964 .iter()
8965 .any(|i| i.code.contains("6.2.1/CodingEquations")),
8966 "Should flag missing CodingEquations: {:#?}",
8967 issues,
8968 );
8969 }
8970
8971 #[test]
8973 fn app2e_flags_cdci_coding_equations_unknown() {
8974 let mut cpl = cpl_with_cdci_descriptor(
8975 ColorPrimaries::Bt709,
8976 TransferCharacteristic::Bt709,
8977 CodingEquations::Bt709,
8978 10,
8979 );
8980 if let Some(ref mut edl) = cpl.essence_descriptor_list {
8981 for ed in &mut edl.essence_descriptors {
8982 if let Some(ref mut cdci) = ed.cdci_descriptor {
8983 cdci.coding_equations = Some(CodingEquations::Unknown(
8984 "06.0e.2b.34.04.01.01.0d.04.01.01.01.ff.00.00.00".to_string(),
8985 ));
8986 }
8987 }
8988 }
8989 let v = App2E2021;
8990 let issues = v.validate_cpl(&cpl);
8991 assert!(
8992 issues.iter().any(|i| i.code.contains("6.2.3")),
8993 "Should flag unknown CodingEquations UL: {:#?}",
8994 issues,
8995 );
8996 }
8997
8998 #[test]
9000 fn app2e_flags_cdci_transfer_characteristic_missing() {
9001 let mut cpl = cpl_with_cdci_descriptor(
9002 ColorPrimaries::Bt709,
9003 TransferCharacteristic::Bt709,
9004 CodingEquations::Bt709,
9005 10,
9006 );
9007 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9008 for ed in &mut edl.essence_descriptors {
9009 if let Some(ref mut cdci) = ed.cdci_descriptor {
9010 cdci.transfer_characteristic = None;
9011 }
9012 }
9013 }
9014 let v = App2E2021;
9015 let issues = v.validate_cpl(&cpl);
9016 assert!(
9017 issues
9018 .iter()
9019 .any(|i| i.code.contains("6.2.1/TransferCharacteristic")),
9020 "Should flag missing TransferCharacteristic: {:#?}",
9021 issues,
9022 );
9023 }
9024
9025 #[test]
9027 fn app2e_flags_cdci_transfer_characteristic_unknown() {
9028 let mut cpl = cpl_with_cdci_descriptor(
9029 ColorPrimaries::Bt709,
9030 TransferCharacteristic::Bt709,
9031 CodingEquations::Bt709,
9032 10,
9033 );
9034 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9035 for ed in &mut edl.essence_descriptors {
9036 if let Some(ref mut cdci) = ed.cdci_descriptor {
9037 cdci.transfer_characteristic = Some(TransferCharacteristic::Unknown(
9038 "06.0e.2b.34.04.01.01.0d.04.01.01.01.ff.00.00.00".to_string(),
9039 ));
9040 }
9041 }
9042 }
9043 let v = App2E2021;
9044 let issues = v.validate_cpl(&cpl);
9045 assert!(
9046 issues.iter().any(|i| i.code.contains("6.2.2")),
9047 "Should flag unknown TransferCharacteristic UL: {:#?}",
9048 issues,
9049 );
9050 }
9051
9052 #[test]
9054 fn app2e_flags_cdci_color_primaries_missing() {
9055 let mut cpl = cpl_with_cdci_descriptor(
9056 ColorPrimaries::Bt709,
9057 TransferCharacteristic::Bt709,
9058 CodingEquations::Bt709,
9059 10,
9060 );
9061 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9062 for ed in &mut edl.essence_descriptors {
9063 if let Some(ref mut cdci) = ed.cdci_descriptor {
9064 cdci.color_primaries = None;
9065 }
9066 }
9067 }
9068 let v = App2E2021;
9069 let issues = v.validate_cpl(&cpl);
9070 assert!(
9071 issues
9072 .iter()
9073 .any(|i| i.code.contains("6.2.1/ColorPrimaries")),
9074 "Should flag missing ColorPrimaries: {:#?}",
9075 issues,
9076 );
9077 }
9078
9079 #[test]
9081 fn app2e_flags_cdci_color_primaries_unknown() {
9082 let mut cpl = cpl_with_cdci_descriptor(
9083 ColorPrimaries::Bt709,
9084 TransferCharacteristic::Bt709,
9085 CodingEquations::Bt709,
9086 10,
9087 );
9088 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9089 for ed in &mut edl.essence_descriptors {
9090 if let Some(ref mut cdci) = ed.cdci_descriptor {
9091 cdci.color_primaries = Some(ColorPrimaries::Unknown(
9092 "06.0e.2b.34.04.01.01.0d.04.01.01.01.ff.00.00.00".to_string(),
9093 ));
9094 }
9095 }
9096 }
9097 let v = App2E2021;
9098 let issues = v.validate_cpl(&cpl);
9099 assert!(
9100 issues.iter().any(|i| i.code.contains("6.2.4")),
9101 "Should flag unknown ColorPrimaries UL: {:#?}",
9102 issues,
9103 );
9104 }
9105
9106 #[test]
9112 fn app2e_flags_cdci_horizontal_subsampling_invalid() {
9113 let mut cpl = cpl_with_cdci_descriptor(
9114 ColorPrimaries::Bt709,
9115 TransferCharacteristic::Bt709,
9116 CodingEquations::Bt709,
9117 10,
9118 );
9119 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9120 for ed in &mut edl.essence_descriptors {
9121 if let Some(ref mut cdci) = ed.cdci_descriptor {
9122 cdci.horizontal_subsampling = Some(3);
9123 }
9124 }
9125 }
9126 let v = App2E2021;
9127 let issues = v.validate_cpl(&cpl);
9128 assert!(
9129 issues
9130 .iter()
9131 .any(|i| i.code.contains("6.4/HorizontalSubsampling")),
9132 "Should flag HorizontalSubsampling=3: {:#?}",
9133 issues,
9134 );
9135 }
9136
9137 #[test]
9139 fn app2e_flags_cdci_horizontal_subsampling_missing() {
9140 let mut cpl = cpl_with_cdci_descriptor(
9141 ColorPrimaries::Bt709,
9142 TransferCharacteristic::Bt709,
9143 CodingEquations::Bt709,
9144 10,
9145 );
9146 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9147 for ed in &mut edl.essence_descriptors {
9148 if let Some(ref mut cdci) = ed.cdci_descriptor {
9149 cdci.horizontal_subsampling = None;
9150 }
9151 }
9152 }
9153 let v = App2E2021;
9154 let issues = v.validate_cpl(&cpl);
9155 assert!(
9156 issues
9157 .iter()
9158 .any(|i| i.code.contains("6.4/HorizontalSubsampling")),
9159 "Should flag missing HorizontalSubsampling: {:#?}",
9160 issues,
9161 );
9162 }
9163
9164 #[test]
9166 fn app2e_flags_cdci_color_siting_nonzero() {
9167 let mut cpl = cpl_with_cdci_descriptor(
9168 ColorPrimaries::Bt709,
9169 TransferCharacteristic::Bt709,
9170 CodingEquations::Bt709,
9171 10,
9172 );
9173 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9174 for ed in &mut edl.essence_descriptors {
9175 if let Some(ref mut cdci) = ed.cdci_descriptor {
9176 cdci.color_siting = Some(3);
9177 }
9178 }
9179 }
9180 let v = App2E2021;
9181 let issues = v.validate_cpl(&cpl);
9182 assert!(
9183 issues.iter().any(|i| i.code.contains("6.4/ColorSiting")),
9184 "Should flag ColorSiting ≠ 0: {:#?}",
9185 issues,
9186 );
9187 }
9188
9189 #[test]
9191 fn app2e_flags_cdci_color_siting_missing() {
9192 let mut cpl = cpl_with_cdci_descriptor(
9193 ColorPrimaries::Bt709,
9194 TransferCharacteristic::Bt709,
9195 CodingEquations::Bt709,
9196 10,
9197 );
9198 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9199 for ed in &mut edl.essence_descriptors {
9200 if let Some(ref mut cdci) = ed.cdci_descriptor {
9201 cdci.color_siting = None;
9202 }
9203 }
9204 }
9205 let v = App2E2021;
9206 let issues = v.validate_cpl(&cpl);
9207 assert!(
9208 issues.iter().any(|i| i.code.contains("6.4/ColorSiting")),
9209 "Should flag missing ColorSiting: {:#?}",
9210 issues,
9211 );
9212 }
9213
9214 #[test]
9216 fn app2e_flags_cdci_vertical_subsampling_invalid() {
9217 let mut cpl = cpl_with_cdci_descriptor(
9218 ColorPrimaries::Bt709,
9219 TransferCharacteristic::Bt709,
9220 CodingEquations::Bt709,
9221 10,
9222 );
9223 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9224 for ed in &mut edl.essence_descriptors {
9225 if let Some(ref mut cdci) = ed.cdci_descriptor {
9226 cdci.vertical_subsampling = Some(2);
9227 }
9228 }
9229 }
9230 let v = App2E2021;
9231 let issues = v.validate_cpl(&cpl);
9232 assert!(
9233 issues
9234 .iter()
9235 .any(|i| i.code.contains("6.4/VerticalSubsampling")),
9236 "Should flag VerticalSubsampling ≠ 1: {:#?}",
9237 issues,
9238 );
9239 }
9240
9241 #[test]
9243 fn app2e_flags_cdci_vertical_subsampling_missing() {
9244 let mut cpl = cpl_with_cdci_descriptor(
9245 ColorPrimaries::Bt709,
9246 TransferCharacteristic::Bt709,
9247 CodingEquations::Bt709,
9248 10,
9249 );
9250 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9251 for ed in &mut edl.essence_descriptors {
9252 if let Some(ref mut cdci) = ed.cdci_descriptor {
9253 cdci.vertical_subsampling = None;
9254 }
9255 }
9256 }
9257 let v = App2E2021;
9258 let issues = v.validate_cpl(&cpl);
9259 assert!(
9260 issues
9261 .iter()
9262 .any(|i| i.code.contains("6.4/VerticalSubsampling")),
9263 "Should flag missing VerticalSubsampling: {:#?}",
9264 issues,
9265 );
9266 }
9267
9268 #[test]
9270 fn app2e_flags_cdci_component_depth_invalid() {
9271 let mut cpl = cpl_with_cdci_descriptor(
9272 ColorPrimaries::Bt709,
9273 TransferCharacteristic::Bt709,
9274 CodingEquations::Bt709,
9275 10,
9276 );
9277 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9278 for ed in &mut edl.essence_descriptors {
9279 if let Some(ref mut cdci) = ed.cdci_descriptor {
9280 cdci.component_depth = Some(14);
9281 }
9282 }
9283 }
9284 let v = App2E2021;
9285 let issues = v.validate_cpl(&cpl);
9286 assert!(
9287 issues.iter().any(|i| i.code.contains("6.4/ComponentDepth")),
9288 "Should flag ComponentDepth=14: {:#?}",
9289 issues,
9290 );
9291 }
9292
9293 #[test]
9295 fn app2e_flags_cdci_component_depth_missing() {
9296 let mut cpl = cpl_with_cdci_descriptor(
9297 ColorPrimaries::Bt709,
9298 TransferCharacteristic::Bt709,
9299 CodingEquations::Bt709,
9300 10,
9301 );
9302 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9303 for ed in &mut edl.essence_descriptors {
9304 if let Some(ref mut cdci) = ed.cdci_descriptor {
9305 cdci.component_depth = None;
9306 }
9307 }
9308 }
9309 let v = App2E2021;
9310 let issues = v.validate_cpl(&cpl);
9311 assert!(
9312 issues.iter().any(|i| i.code.contains("6.4/ComponentDepth")),
9313 "Should flag missing ComponentDepth: {:#?}",
9314 issues,
9315 );
9316 }
9317
9318 #[test]
9324 fn app2e_flags_rgba_component_max_ref_missing() {
9325 let mut cpl =
9326 cpl_with_rgba_descriptor(ColorPrimaries::P3D65, TransferCharacteristic::PqSt2084);
9327 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9328 for ed in &mut edl.essence_descriptors {
9329 if let Some(ref mut rgba) = ed.rgba_descriptor {
9330 rgba.component_max_ref = None;
9331 }
9332 }
9333 }
9334 let v = App2E2021;
9335 let issues = v.validate_cpl(&cpl);
9336 assert!(
9337 issues
9338 .iter()
9339 .any(|i| i.code.contains("6.3/ComponentMaxRef")),
9340 "Should flag missing ComponentMaxRef: {:#?}",
9341 issues,
9342 );
9343 }
9344
9345 #[test]
9347 fn app2e_flags_rgba_component_min_ref_missing() {
9348 let mut cpl =
9349 cpl_with_rgba_descriptor(ColorPrimaries::P3D65, TransferCharacteristic::PqSt2084);
9350 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9351 for ed in &mut edl.essence_descriptors {
9352 if let Some(ref mut rgba) = ed.rgba_descriptor {
9353 rgba.component_min_ref = None;
9354 }
9355 }
9356 }
9357 let v = App2E2021;
9358 let issues = v.validate_cpl(&cpl);
9359 assert!(
9360 issues
9361 .iter()
9362 .any(|i| i.code.contains("6.3/ComponentMinRef")),
9363 "Should flag missing ComponentMinRef: {:#?}",
9364 issues,
9365 );
9366 }
9367
9368 #[test]
9370 fn app2e_flags_rgba_scanning_direction_missing() {
9371 let mut cpl =
9372 cpl_with_rgba_descriptor(ColorPrimaries::P3D65, TransferCharacteristic::PqSt2084);
9373 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9374 for ed in &mut edl.essence_descriptors {
9375 if let Some(ref mut rgba) = ed.rgba_descriptor {
9376 rgba.scanning_direction = None;
9377 }
9378 }
9379 }
9380 let v = App2E2021;
9381 let issues = v.validate_cpl(&cpl);
9382 assert!(
9383 issues
9384 .iter()
9385 .any(|i| i.code.contains("6.3/ScanningDirection")),
9386 "Should flag missing ScanningDirection: {:#?}",
9387 issues,
9388 );
9389 }
9390
9391 #[test]
9393 fn app2e_flags_rgba_scanning_direction_wrong() {
9394 let mut cpl =
9395 cpl_with_rgba_descriptor(ColorPrimaries::P3D65, TransferCharacteristic::PqSt2084);
9396 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9397 for ed in &mut edl.essence_descriptors {
9398 if let Some(ref mut rgba) = ed.rgba_descriptor {
9399 rgba.scanning_direction =
9400 Some("ScanningDirection_RightToLeftBottomToTop".to_string());
9401 }
9402 }
9403 }
9404 let v = App2E2021;
9405 let issues = v.validate_cpl(&cpl);
9406 assert!(
9407 issues
9408 .iter()
9409 .any(|i| i.code.contains("6.3/ScanningDirection")),
9410 "Should flag wrong ScanningDirection: {:#?}",
9411 issues,
9412 );
9413 }
9414
9415 #[test]
9417 fn app2e_flags_rgba_table11_ref_mismatch() {
9418 let mut cpl =
9419 cpl_with_rgba_descriptor(ColorPrimaries::Bt709, TransferCharacteristic::Bt709);
9420 if let Some(ref mut edl) = cpl.essence_descriptor_list {
9421 for ed in &mut edl.essence_descriptors {
9422 if let Some(ref mut rgba) = ed.rgba_descriptor {
9423 rgba.component_min_ref = Some(0);
9426 rgba.component_max_ref = Some(940);
9427 }
9428 }
9429 }
9430 let v = App2E2021;
9431 let issues = v.validate_cpl(&cpl);
9432 assert!(
9433 issues.iter().any(|i| i.code.contains("6.3.2/")),
9434 "Should flag Table11 ref mismatch: {:#?}",
9435 issues,
9436 );
9437 }
9438
9439 #[test]
9445 fn core_flags_duplicate_resource_id() {
9446 let mut cpl = minimal_cpl();
9447 let dup_id = uuid(42);
9448 let mut sl = empty_sequence_list();
9449 sl.main_image_sequences.push(MainImageSequence {
9450 id: uuid(3),
9451 track_id: uuid(4),
9452 resource_list: ResourceList {
9453 resources: vec![Resource {
9454 id: dup_id,
9455 annotation: None,
9456 edit_rate: None,
9457 intrinsic_duration: 100,
9458 entry_point: None,
9459 source_duration: None,
9460 source_encoding: Some(uuid(10)),
9461 track_file_id: Some(uuid(50)),
9462 repeat_count: None,
9463 key_id: None,
9464 hash: None,
9465 markers: vec![],
9466 }],
9467 },
9468 });
9469 sl.main_audio_sequences.push(MainAudioSequence {
9470 id: uuid(5),
9471 track_id: uuid(6),
9472 resource_list: ResourceList {
9473 resources: vec![Resource {
9474 id: dup_id, annotation: None,
9476 edit_rate: None,
9477 intrinsic_duration: 100,
9478 entry_point: None,
9479 source_duration: None,
9480 source_encoding: Some(uuid(11)),
9481 track_file_id: Some(uuid(51)),
9482 repeat_count: None,
9483 key_id: None,
9484 hash: None,
9485 markers: vec![],
9486 }],
9487 },
9488 });
9489 cpl.segment_list.segments[0].sequence_list = sl;
9490 let v = CoreConstraints2020;
9491 let issues = v.validate_cpl(&cpl);
9492 assert!(
9493 issues.iter().any(|i| i.code.contains("UniqueResourceId")),
9494 "Should flag duplicate resource ID: {:#?}",
9495 issues,
9496 );
9497 }
9498
9499 #[test]
9501 fn emitted_codes_do_not_use_general_fallback() {
9502 let mut cpl = minimal_cpl();
9503 cpl.edit_rate = None;
9504 cpl.issue_date = "invalid-date".to_string();
9505
9506 let core_issues = CoreConstraints2020.validate_cpl(&cpl);
9507 assert!(
9508 !core_issues.iter().any(|i| i.code.contains(":General/")),
9509 "Core validator emitted :General fallback codes: {:#?}",
9510 core_issues,
9511 );
9512
9513 let app2e_issues = App2E2021.validate_cpl(&cpl);
9514 assert!(
9515 !app2e_issues.iter().any(|i| i.code.contains(":General/")),
9516 "App2E validator emitted :General fallback codes: {:#?}",
9517 app2e_issues,
9518 );
9519 }
9520
9521 #[test]
9527 fn helper_any_uri_valid() {
9528 assert!(is_valid_any_uri("http://www.movielabs.com/md/ratings"));
9529 assert!(is_valid_any_uri("urn:smpte:2067-3:ag"));
9530 assert!(is_valid_any_uri("https://example.com/agency"));
9531 assert!(is_valid_any_uri("relative/path"));
9532 }
9533
9534 #[test]
9535 fn helper_any_uri_invalid() {
9536 assert!(!is_valid_any_uri("http://example.com with space"));
9537 assert!(!is_valid_any_uri("has\ttab"));
9538 assert!(!is_valid_any_uri("has\nnewline"));
9539 }
9540
9541 #[test]
9547 fn core_flags_invalid_total_running_time() {
9548 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
9549 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
9550 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
9551 <ContentTitle>Test</ContentTitle>
9552 <EditRate>24 1</EditRate>
9553 <TotalRunningTime>2:30:00</TotalRunningTime>
9554 <SegmentList><Segment>
9555 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
9556 <SequenceList/>
9557 </Segment></SegmentList>
9558 </CompositionPlaylist>"#;
9559 let cpl =
9560 crate::cpl::parse_cpl(xml).expect("should parse despite invalid TotalRunningTime");
9561 let issues = CoreConstraints2013.validate_cpl(&cpl);
9562 assert!(
9563 issues.iter().any(|i| i.code.contains("TotalRunningTime")),
9564 "Invalid TotalRunningTime format should be flagged: {:#?}",
9565 issues,
9566 );
9567 }
9568
9569 #[test]
9570 fn core_accepts_valid_total_running_time() {
9571 let mut cpl = minimal_cpl();
9572 cpl.total_running_time = Some("02:30:00".to_string());
9573 let issues = CoreConstraints2020.validate_cpl(&cpl);
9574 assert!(
9575 !issues.iter().any(|i| i.code.contains("TotalRunningTime")),
9576 "Valid TotalRunningTime should be accepted: {:#?}",
9577 issues,
9578 );
9579 }
9580
9581 #[test]
9586 fn core_flags_timecode_rate_zero() {
9587 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
9588 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
9589 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
9590 <ContentTitle>Test</ContentTitle>
9591 <CompositionTimecode>
9592 <TimecodeDropFrame>false</TimecodeDropFrame>
9593 <TimecodeRate>0</TimecodeRate>
9594 <TimecodeStartAddress>00:00:00:00</TimecodeStartAddress>
9595 </CompositionTimecode>
9596 <EditRate>24 1</EditRate>
9597 <SegmentList><Segment>
9598 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
9599 <SequenceList/>
9600 </Segment></SegmentList>
9601 </CompositionPlaylist>"#;
9602 let cpl = crate::cpl::parse_cpl(xml).expect("should parse despite zero TimecodeRate");
9603 let issues = CoreConstraints2013.validate_cpl(&cpl);
9604 assert!(
9605 issues.iter().any(|i| i.code.contains("TimecodeRate")),
9606 "TimecodeRate of 0 should be flagged: {:#?}",
9607 issues,
9608 );
9609 }
9610
9611 #[test]
9612 fn core_accepts_positive_timecode_rate() {
9613 let mut cpl = minimal_cpl();
9614 cpl.composition_timecode = Some(CompositionTimecode {
9615 timecode_drop_frame: Some(false),
9616 timecode_rate: Some(24),
9617 timecode_start_address: Some("00:00:00:00".to_string()),
9618 });
9619 let issues = CoreConstraints2020.validate_cpl(&cpl);
9620 assert!(
9621 !issues
9622 .iter()
9623 .any(|i| i.code.contains("CompositionTimecode/Rate-Zero")),
9624 "Positive TimecodeRate should be accepted: {:#?}",
9625 issues,
9626 );
9627 }
9628
9629 #[test]
9635 fn core_flags_invalid_timecode_start_address() {
9636 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
9637 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
9638 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
9639 <ContentTitle>Test</ContentTitle>
9640 <CompositionTimecode>
9641 <TimecodeDropFrame>false</TimecodeDropFrame>
9642 <TimecodeRate>24</TimecodeRate>
9643 <TimecodeStartAddress>10:00:00</TimecodeStartAddress>
9644 </CompositionTimecode>
9645 <EditRate>24 1</EditRate>
9646 <SegmentList><Segment>
9647 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
9648 <SequenceList/>
9649 </Segment></SegmentList>
9650 </CompositionPlaylist>"#;
9651 let cpl =
9652 crate::cpl::parse_cpl(xml).expect("should parse despite invalid TimecodeStartAddress");
9653 let issues = CoreConstraints2013.validate_cpl(&cpl);
9654 assert!(
9655 issues
9656 .iter()
9657 .any(|i| i.code.contains("TimecodeStartAddress")),
9658 "Invalid TimecodeStartAddress format should be flagged: {:#?}",
9659 issues,
9660 );
9661 }
9662
9663 #[test]
9664 fn core_accepts_valid_timecode_start_address() {
9665 let mut cpl = minimal_cpl();
9666 cpl.composition_timecode = Some(CompositionTimecode {
9667 timecode_drop_frame: Some(false),
9668 timecode_rate: Some(24),
9669 timecode_start_address: Some("10:00:00:00".to_string()),
9670 });
9671 let issues = CoreConstraints2020.validate_cpl(&cpl);
9672 assert!(
9673 !issues
9674 .iter()
9675 .any(|i| i.code.contains("CompositionTimecode/StartAddress-Format")),
9676 "Valid TimecodeStartAddress should be accepted: {:#?}",
9677 issues,
9678 );
9679 }
9680
9681 #[test]
9684 fn core_flags_empty_content_version_list() {
9685 let mut cpl = minimal_cpl();
9686 cpl.content_version_list = Some(ContentVersionList {
9687 content_versions: vec![],
9688 });
9689 let issues = CoreConstraints2020.validate_cpl(&cpl);
9690 assert!(
9691 issues
9692 .iter()
9693 .any(|i| i.code.contains("ContentVersionListEmpty")),
9694 "Empty ContentVersionList should be flagged: {:#?}",
9695 issues,
9696 );
9697 }
9698
9699 #[test]
9700 fn core_accepts_non_empty_content_version_list() {
9701 let mut cpl = minimal_cpl();
9702 cpl.content_version_list = Some(ContentVersionList {
9703 content_versions: vec![ContentVersion {
9704 id: "urn:uuid:00000000-0000-0000-0000-000000000001".to_string(),
9705 label_text: Some(LanguageString {
9706 text: "v1".to_string(),
9707 language: None,
9708 }),
9709 }],
9710 });
9711 let issues = CoreConstraints2020.validate_cpl(&cpl);
9712 assert!(
9713 !issues
9714 .iter()
9715 .any(|i| i.code.contains("ContentVersionListEmpty")),
9716 "Non-empty ContentVersionList should be accepted: {:#?}",
9717 issues,
9718 );
9719 }
9720
9721 #[test]
9727 fn core_flags_empty_locale_list() {
9728 let xml = r#"<CompositionPlaylist xmlns="http://www.smpte-ra.org/schemas/2067-3/2013">
9729 <Id>urn:uuid:00000000-0000-0000-0000-000000000001</Id>
9730 <IssueDate>2024-01-01T00:00:00Z</IssueDate>
9731 <ContentTitle>Test</ContentTitle>
9732 <EditRate>24 1</EditRate>
9733 <LocaleList/>
9734 <SegmentList><Segment>
9735 <Id>urn:uuid:00000000-0000-0000-0000-000000000002</Id>
9736 <SequenceList/>
9737 </Segment></SegmentList>
9738 </CompositionPlaylist>"#;
9739 let cpl = crate::cpl::parse_cpl(xml).expect("should parse despite empty LocaleList");
9740 let issues = CoreConstraints2013.validate_cpl(&cpl);
9741 assert!(
9742 issues.iter().any(|i| i.code.contains("Locale")),
9743 "Empty LocaleList should be flagged: {:#?}",
9744 issues,
9745 );
9746 }
9747
9748 #[test]
9749 fn core_accepts_non_empty_locale_list() {
9750 let mut cpl = minimal_cpl();
9751 cpl.locale_list = Some(LocaleList {
9752 locales: vec![Locale {
9753 language_list: None,
9754 region_list: None,
9755 content_maturity_rating_list: None,
9756 }],
9757 });
9758 let issues = CoreConstraints2020.validate_cpl(&cpl);
9759 assert!(
9760 !issues
9761 .iter()
9762 .any(|i| i.code.contains("LocaleList-NonEmpty")),
9763 "Non-empty LocaleList should be accepted: {:#?}",
9764 issues,
9765 );
9766 }
9767
9768 #[test]
9771 fn core_flags_empty_essence_descriptor_list() {
9772 let mut cpl = minimal_cpl();
9773 cpl.essence_descriptor_list = Some(EssenceDescriptorList {
9774 essence_descriptors: vec![],
9775 });
9776 let issues = CoreConstraints2020.validate_cpl(&cpl);
9777 assert!(
9778 issues
9779 .iter()
9780 .any(|i| i.code.contains("EssenceDescriptorListEmpty")),
9781 "Empty EssenceDescriptorList should be flagged: {:#?}",
9782 issues,
9783 );
9784 }
9785
9786 #[test]
9789 fn app2e_flags_agency_with_whitespace() {
9790 let mut cpl = minimal_cpl();
9791 cpl.locale_list = Some(LocaleList {
9792 locales: vec![Locale {
9793 language_list: None,
9794 region_list: None,
9795 content_maturity_rating_list: Some(ContentMaturityRatingList {
9796 ratings: vec![ContentMaturityRating {
9797 agency: "http://www.example.com bad uri".to_string(),
9798 rating: Some("PG".to_string()),
9799 audience: None,
9800 }],
9801 }),
9802 }],
9803 });
9804 let issues = App2E2021.validate_cpl(&cpl);
9805 assert!(
9806 issues
9807 .iter()
9808 .any(|i| i.code.contains("ContentMaturityRating-Agency-URI")),
9809 "Agency with whitespace should be flagged: {:#?}",
9810 issues,
9811 );
9812 }
9813
9814 #[test]
9815 fn app2e_accepts_valid_agency_uri() {
9816 let mut cpl = minimal_cpl();
9817 cpl.locale_list = Some(LocaleList {
9818 locales: vec![Locale {
9819 language_list: None,
9820 region_list: None,
9821 content_maturity_rating_list: Some(ContentMaturityRatingList {
9822 ratings: vec![ContentMaturityRating {
9823 agency: "http://www.movielabs.com/md/ratings".to_string(),
9824 rating: Some("PG-13".to_string()),
9825 audience: None,
9826 }],
9827 }),
9828 }],
9829 });
9830 let issues = App2E2021.validate_cpl(&cpl);
9831 assert!(
9832 !issues
9833 .iter()
9834 .any(|i| i.code.contains("ContentMaturityRating-Agency-URI")),
9835 "Valid Agency URI should be accepted: {:#?}",
9836 issues,
9837 );
9838 }
9839}