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