//! Codec-agnostic per-picture coding carrier. //! //! [`PictureInfo`] is the single per-frame carrier of coding signals that the //! muxer (and any downstream index/diagnostic) reads WITHOUT branching on the //! codec. Each codec's parser decodes its own bitstream once and folds the raw //! signals into a [`CodingDetail`] variant; consumers then call ONLY the //! codec-agnostic accessors ([`coding_type`](PictureInfo::coding_type), //! [`field_order`](PictureInfo::field_order), [`nb_fields`](PictureInfo::nb_fields), //! [`progressive`](PictureInfo::progressive)). The accessor surface is fixed: //! adding a codec means adding a `CodingDetail` arm, never changing a consumer. //! //! Spec references: ITU-T H.273 (CICP code points, shared elsewhere), //! ISO/IEC 13818-2 §6.3.10 (MPEG-2 picture coding extension: `top_field_first`, //! `repeat_first_field`, `progressive_frame`), RFC 9559 §5.1.4.1.28 //! (Matroska `FieldOrder` element 0x9D). /// Coding/prediction type of a coded picture, mapped to the three families the /// muxer cares about (cue/keyframe marking, B-frame ordering). Each codec maps /// its own picture/slice type onto this: /// - MPEG-2 `picture_coding_type` (ISO/IEC 13818-2 §6.3.8): 1→I, 2→P, 3→B. /// - H.264/HEVC: slice type / IDR detection → I for intra-coded keyframes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CodingType { /// Intra-coded (I / IDR) — independently decodable, a cue/keyframe point. I, /// Predicted (P) — references earlier pictures. P, /// Bi-predicted (B) — references earlier and later pictures. B, } /// Field display order of an interlaced coded picture, mapped onto the Matroska /// `FieldOrder` element (RFC 9559 §5.1.4.1.28, element 0x9D). `Progressive` /// means the picture is not interlaced (the element is omitted by the muxer); /// `None` from [`PictureInfo::field_order`] means the codec could not determine /// it (signal absent / not yet decoded). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FieldOrder { /// Top field is displayed first (MPEG-2 `top_field_first == 1`). Tff, /// Bottom field is displayed first (MPEG-2 `top_field_first == 0`). Bff, /// Progressive frame — no field order applies. Progressive, } /// MPEG-2 picture coding extension signals, decoded once at the parse site. /// /// All four bits are read from ISO/IEC 13818-2 §6.3.10 (picture coding /// extension) and §6.3.5 (sequence extension `progressive_sequence`); this /// struct is the raw record the agnostic accessors derive from. Consumers do /// NOT read these fields directly — they go through [`PictureInfo`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Mpeg2Coding { /// `top_field_first` (picture coding extension). pub top_field_first: bool, /// `repeat_first_field` (picture coding extension) — the 2:3 pulldown bit. pub repeat_first_field: bool, /// `progressive_frame` (picture coding extension). pub progressive_frame: bool, /// `progressive_sequence` (sequence extension) in force for this picture. pub progressive_sequence: bool, /// True when this access unit codes a whole frame (`picture_structure == 11`); /// false for a single field picture (occupies one field period). pub frame_picture: bool, } /// Per-codec raw coding detail. One arm per codec carrying that codec's own /// signals; the agnostic accessors on [`PictureInfo`] match on this. Codecs /// that have not yet had their field/pulldown signals wired carry `None` for /// `field_order` via the accessor (the arm exists, the bits do not). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CodingDetail { /// MPEG-2 Video (ISO/IEC 13818-2) picture coding extension signals. Mpeg2(Mpeg2Coding), /// A codec that reports coding type but no field/pulldown detail yet /// (H.264 / HEVC / VC-1). Field order is reported as unknown. CodingTypeOnly, } /// HDR10 static metadata measured from a video bitstream (HEVC SEI). Carried on /// [`PictureInfo`] as the per-stream colour-volume signalling: it only ever /// reaches the muxer when BOTH SEI messages were actually present in the stream, /// so an SDR / no-SEI track leaves it `None` and the muxer omits the elements /// (never fabricated). /// /// All values are stored in their RAW SEI integer units (NOT yet scaled to the /// Matroska float domain); the muxer applies the H.265 → Matroska unit /// conversion at emit time so the scaling lives in exactly one place. /// /// Spec: Rec. ITU-T H.265 D.2.28 (Mastering Display Colour Volume, /// payloadType 137) and D.2.35 (Content Light Level Info, payloadType 144). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Hdr10Metadata { /// `display_primaries_x[c]` / `display_primaries_y[c]` for c = 0,1,2. /// Per H.265 D.3.28 the SEI order is c=0 → Green, c=1 → Blue, c=2 → Red. /// Stored here in that SAME SEI order; the muxer maps to Matroska's R/G/B /// element layout. Units of 0.00002 (chromaticity). pub display_primaries_x: [u16; 3], pub display_primaries_y: [u16; 3], /// `white_point_x` / `white_point_y` in units of 0.00002 (chromaticity). pub white_point_x: u16, pub white_point_y: u16, /// `max_display_mastering_luminance` in units of 0.0001 cd/m². pub max_display_mastering_luminance: u32, /// `min_display_mastering_luminance` in units of 0.0001 cd/m². pub min_display_mastering_luminance: u32, /// `max_content_light_level` (MaxCLL) in cd/m² — already an integer. pub max_content_light_level: u16, /// `max_pic_average_light_level` (MaxFALL) in cd/m² — already an integer. pub max_pic_average_light_level: u16, } /// Codec-agnostic per-picture coding carrier — the single per-frame record the /// muxer reads through the accessors below. Raw codec signals live in /// [`CodingDetail`]; consumers MUST use the accessors, never the inner fields. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PictureInfo { /// Agnostic coding type (I/P/B). Set by every video parser that fills /// `coding`, derived from the codec's own picture/slice type. coding_type: CodingType, /// Raw per-codec coding detail. Holds the bits the field/pulldown /// accessors derive from. detail: CodingDetail, /// HDR10 static metadata measured from the bitstream (HEVC SEI), or `None` /// when the stream carried no HDR10 SEI (SDR / not signalled). Per-stream, /// but rides the per-picture carrier so it flows the same deferred-muxer /// path the measured field order does. Never fabricated. hdr10: Option, } impl PictureInfo { /// Build a `PictureInfo` for MPEG-2 from its decoded coding type and the /// picture-coding-extension signals. pub fn mpeg2(coding_type: CodingType, m: Mpeg2Coding) -> Self { Self { coding_type, detail: CodingDetail::Mpeg2(m), hdr10: None, } } /// Build a `PictureInfo` for a codec that only reports its coding type /// (no field/pulldown detail decoded yet): H.264, HEVC, VC-1. pub fn coding_type_only(coding_type: CodingType) -> Self { Self { coding_type, detail: CodingDetail::CodingTypeOnly, hdr10: None, } } /// Attach measured HDR10 static metadata (HEVC SEI) to this picture, /// consuming and returning `self` for builder-style use. Only ever called /// with `Some(..)` once both HDR10 SEI messages have been seen, so an SDR /// track never carries fabricated colour-volume data. pub fn with_hdr10(mut self, hdr10: Option) -> Self { self.hdr10 = hdr10; self } /// Measured HDR10 static metadata for this picture's stream, or `None` when /// the bitstream signalled no HDR10 SEI. Read at mux time to emit the /// Matroska MasteringMetadata / MaxCLL / MaxFALL — omitted entirely when /// `None`. pub fn hdr10(&self) -> Option { self.hdr10 } /// Agnostic coding type (I/P/B). The single signal for cue/keyframe marking /// and B-frame display ordering. pub fn coding_type(&self) -> CodingType { self.coding_type } /// Field display order for this picture, or `None` when the codec could not /// determine it (signal absent / not yet wired). MPEG-2: derived from /// `top_field_first` and the progressive flags (ISO/IEC 13818-2 §6.3.10) — /// a progressive frame/sequence reports [`FieldOrder::Progressive`]. pub fn field_order(&self) -> Option { match self.detail { CodingDetail::Mpeg2(m) => { if !m.frame_picture { // A single field picture is inherently interlaced. Which // field it actually codes is given by picture_structure // (top/bottom), not by top_field_first — §6.3.10 constrains // top_field_first to 0 for field pictures, so it is not the // spec source here. picture_structure is not retained on // this carrier, so top_field_first is used only as the lone // field hint available (best-effort, not spec-derived). Some(if m.top_field_first { FieldOrder::Tff } else { FieldOrder::Bff }) } else if m.progressive_sequence || m.progressive_frame { Some(FieldOrder::Progressive) } else if m.top_field_first { Some(FieldOrder::Tff) } else { Some(FieldOrder::Bff) } } CodingDetail::CodingTypeOnly => None, } } /// Number of field-display periods this picture occupies — the basis for /// soft-telecine (2:3 pulldown) timing. MPEG-2 (ISO/IEC 13818-2 §6.3.10): /// a field picture occupies 1 field, /// a normal frame 2, a `repeat_first_field` progressive-frame 3 (or 4/6 in a /// progressive sequence); an rff bit on a non-progressive interlaced frame is /// spec-forbidden (§6.3.10) and is treated as 2. Codecs without pulldown /// signalling report the normal 2 fields. pub fn nb_fields(&self) -> u8 { match self.detail { CodingDetail::Mpeg2(m) => { if !m.frame_picture { return 1; } if !m.repeat_first_field { return 2; } if m.progressive_sequence { if m.top_field_first { 6 } else { 4 } } else if m.progressive_frame { 3 } else { 2 } } CodingDetail::CodingTypeOnly => 2, } } /// Whether this picture is progressive, or `None` when the codec did not /// signal it. MPEG-2: `progressive_sequence || progressive_frame`. pub fn progressive(&self) -> Option { match self.detail { CodingDetail::Mpeg2(m) => Some(m.progressive_sequence || m.progressive_frame), CodingDetail::CodingTypeOnly => None, } } /// I-picture ⇒ cue/keyframe point. Convenience over `coding_type()`. pub fn keyframe(&self) -> bool { self.coding_type == CodingType::I } } #[cfg(test)] mod tests { use super::*; fn mpeg2( ct: CodingType, tff: bool, rff: bool, prog_frame: bool, prog_seq: bool, frame_pic: bool, ) -> PictureInfo { PictureInfo::mpeg2( ct, Mpeg2Coding { top_field_first: tff, repeat_first_field: rff, progressive_frame: prog_frame, progressive_sequence: prog_seq, frame_picture: frame_pic, }, ) } #[test] fn coding_type_accessor_returns_stored_type() { assert_eq!( mpeg2(CodingType::I, true, false, false, false, true).coding_type(), CodingType::I ); assert_eq!( mpeg2(CodingType::B, true, false, false, false, true).coding_type(), CodingType::B ); } #[test] fn keyframe_only_for_intra() { assert!(mpeg2(CodingType::I, true, false, false, false, true).keyframe()); assert!(!mpeg2(CodingType::P, true, false, false, false, true).keyframe()); assert!(!mpeg2(CodingType::B, true, false, false, false, true).keyframe()); } #[test] fn mpeg2_field_order_tff_when_top_field_first() { // Interlaced frame picture, tff set → top-field-first. assert_eq!( mpeg2(CodingType::I, true, false, false, false, true).field_order(), Some(FieldOrder::Tff) ); } #[test] fn mpeg2_field_order_bff_when_not_top_field_first() { // Interlaced frame picture, tff clear → bottom-field-first. assert_eq!( mpeg2(CodingType::I, false, false, false, false, true).field_order(), Some(FieldOrder::Bff) ); } #[test] fn mpeg2_field_order_progressive_for_progressive_frame() { assert_eq!( mpeg2(CodingType::I, true, false, true, false, true).field_order(), Some(FieldOrder::Progressive) ); // Progressive sequence likewise. assert_eq!( mpeg2(CodingType::I, true, false, false, true, true).field_order(), Some(FieldOrder::Progressive) ); } #[test] fn mpeg2_nb_fields_normal_and_telecine() { // Normal interlaced frame: 2 fields. assert_eq!( mpeg2(CodingType::P, true, false, false, false, true).nb_fields(), 2 ); // NTSC 2:3 soft telecine (interlaced seq, progressive frame, rff): 3. assert_eq!( mpeg2(CodingType::P, false, true, true, false, true).nb_fields(), 3 ); // Field picture: 1 field. assert_eq!( mpeg2(CodingType::P, false, false, false, false, false).nb_fields(), 1 ); // Progressive sequence, rff + tff: 6. assert_eq!( mpeg2(CodingType::P, true, true, false, true, true).nb_fields(), 6 ); // Progressive sequence, rff no tff: 4. assert_eq!( mpeg2(CodingType::P, false, true, false, true, true).nb_fields(), 4 ); } #[test] fn mpeg2_progressive_accessor() { assert_eq!( mpeg2(CodingType::I, true, false, true, false, true).progressive(), Some(true) ); assert_eq!( mpeg2(CodingType::I, true, false, false, false, true).progressive(), Some(false) ); } #[test] fn coding_type_only_reports_unknown_field_and_progressive() { let p = PictureInfo::coding_type_only(CodingType::P); assert_eq!(p.coding_type(), CodingType::P); assert_eq!(p.field_order(), None); assert_eq!(p.progressive(), None); // No pulldown signalling for these codecs → normal 2-field frame. assert_eq!(p.nb_fields(), 2); } }