From 2cbf91ddedde5b9dd2251e6201acab794f3d159c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 3 Oct 2026 02:43:29 -0400 Subject: [PATCH] M9.15: MPEG-1/2 video decoder crate (frame/field pictures, 4:2:2, IEEE 1180 IDCT) with ffmpeg oracle tests Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 17 + Cargo.toml | 2 + crates/mpeg2v/Cargo.toml | 21 + crates/mpeg2v/src/bits.rs | 93 ++++ crates/mpeg2v/src/headers.rs | 466 +++++++++++++++++ crates/mpeg2v/src/idct.rs | 179 +++++++ crates/mpeg2v/src/lib.rs | 800 ++++++++++++++++++++++++++++++ crates/mpeg2v/src/mc.rs | 154 ++++++ crates/mpeg2v/src/slice.rs | 615 +++++++++++++++++++++++ crates/mpeg2v/src/tests.rs | 171 +++++++ crates/mpeg2v/src/vlc.rs | 627 +++++++++++++++++++++++ crates/mpeg2v/tests/common/mod.rs | 129 +++++ crates/mpeg2v/tests/oracle.rs | 234 +++++++++ crates/mpegts/Cargo.toml | 16 + crates/mpegts/src/lib.rs | 1 + xtask/src/main.rs | 3 + 16 files changed, 3528 insertions(+) create mode 100644 crates/mpeg2v/Cargo.toml create mode 100644 crates/mpeg2v/src/bits.rs create mode 100644 crates/mpeg2v/src/headers.rs create mode 100644 crates/mpeg2v/src/idct.rs create mode 100644 crates/mpeg2v/src/lib.rs create mode 100644 crates/mpeg2v/src/mc.rs create mode 100644 crates/mpeg2v/src/slice.rs create mode 100644 crates/mpeg2v/src/tests.rs create mode 100644 crates/mpeg2v/src/vlc.rs create mode 100644 crates/mpeg2v/tests/common/mod.rs create mode 100644 crates/mpeg2v/tests/oracle.rs create mode 100644 crates/mpegts/Cargo.toml create mode 100644 crates/mpegts/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d874f8b59..872b548c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2012,6 +2012,23 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "filmcraft-mpeg2v" +version = "0.1.0" +dependencies = [ + "filmcraft-testkit", + "rayon", + "thiserror 2.0.21", +] + +[[package]] +name = "filmcraft-mpegts" +version = "0.1.0" +dependencies = [ + "filmcraft-testkit", + "thiserror 2.0.21", +] + [[package]] name = "filmcraft-mxf" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 431df0773..f9b43019c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ filmcraft-bitstream = { path = "crates/bitstream" } filmcraft-isobmff = { path = "crates/isobmff" } filmcraft-matroska = { path = "crates/matroska" } filmcraft-mxf = { path = "crates/mxf" } +filmcraft-mpegts = { path = "crates/mpegts" } +filmcraft-mpeg2v = { path = "crates/mpeg2v" } filmcraft-ogg = { path = "crates/ogg" } filmcraft-h264 = { path = "crates/h264" } filmcraft-hevc = { path = "crates/hevc" } diff --git a/crates/mpeg2v/Cargo.toml b/crates/mpeg2v/Cargo.toml new file mode 100644 index 000000000..ca418feaa --- /dev/null +++ b/crates/mpeg2v/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "filmcraft-mpeg2v" +description = "Clean-room MPEG-2 video (ITU-T H.262 | ISO/IEC 13818-2) and MPEG-1 video (ISO/IEC 11172-2) decoder" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[features] +default = ["threads"] +threads = ["dep:rayon"] + +[dependencies] +thiserror = { workspace = true } +rayon = { workspace = true, optional = true } + +[dev-dependencies] +filmcraft-testkit = { workspace = true } diff --git a/crates/mpeg2v/src/bits.rs b/crates/mpeg2v/src/bits.rs new file mode 100644 index 000000000..eb8f50406 --- /dev/null +++ b/crates/mpeg2v/src/bits.rs @@ -0,0 +1,93 @@ +//! Big-endian bit reader over one start-code unit. Reads past the end return zero bits; callers +//! check [`Bits::overrun`] to detect truncated data. + +#[derive(Clone)] +pub(crate) struct Bits<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Bits<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + /// The next 32 bits, MSB first (zeros past the end). + #[inline(always)] + pub fn peek32(&self) -> u32 { + let byte = self.pos >> 3; + let w = if byte + 8 <= self.data.len() { + u64::from_be_bytes(self.data[byte..byte + 8].try_into().expect("8 bytes")) + } else { + let mut b = [0u8; 8]; + if byte < self.data.len() { + let n = self.data.len() - byte; + b[..n].copy_from_slice(&self.data[byte..]); + } + u64::from_be_bytes(b) + }; + ((w << (self.pos & 7)) >> 32) as u32 + } + + /// The next `n` (1..=32) bits without consuming them. + #[inline(always)] + pub fn peek(&self, n: u32) -> u32 { + debug_assert!((1..=32).contains(&n)); + ((self.peek32() as u64) >> (32 - n)) as u32 + } + + #[inline(always)] + pub fn skip(&mut self, n: u32) { + self.pos += n as usize; + } + + #[inline(always)] + pub fn read(&mut self, n: u32) -> u32 { + if n == 0 { + return 0; + } + let v = self.peek(n); + self.pos += n as usize; + v + } + + #[inline(always)] + pub fn bit(&mut self) -> bool { + self.read(1) == 1 + } + + /// A `n`-bit two's complement value. + #[inline(always)] + pub fn read_signed(&mut self, n: u32) -> i32 { + let v = self.read(n) as i32; + (v << (32 - n)) >> (32 - n) + } + + pub fn bits_left(&self) -> isize { + (self.data.len() * 8) as isize - self.pos as isize + } + + /// More than a few bytes have been read past the end: the data is truncated or corrupt. + pub fn overrun(&self) -> bool { + self.bits_left() < 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reads_across_bytes_and_past_the_end() { + let d = [0b1010_1100, 0xFF, 0x01]; + let mut b = Bits::new(&d); + assert_eq!(b.read(3), 0b101); + assert_eq!(b.read(7), 0b011_0011); + assert_eq!(b.peek(4), 0xF); + assert_eq!(b.read_signed(4), -1); + assert_eq!(b.read(10), 0b11_0000_0001); + assert!(!b.overrun()); + assert_eq!(b.read(8), 0); + assert!(b.overrun()); + } +} diff --git a/crates/mpeg2v/src/headers.rs b/crates/mpeg2v/src/headers.rs new file mode 100644 index 000000000..514aba50f --- /dev/null +++ b/crates/mpeg2v/src/headers.rs @@ -0,0 +1,466 @@ +//! Start codes and headers (H.262 §6.2 / §6.3; ISO/IEC 11172-2 §2.4.2). + +use crate::bits::Bits; +use crate::{Error, Result}; + +pub const PICTURE_START: u8 = 0x00; +pub const SLICE_MIN: u8 = 0x01; +pub const SLICE_MAX: u8 = 0xAF; +pub const USER_DATA: u8 = 0xB2; +pub const SEQUENCE_HEADER: u8 = 0xB3; +pub const SEQUENCE_ERROR: u8 = 0xB4; +pub const EXTENSION: u8 = 0xB5; +pub const SEQUENCE_END: u8 = 0xB7; +pub const GROUP_START: u8 = 0xB8; + +/// Start codes in `data`: (offset of the `00 00 01` prefix, start code value). A unit's payload +/// runs from `offset + 4` to the next start code. +pub fn start_codes(data: &[u8]) -> Vec<(usize, u8)> { + let mut out = Vec::new(); + let mut i = 0; + let n = data.len(); + while i + 3 < n { + // skip quickly to a zero byte at i + 2 + if data[i + 2] > 1 { + i += 3; + continue; + } + if data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1 { + out.push((i, data[i + 3])); + i += 4; + } else { + i += 1; + } + } + out +} + +/// Default intra quantiser matrix (H.262 §6.3.11), raster order. +pub const DEFAULT_INTRA: [u8; 64] = [ + 8, 16, 19, 22, 26, 27, 29, 34, // + 16, 16, 22, 24, 27, 29, 34, 37, // + 19, 22, 26, 27, 29, 34, 34, 38, // + 22, 22, 26, 27, 29, 34, 37, 40, // + 22, 26, 27, 29, 32, 35, 40, 48, // + 26, 27, 29, 32, 35, 40, 48, 58, // + 26, 27, 29, 34, 38, 46, 56, 69, // + 27, 29, 35, 38, 46, 56, 69, 83, +]; + +/// Scan index → raster position: zigzag (scan 0) and alternate (scan 1), H.262 Figure 7-2/7-3. +pub const SCAN: [[u8; 64]; 2] = [ + [ + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, + ], + [ + 0, 8, 16, 24, 1, 9, 2, 10, 17, 25, 32, 40, 48, 56, 57, 49, 41, 33, 26, 18, 3, 11, 4, 12, 19, 27, 34, 42, 50, 58, 35, 43, 51, 59, 20, 28, 5, 13, 6, 14, + 21, 29, 36, 44, 52, 60, 37, 45, 53, 61, 22, 30, 7, 15, 23, 31, 38, 46, 54, 62, 39, 47, 55, 63, + ], +]; + +/// A quantiser matrix as transmitted (zigzag order) → raster order. +fn read_matrix(b: &mut Bits) -> [u8; 64] { + let mut m = [0u8; 64]; + for i in 0..64 { + m[SCAN[0][i] as usize] = b.read(8) as u8; + } + m +} + +/// sequence_header() (§6.2.2.1). +#[derive(Clone, Debug, PartialEq)] +pub struct SequenceHeader { + pub horizontal_size: u32, + pub vertical_size: u32, + pub aspect_ratio_information: u8, + pub frame_rate_code: u8, + pub bit_rate_value: u32, + pub vbv_buffer_size_value: u32, + pub constrained_parameters: bool, + /// Raster order; `None`: default. + pub intra_matrix: Option<[u8; 64]>, + pub non_intra_matrix: Option<[u8; 64]>, +} + +impl SequenceHeader { + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < 8 { + return Err(Error::Invalid("short sequence header".into())); + } + let mut b = Bits::new(payload); + let horizontal_size = b.read(12); + let vertical_size = b.read(12); + let aspect_ratio_information = b.read(4) as u8; + let frame_rate_code = b.read(4) as u8; + let bit_rate_value = b.read(18); + b.skip(1); + let vbv_buffer_size_value = b.read(10); + let constrained_parameters = b.bit(); + let intra_matrix = b.bit().then(|| read_matrix(&mut b)); + let non_intra_matrix = b.bit().then(|| read_matrix(&mut b)); + if b.overrun() { + return Err(Error::Invalid("truncated sequence header".into())); + } + if horizontal_size == 0 || vertical_size == 0 { + return Err(Error::Invalid("zero picture size".into())); + } + Ok(Self { + horizontal_size, + vertical_size, + aspect_ratio_information, + frame_rate_code, + bit_rate_value, + vbv_buffer_size_value, + constrained_parameters, + intra_matrix, + non_intra_matrix, + }) + } +} + +/// sequence_extension() (§6.2.2.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SequenceExtension { + pub profile_and_level_indication: u8, + pub progressive_sequence: bool, + /// 1 = 4:2:0, 2 = 4:2:2, 3 = 4:4:4. + pub chroma_format: u8, + pub horizontal_size_extension: u8, + pub vertical_size_extension: u8, + pub bit_rate_extension: u16, + pub vbv_buffer_size_extension: u8, + pub low_delay: bool, + pub frame_rate_extension_n: u8, + pub frame_rate_extension_d: u8, +} + +/// sequence_display_extension() (§6.2.2.4). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct DisplayExtension { + pub video_format: u8, + /// (colour_primaries, transfer_characteristics, matrix_coefficients), H.262 Tables 6-7..6-9. + pub colour: Option<(u8, u8, u8)>, + pub display_horizontal_size: u16, + pub display_vertical_size: u16, +} + +/// quant_matrix_extension() (§6.2.3.2), raster order. +#[derive(Clone, Copy, Debug, Default)] +pub struct QuantMatrixExtension { + pub intra: Option<[u8; 64]>, + pub non_intra: Option<[u8; 64]>, + pub chroma_intra: Option<[u8; 64]>, + pub chroma_non_intra: Option<[u8; 64]>, +} + +/// picture_coding_extension() (§6.2.3.1). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PictureCodingExtension { + /// f_code[s][t]: s 0 forward / 1 backward, t 0 horizontal / 1 vertical. + pub f_code: [[u8; 2]; 2], + pub intra_dc_precision: u8, + /// 1 top field, 2 bottom field, 3 frame picture. + pub picture_structure: u8, + pub top_field_first: bool, + pub frame_pred_frame_dct: bool, + pub concealment_motion_vectors: bool, + pub q_scale_type: bool, + pub intra_vlc_format: bool, + pub alternate_scan: bool, + pub repeat_first_field: bool, + pub chroma_420_type: bool, + pub progressive_frame: bool, +} + +impl PictureCodingExtension { + /// The values an MPEG-1 picture behaves as (progressive frame picture, zigzag, linear q). + pub fn mpeg1(ph: &PictureHeader) -> Self { + Self { + f_code: [[ph.forward_f_code; 2], [ph.backward_f_code; 2]], + intra_dc_precision: 0, + picture_structure: 3, + top_field_first: false, + frame_pred_frame_dct: true, + concealment_motion_vectors: false, + q_scale_type: false, + intra_vlc_format: false, + alternate_scan: false, + repeat_first_field: false, + chroma_420_type: true, + progressive_frame: true, + } + } +} + +/// An extension unit (§6.2.2.2 / §6.2.3). +#[derive(Clone, Debug)] +pub enum Extension { + Sequence(SequenceExtension), + Display(DisplayExtension), + QuantMatrix(Box), + PictureCoding(PictureCodingExtension), + /// Scalability, copyright, picture display… (not needed to decode the base layer). + Other(u8), +} + +impl Extension { + pub fn parse(payload: &[u8]) -> Result { + let mut b = Bits::new(payload); + let id = b.read(4) as u8; + let e = match id { + 1 => { + let profile_and_level_indication = b.read(8) as u8; + let progressive_sequence = b.bit(); + let chroma_format = b.read(2) as u8; + let horizontal_size_extension = b.read(2) as u8; + let vertical_size_extension = b.read(2) as u8; + let bit_rate_extension = b.read(12) as u16; + b.skip(1); + let vbv_buffer_size_extension = b.read(8) as u8; + let low_delay = b.bit(); + let frame_rate_extension_n = b.read(2) as u8; + let frame_rate_extension_d = b.read(5) as u8; + Extension::Sequence(SequenceExtension { + profile_and_level_indication, + progressive_sequence, + chroma_format, + horizontal_size_extension, + vertical_size_extension, + bit_rate_extension, + vbv_buffer_size_extension, + low_delay, + frame_rate_extension_n, + frame_rate_extension_d, + }) + } + 2 => { + let video_format = b.read(3) as u8; + let colour = b.bit().then(|| (b.read(8) as u8, b.read(8) as u8, b.read(8) as u8)); + let display_horizontal_size = b.read(14) as u16; + b.skip(1); + let display_vertical_size = b.read(14) as u16; + Extension::Display(DisplayExtension { video_format, colour, display_horizontal_size, display_vertical_size }) + } + 3 => { + let intra = b.bit().then(|| read_matrix(&mut b)); + let non_intra = b.bit().then(|| read_matrix(&mut b)); + let chroma_intra = b.bit().then(|| read_matrix(&mut b)); + let chroma_non_intra = b.bit().then(|| read_matrix(&mut b)); + Extension::QuantMatrix(Box::new(QuantMatrixExtension { intra, non_intra, chroma_intra, chroma_non_intra })) + } + 8 => { + let f_code = [[b.read(4) as u8, b.read(4) as u8], [b.read(4) as u8, b.read(4) as u8]]; + let intra_dc_precision = b.read(2) as u8; + let picture_structure = b.read(2) as u8; + let top_field_first = b.bit(); + let frame_pred_frame_dct = b.bit(); + let concealment_motion_vectors = b.bit(); + let q_scale_type = b.bit(); + let intra_vlc_format = b.bit(); + let alternate_scan = b.bit(); + let repeat_first_field = b.bit(); + let chroma_420_type = b.bit(); + let progressive_frame = b.bit(); + if picture_structure == 0 { + return Err(Error::Invalid("reserved picture_structure".into())); + } + Extension::PictureCoding(PictureCodingExtension { + f_code, + intra_dc_precision, + picture_structure, + top_field_first, + frame_pred_frame_dct, + concealment_motion_vectors, + q_scale_type, + intra_vlc_format, + alternate_scan, + repeat_first_field, + chroma_420_type, + progressive_frame, + }) + } + other => Extension::Other(other), + }; + if b.overrun() { + return Err(Error::Invalid(format!("truncated extension {id}"))); + } + Ok(e) + } +} + +/// group_of_pictures_header() (§6.2.2.6). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct GopHeader { + pub drop_frame: bool, + pub hours: u8, + pub minutes: u8, + pub seconds: u8, + pub pictures: u8, + pub closed_gop: bool, + pub broken_link: bool, +} + +impl GopHeader { + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < 4 { + return Err(Error::Invalid("short GOP header".into())); + } + let mut b = Bits::new(payload); + let drop_frame = b.bit(); + let hours = b.read(5) as u8; + let minutes = b.read(6) as u8; + b.skip(1); + let seconds = b.read(6) as u8; + let pictures = b.read(6) as u8; + let closed_gop = b.bit(); + let broken_link = b.bit(); + Ok(Self { drop_frame, hours, minutes, seconds, pictures, closed_gop, broken_link }) + } +} + +/// picture_coding_type. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PictureType { + I, + P, + B, + /// ISO/IEC 11172-2 DC-coded picture. + D, +} + +impl PictureType { + pub fn is_anchor(self) -> bool { + matches!(self, PictureType::I | PictureType::P | PictureType::D) + } +} + +/// picture_header() (§6.2.3). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PictureHeader { + pub temporal_reference: u16, + pub coding_type: PictureType, + pub vbv_delay: u16, + pub full_pel_forward_vector: bool, + pub forward_f_code: u8, + pub full_pel_backward_vector: bool, + pub backward_f_code: u8, +} + +impl PictureHeader { + pub fn parse(payload: &[u8]) -> Result { + if payload.len() < 4 { + return Err(Error::Invalid("short picture header".into())); + } + let mut b = Bits::new(payload); + let temporal_reference = b.read(10) as u16; + let coding_type = match b.read(3) { + 1 => PictureType::I, + 2 => PictureType::P, + 3 => PictureType::B, + 4 => PictureType::D, + t => return Err(Error::Invalid(format!("picture_coding_type {t}"))), + }; + let vbv_delay = b.read(16) as u16; + let (mut full_pel_forward_vector, mut forward_f_code, mut full_pel_backward_vector, mut backward_f_code) = (false, 7, false, 7); + if matches!(coding_type, PictureType::P | PictureType::B) { + full_pel_forward_vector = b.bit(); + forward_f_code = b.read(3) as u8; + } + if coding_type == PictureType::B { + full_pel_backward_vector = b.bit(); + backward_f_code = b.read(3) as u8; + } + Ok(Self { temporal_reference, coding_type, vbv_delay, full_pel_forward_vector, forward_f_code, full_pel_backward_vector, backward_f_code }) + } +} + +/// Frame rate from frame_rate_code (Table 6-4) and the sequence extension's n/d (MPEG-2). +pub fn frame_rate(code: u8, ext_n: u8, ext_d: u8) -> Option<(u32, u32)> { + let (n, d) = match code { + 1 => (24000, 1001), + 2 => (24, 1), + 3 => (25, 1), + 4 => (30000, 1001), + 5 => (30, 1), + 6 => (50, 1), + 7 => (60000, 1001), + 8 => (60, 1), + _ => return None, + }; + let (n, d) = (n * (ext_n as u32 + 1), d * (ext_d as u32 + 1)); + let g = gcd(n, d); + Some((n / g, d / g)) +} + +pub(crate) fn gcd(a: u32, b: u32) -> u32 { + if b == 0 { a.max(1) } else { gcd(b, a % b) } +} + +/// Profile and level names (Tables 8-2, 8-3, 8-7). +pub fn profile_level_name(pli: u8) -> String { + if pli & 0x80 != 0 { + return match pli { + 0x82 => "4:2:2@High".into(), + 0x85 => "4:2:2@Main".into(), + 0x8A => "Multi-view@High".into(), + 0x8B => "Multi-view@High-1440".into(), + 0x8D => "Multi-view@Main".into(), + 0x8E => "Multi-view@Low".into(), + other => format!("profile/level 0x{other:02x}"), + }; + } + let profile = match (pli >> 4) & 7 { + 1 => "High", + 2 => "Spatially Scalable", + 3 => "SNR Scalable", + 4 => "Main", + 5 => "Simple", + _ => "Reserved", + }; + let level = match pli & 15 { + 4 => "High", + 6 => "High-1440", + 8 => "Main", + 10 => "Low", + _ => "Reserved", + }; + format!("{profile}@{level}") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scans_are_permutations_and_inverse_of_the_figures() { + for s in SCAN { + let mut seen = [false; 64]; + for &p in &s { + assert!(!seen[p as usize]); + seen[p as usize] = true; + } + } + // Figure 7-3 row 0 of the alternate scan: 0 4 6 20 22 36 38 52 + let inv: Vec = (0..8).map(|u| SCAN[1].iter().position(|&p| p as usize == u).unwrap()).collect(); + assert_eq!(inv, vec![0, 4, 6, 20, 22, 36, 38, 52]); + // Figure 7-2 row 0 of the zigzag scan: 0 1 5 6 14 15 27 28 + let inv: Vec = (0..8).map(|u| SCAN[0].iter().position(|&p| p as usize == u).unwrap()).collect(); + assert_eq!(inv, vec![0, 1, 5, 6, 14, 15, 27, 28]); + } + + #[test] + fn start_codes_found_at_every_alignment() { + let d = [0, 0, 1, 0xB3, 9, 0, 0, 0, 1, 0x00, 0, 0, 1, 0x01, 0, 0]; + assert_eq!(start_codes(&d), vec![(0, 0xB3), (6, 0x00), (10, 0x01)]); + } + + #[test] + fn frame_rates() { + assert_eq!(frame_rate(4, 0, 0), Some((30000, 1001))); + assert_eq!(frame_rate(3, 1, 0), Some((50, 1))); + assert_eq!(frame_rate(0, 0, 0), None); + assert_eq!(profile_level_name(0x44), "Main@High"); + assert_eq!(profile_level_name(0x85), "4:2:2@Main"); + } +} diff --git a/crates/mpeg2v/src/idct.rs b/crates/mpeg2v/src/idct.rs new file mode 100644 index 000000000..f94b935f0 --- /dev/null +++ b/crates/mpeg2v/src/idct.rs @@ -0,0 +1,179 @@ +//! 8×8 inverse DCT (H.262 Annex A): the separable 2-D IDCT evaluated in `f32` with even/odd +//! symmetry, rounded to the nearest integer and saturated to [-256, 255]. Its error is far inside +//! the IEEE Std 1180-1990 accuracy limits the standard requires (checked by the test procedure of +//! IEEE 1180 itself, below). + +use std::sync::OnceLock; + +/// `M[x][u] = C(u)/2 · cos((2x+1)uπ/16)`, `C(0) = 1/√2`, `C(u>0) = 1`. +fn basis() -> &'static [[f32; 8]; 8] { + static B: OnceLock<[[f32; 8]; 8]> = OnceLock::new(); + B.get_or_init(|| { + let mut m = [[0f32; 8]; 8]; + for (x, row) in m.iter_mut().enumerate() { + for (u, v) in row.iter_mut().enumerate() { + let c = if u == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 }; + *v = (c / 2.0 * (((2 * x + 1) * u) as f64 * std::f64::consts::PI / 16.0).cos()) as f32; + } + } + m + }) +} + +/// In-place IDCT of a block of dequantised coefficients (raster order). `dc_only`: only `blk[0]` +/// may be non-zero. +#[inline] +pub(crate) fn idct(blk: &mut [i32; 64], dc_only: bool) { + if dc_only { + let v = ((blk[0] + 4) >> 3).clamp(-256, 255); + blk.fill(v); + return; + } + let m = basis(); + // The DC term F[0][0]/8 is added exactly (so ties round as in the DC-only path). + let dc = blk[0] as f32 * 0.125; + blk[0] = 0; + // rows: tmp[v][x] = Σ_u M[x][u] F[v][u] + let mut tmp = [[0f32; 8]; 8]; + for v in 0..8 { + let r = &blk[v * 8..v * 8 + 8]; + if r.iter().all(|&c| c == 0) { + continue; + } + let f: [f32; 8] = std::array::from_fn(|u| r[u] as f32); + for x in 0..4 { + let mx = &m[x]; + let e = mx[0] * f[0] + mx[2] * f[2] + mx[4] * f[4] + mx[6] * f[6]; + let o = mx[1] * f[1] + mx[3] * f[3] + mx[5] * f[5] + mx[7] * f[7]; + tmp[v][x] = e + o; + tmp[v][7 - x] = e - o; + } + } + // columns, eight at a time: out[y][x] = Σ_v M[y][v] tmp[v][x] + for y in 0..4 { + let my = &m[y]; + let mut e = [0f32; 8]; + let mut o = [0f32; 8]; + for x in 0..8 { + e[x] = my[0] * tmp[0][x] + my[2] * tmp[2][x] + my[4] * tmp[4][x] + my[6] * tmp[6][x]; + o[x] = my[1] * tmp[1][x] + my[3] * tmp[3][x] + my[5] * tmp[5][x] + my[7] * tmp[7][x]; + } + for x in 0..8 { + blk[y * 8 + x] = ((dc + (e[x] + o[x]) + 0.5).floor() as i32).clamp(-256, 255); + blk[(7 - y) * 8 + x] = ((dc + (e[x] - o[x]) + 0.5).floor() as i32).clamp(-256, 255); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The pseudo-random generator of IEEE Std 1180-1990 (values in [-l, h]). + struct Rand(i64); + impl Rand { + fn next(&mut self, l: i64, h: i64) -> i64 { + self.0 = (self.0.wrapping_mul(1_103_515_245).wrapping_add(12_345)) & 0xFFFF_FFFF; + let i = self.0 & 0x7FFF_FFFE; + let x = i as f64 / 0x7FFF_FFFF as f64 * (l + h + 1) as f64; + x as i64 - l + } + } + + fn c(u: usize) -> f64 { + if u == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 } + } + + fn cosv(a: usize, b: usize) -> f64 { + static T: OnceLock<[[f64; 8]; 8]> = OnceLock::new(); + T.get_or_init(|| std::array::from_fn(|a| std::array::from_fn(|b| (((2 * a + 1) * b) as f64 * std::f64::consts::PI / 16.0).cos())))[a][b] + } + + /// Separable double-precision 1-D passes (exact up to f64 rounding). + fn fdct(p: &[i64; 64]) -> [i64; 64] { + let mut t = [0f64; 64]; + for y in 0..8 { + for u in 0..8 { + t[y * 8 + u] = (0..8).map(|x| p[y * 8 + x] as f64 * cosv(x, u)).sum::() * c(u) / 2.0; + } + } + let mut out = [0i64; 64]; + for v in 0..8 { + for u in 0..8 { + let s = (0..8).map(|y| t[y * 8 + u] * cosv(y, v)).sum::() * c(v) / 2.0; + out[v * 8 + u] = (s.round() as i64).clamp(-2048, 2047); + } + } + out + } + + fn reference_idct(f: &[i64; 64]) -> [i64; 64] { + let mut t = [0f64; 64]; + for v in 0..8 { + for x in 0..8 { + t[v * 8 + x] = (0..8).map(|u| c(u) * f[v * 8 + u] as f64 * cosv(x, u)).sum::() / 2.0; + } + } + let mut out = [0i64; 64]; + for y in 0..8 { + for x in 0..8 { + let s = (0..8).map(|v| c(v) * t[v * 8 + x] * cosv(y, v)).sum::() / 2.0; + out[y * 8 + x] = (s.round() as i64).clamp(-256, 255); + } + } + out + } + + /// IEEE 1180-1990 §3: 10 000 blocks for each range and sign; peak error ≤ 1, per-pixel mean + /// square error ≤ 0.06, overall ≤ 0.02, per-pixel mean error ≤ 0.015, overall ≤ 0.0015. + #[test] + fn ieee_1180_accuracy() { + let blocks = if cfg!(debug_assertions) { 2_000 } else { 10_000 }; + for (l, h) in [(256i64, 255i64), (5, 5), (300, 300)] { + for sign in [1i64, -1] { + let mut rng = Rand(1); + let mut sum_err = [0i64; 64]; + let mut sum_sq = [0i64; 64]; + for _ in 0..blocks { + let mut p = [0i64; 64]; + for v in p.iter_mut() { + *v = rng.next(l, h) * sign; + } + let f = fdct(&p); + let r = reference_idct(&f); + let mut blk: [i32; 64] = std::array::from_fn(|i| f[i] as i32); + idct(&mut blk, false); + for i in 0..64 { + let e = blk[i] as i64 - r[i]; + assert!(e.abs() <= 1, "peak error {e} (range {l}/{h} sign {sign})"); + sum_err[i] += e; + sum_sq[i] += e * e; + } + } + let n = blocks as f64; + for i in 0..64 { + assert!(sum_sq[i] as f64 / n <= 0.06); + assert!((sum_err[i] as f64 / n).abs() <= 0.015); + } + assert!(sum_sq.iter().sum::() as f64 / (64.0 * n) <= 0.02); + assert!((sum_err.iter().sum::() as f64 / (64.0 * n)).abs() <= 0.0015); + } + } + // all-zero input gives all-zero output + let mut z = [0i32; 64]; + idct(&mut z, false); + assert!(z.iter().all(|&v| v == 0)); + } + + #[test] + fn dc_only_matches_the_full_transform() { + for dc in -2048..=2047 { + let mut a = [0i32; 64]; + a[0] = dc; + let mut b = a; + idct(&mut a, true); + idct(&mut b, false); + assert_eq!(a, b, "dc {dc}"); + } + } +} diff --git a/crates/mpeg2v/src/lib.rs b/crates/mpeg2v/src/lib.rs new file mode 100644 index 000000000..edf6c1520 --- /dev/null +++ b/crates/mpeg2v/src/lib.rs @@ -0,0 +1,800 @@ +//! Clean-room MPEG-2 video (ITU-T H.262 | ISO/IEC 13818-2) and MPEG-1 video (ISO/IEC 11172-2) +//! decoder. +//! +//! - Main Profile at Low/Main/High-1440/High Level, 4:2:2 Profile (XDCAM HD422, IMX / D-10), +//! Simple Profile, and MPEG-1 (including D pictures and constrained-parameters streams). +//! - Progressive and interlaced coding: frame pictures (frame / field / dual-prime motion +//! compensation, frame and field DCT) and field pictures (field / 16x8 / dual-prime motion +//! compensation, the second field predicted from the first), alternate scan, both intra VLC +//! tables, linear and non-linear quantiser scale, quantiser matrices (sequence header and quant +//! matrix extension), intra DC precision 8-11 bits, concealment motion vectors. +//! - Output in display order as planar 8-bit frames (two fields woven into one frame), with +//! field order, repeat-first-field, picture type and colour description. +//! +//! ```no_run +//! let mut dec = filmcraft_mpeg2v::Decoder::new(); +//! # let access_units: Vec<(Vec, i64)> = vec![]; +//! for (au, pts) in access_units { +//! for pic in dec.decode(&au, pts)? { +//! println!("{}x{} {:?} pts {}", pic.width, pic.height, pic.picture_type, pic.pts); +//! } +//! } +//! let rest = dec.flush(); +//! # Ok::<(), filmcraft_mpeg2v::Error>(()) +//! ``` + +mod bits; +pub mod headers; +mod idct; +mod mc; +mod slice; +mod vlc; + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +pub use headers::{ + DisplayExtension, Extension, GopHeader, PictureCodingExtension, PictureHeader, PictureType, QuantMatrixExtension, SequenceExtension, SequenceHeader, + profile_level_name, start_codes, +}; + +use headers::{EXTENSION, GROUP_START, PICTURE_START, SEQUENCE_END, SEQUENCE_HEADER, SLICE_MAX, SLICE_MIN}; +use mc::Frame; +use slice::{PicParams, Target, decode_slice, slice_row}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("invalid MPEG video: {0}")] + Invalid(String), + #[error("unsupported MPEG video: {0}")] + Unsupported(String), +} + +pub type Result = std::result::Result; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ChromaFormat { + Yuv420, + Yuv422, + Yuv444, +} + +/// Stream-level properties (sequence header and its extensions). +#[derive(Clone, Debug, PartialEq)] +pub struct SequenceInfo { + /// ISO/IEC 13818-2 (has a sequence extension) or ISO/IEC 11172-2. + pub mpeg2: bool, + pub width: u32, + pub height: u32, + pub chroma: ChromaFormat, + pub profile_and_level: Option, + pub progressive_sequence: bool, + pub low_delay: bool, + /// Frames per second (num, den). + pub frame_rate: Option<(u32, u32)>, + /// Sample aspect ratio. + pub sar: (u32, u32), + /// Display aspect ratio when signalled (MPEG-2 aspect_ratio_information 2-4). + pub dar: Option<(u32, u32)>, + /// (colour_primaries, transfer_characteristics, matrix_coefficients) from the sequence + /// display extension. + pub colour: Option<(u8, u8, u8)>, + pub video_format: Option, + /// bits/s (0: unknown / variable). + pub bit_rate: u64, +} + +impl SequenceInfo { + /// "MPEG-2 Video (Main@High)", "MPEG-1 Video". + pub fn codec_name(&self) -> String { + match (self.mpeg2, self.profile_and_level) { + (false, _) => "MPEG-1 Video".into(), + (true, Some(p)) => format!("MPEG-2 Video ({})", profile_level_name(p)), + (true, None) => "MPEG-2 Video".into(), + } + } +} + +/// A decoded frame (both fields of field-coded pictures), cropped to the coded size. +#[derive(Clone, Debug)] +pub struct Picture { + pub width: u32, + pub height: u32, + pub chroma: ChromaFormat, + pub y: Vec, + pub cb: Vec, + pub cr: Vec, + /// The timestamp passed with the access unit the picture (its first field) came in. + pub pts: i64, + pub picture_type: PictureType, + pub temporal_reference: u16, + pub progressive_frame: bool, + pub top_field_first: bool, + pub repeat_first_field: bool, + /// Coded as two field pictures. + pub field_pictures: bool, + pub info: Arc, +} + +impl Picture { + pub fn chroma_width(&self) -> u32 { + match self.chroma { + ChromaFormat::Yuv444 => self.width, + _ => self.width.div_ceil(2), + } + } + pub fn chroma_height(&self) -> u32 { + match self.chroma { + ChromaFormat::Yuv420 => self.height.div_ceil(2), + _ => self.height, + } + } + /// Interlaced content and which field comes first: `None` for progressive frames. + pub fn field_order(&self) -> Option { + if self.progressive_frame { + None + } else if self.top_field_first { + Some(FieldOrder::TopFirst) + } else { + Some(FieldOrder::BottomFirst) + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FieldOrder { + TopFirst, + BottomFirst, +} + +/// The active sequence. +#[derive(Clone)] +struct Seq { + hdr: SequenceHeader, + ext: Option, + display: Option, + info: Arc, + mb_width: usize, + mb_height: usize, +} + +/// MPEG-1 pel_aspect_ratio (height/width of a sample) ×10000, Table 2-D.2. +const MPEG1_PEL_ASPECT: [u32; 15] = [0, 10000, 6735, 7031, 7615, 8055, 8437, 8935, 9157, 9815, 10255, 10695, 10950, 11575, 12015]; + +impl Seq { + fn new(hdr: SequenceHeader, ext: Option, display: Option) -> Result { + let (w, h) = match &ext { + Some(e) => (hdr.horizontal_size | (e.horizontal_size_extension as u32) << 12, hdr.vertical_size | (e.vertical_size_extension as u32) << 12), + None => (hdr.horizontal_size, hdr.vertical_size), + }; + let chroma = match ext.map_or(1, |e| e.chroma_format) { + 1 => ChromaFormat::Yuv420, + 2 => ChromaFormat::Yuv422, + 3 => ChromaFormat::Yuv444, + _ => return Err(Error::Invalid("reserved chroma_format".into())), + }; + if w > 16383 || h > 16383 { + return Err(Error::Unsupported(format!("{w}x{h}"))); + } + let progressive_sequence = ext.is_none_or(|e| e.progressive_sequence); + let mb_width = (w as usize).div_ceil(16); + let mb_height = if progressive_sequence { (h as usize).div_ceil(16) } else { 2 * (h as usize).div_ceil(32) }; + let frame_rate = headers::frame_rate(hdr.frame_rate_code, ext.map_or(0, |e| e.frame_rate_extension_n), ext.map_or(0, |e| e.frame_rate_extension_d)); + let (sar, dar) = if ext.is_some() { + let (dw, dh) = display + .filter(|d| d.display_horizontal_size > 0 && d.display_vertical_size > 0) + .map_or((w, h), |d| (d.display_horizontal_size as u32, d.display_vertical_size as u32)); + let dar = match hdr.aspect_ratio_information { + 2 => Some((4, 3)), + 3 => Some((16, 9)), + 4 => Some((221, 100)), + _ => None, + }; + let sar = match dar { + Some((n, d)) => { + // the display rectangle (dw × dh samples) has this aspect: SAR = DAR · dh / dw + let (n, d) = (n as u64 * dh as u64, d as u64 * dw as u64); + let g = gcd64(n, d); + ((n / g) as u32, (d / g) as u32) + } + None => (1, 1), + }; + (reduce_sar(sar), dar) + } else { + let v = MPEG1_PEL_ASPECT.get(hdr.aspect_ratio_information as usize).copied().filter(|&v| v > 0).unwrap_or(10000); + let g = headers::gcd(10000, v); + ((10000 / g, v / g), None) + }; + let bit_rate = (hdr.bit_rate_value as u64 | (ext.map_or(0, |e| e.bit_rate_extension as u64) << 18)) * 400; + let info = SequenceInfo { + mpeg2: ext.is_some(), + width: w, + height: h, + chroma, + profile_and_level: ext.map(|e| e.profile_and_level_indication), + progressive_sequence, + low_delay: ext.is_some_and(|e| e.low_delay), + frame_rate, + sar, + dar, + colour: display.and_then(|d| d.colour), + video_format: display.map(|d| d.video_format), + bit_rate: if hdr.bit_rate_value == 0x3FFFF && ext.is_none() { 0 } else { bit_rate }, + }; + Ok(Seq { hdr, ext, display, info: Arc::new(info), mb_width, mb_height }) + } + + fn shifts(&self) -> (u32, u32) { + match self.info.chroma { + ChromaFormat::Yuv420 => (1, 1), + ChromaFormat::Yuv422 => (1, 0), + ChromaFormat::Yuv444 => (0, 0), + } + } +} + +fn gcd64(a: u64, b: u64) -> u64 { + if b == 0 { a.max(1) } else { gcd64(b, a % b) } +} + +fn reduce_sar((n, d): (u32, u32)) -> (u32, u32) { + if n == 0 || d == 0 { + return (1, 1); + } + let g = headers::gcd(n, d); + (n / g, d / g) +} + +/// Parse the first sequence header (and its extensions) in `data`. +pub fn probe(data: &[u8]) -> Option { + let codes = start_codes(data); + let mut seq: Option<(SequenceHeader, Option, Option)> = None; + for (k, &(pos, code)) in codes.iter().enumerate() { + let end = codes.get(k + 1).map_or(data.len(), |c| c.0); + let payload = &data[(pos + 4).min(end)..end]; + match code { + SEQUENCE_HEADER if seq.is_none() => seq = Some((SequenceHeader::parse(payload).ok()?, None, None)), + EXTENSION => { + if let Some(s) = seq.as_mut() { + match Extension::parse(payload) { + Ok(Extension::Sequence(e)) => s.1 = Some(e), + Ok(Extension::Display(d)) => s.2 = Some(d), + _ => {} + } + } + } + _ if seq.is_some() => break, + _ => {} + } + } + let (h, e, d) = seq?; + Seq::new(h, e, d).ok().map(|s| (*s.info).clone()) +} + +/// What an access unit holds, without decoding it. +#[derive(Clone, Debug, Default)] +pub struct AccessUnitInfo { + pub sequence_header: bool, + pub gop: Option, + pub pictures: Vec<(PictureHeader, Option)>, +} + +impl AccessUnitInfo { + /// Starts with an I picture (decoding can begin here when a sequence header has been seen). + pub fn is_intra(&self) -> bool { + self.pictures.first().is_some_and(|p| matches!(p.0.coding_type, PictureType::I | PictureType::D)) + } + /// Only B pictures (never used for prediction). + pub fn is_disposable(&self) -> bool { + !self.pictures.is_empty() && self.pictures.iter().all(|p| p.0.coding_type == PictureType::B) + } +} + +pub fn scan_access_unit(data: &[u8]) -> AccessUnitInfo { + let codes = start_codes(data); + let mut info = AccessUnitInfo::default(); + for (k, &(pos, code)) in codes.iter().enumerate() { + let end = codes.get(k + 1).map_or(data.len(), |c| c.0); + let payload = &data[(pos + 4).min(end)..end]; + match code { + SEQUENCE_HEADER => info.sequence_header = true, + GROUP_START => info.gop = GopHeader::parse(payload).ok(), + PICTURE_START => { + if let Ok(ph) = PictureHeader::parse(payload) { + info.pictures.push((ph, None)); + } + } + EXTENSION => { + if let (Ok(Extension::PictureCoding(p)), Some(last)) = (Extension::parse(payload), info.pictures.last_mut()) { + last.1 = Some(p); + } + } + _ => {} + } + } + info +} + +/// Split an elementary stream into access units: one coded frame each (a frame picture or a +/// pair of field pictures) with the sequence / GOP headers before it. Bytes before the first +/// header are skipped. +pub fn access_units(es: &[u8]) -> Vec> { + let codes = start_codes(es); + let mut out = Vec::new(); + let mut cur: Option = None; + let mut header: Option = None; + let mut open_field: Option = None; + for (k, &(pos, code)) in codes.iter().enumerate() { + match code { + SEQUENCE_HEADER | GROUP_START => { + header.get_or_insert(pos); + } + PICTURE_START => { + // picture_structure from the picture coding extension that follows + let structure = codes[k + 1..] + .iter() + .take_while(|c| c.1 == EXTENSION || c.1 == headers::USER_DATA) + .find_map(|&(p, c)| { + let end = codes.iter().find(|x| x.0 > p).map_or(es.len(), |x| x.0); + (c == EXTENSION).then(|| Extension::parse(&es[(p + 4).min(end)..end]).ok()).flatten() + }) + .and_then(|e| match e { + Extension::PictureCoding(p) => Some(p.picture_structure), + _ => None, + }) + .unwrap_or(3); + let start = header.take().unwrap_or(pos); + let second = matches!(open_field, Some(s) if structure != 3 && structure != s); + if second { + open_field = None; + } else { + if let Some(c) = cur { + out.push(c..start); + } + cur = Some(start); + open_field = (structure != 3).then_some(structure); + } + } + _ => {} + } + } + if let Some(c) = cur { + out.push(c..es.len()); + } + out +} + +/// A picture whose header has been read and whose slices are being collected. +struct Pending { + ph: PictureHeader, + pce: Option, + /// (start code, payload range). + slices: Vec<(u8, std::ops::Range)>, +} + +#[derive(Clone)] +struct Meta { + pts: i64, + ptype: PictureType, + temporal_reference: u16, + pce: PictureCodingExtension, + field_pictures: bool, + info: Arc, +} + +/// A frame whose first field has been decoded. +struct Building { + frame: Frame, + parity: usize, + meta: Meta, +} + +struct Anchor { + frame: Arc, + meta: Meta, + shown: bool, +} + +/// A stateful decoder. Feed access units (or any chunks holding whole pictures) in decode order; +/// pictures come out in display order. +pub struct Decoder { + seq: Option, + /// A sequence header is waiting for its extensions (applied at the next non-extension unit). + seq_pending: Option<(SequenceHeader, Option, Option)>, + matrices: [[u8; 64]; 4], + closed_gop: bool, + /// Older and newer anchor (reference) frames. + fwd: Option, + bwd: Option, + building: Option, + pending: Option, + out: Vec, + errors: usize, + last_error: Option<&'static str>, + threads: bool, +} + +impl Default for Decoder { + fn default() -> Self { + Self::new() + } +} + +impl Decoder { + pub fn new() -> Decoder { + Decoder { + seq: None, + seq_pending: None, + matrices: [headers::DEFAULT_INTRA, [16; 64], headers::DEFAULT_INTRA, [16; 64]], + closed_gop: false, + fwd: None, + bwd: None, + building: None, + pending: None, + out: Vec::new(), + errors: 0, + last_error: None, + threads: cfg!(feature = "threads"), + } + } + + /// Decode slices on the rayon pool (default with the `threads` feature). + pub fn set_threads(&mut self, on: bool) { + self.threads = on && cfg!(feature = "threads"); + } + + /// Slices that failed to decode so far (corrupt data; the picture is still output). + pub fn errors(&self) -> usize { + self.errors + } + + /// What the most recent slice error was. + pub fn last_error(&self) -> Option<&'static str> { + self.last_error + } + + /// The active sequence. + pub fn sequence(&self) -> Option<&SequenceInfo> { + self.seq.as_ref().map(|s| &*s.info) + } + + /// Decode a chunk of the elementary stream holding whole pictures (an access unit) tagged + /// with `pts`. Returns the pictures that became displayable, in display order. + pub fn decode(&mut self, data: &[u8], pts: i64) -> Result> { + let codes = start_codes(data); + let mut err = None; + for (k, &(pos, code)) in codes.iter().enumerate() { + let end = codes.get(k + 1).map_or(data.len(), |c| c.0); + let range = (pos + 4).min(end)..end; + let payload = &data[range.clone()]; + if code != EXTENSION { + self.apply_sequence(); + } + match code { + SEQUENCE_HEADER => { + self.finish_picture(data, pts); + match SequenceHeader::parse(payload) { + Ok(h) => self.seq_pending = Some((h, None, None)), + Err(e) => err = Some(e), + } + } + EXTENSION => match Extension::parse(payload) { + Ok(Extension::Sequence(e)) => { + if let Some(s) = self.seq_pending.as_mut() { + s.1 = Some(e); + } else if let Some(s) = &self.seq { + self.seq_pending = Some((s.hdr.clone(), Some(e), s.display)); + } + } + Ok(Extension::Display(d)) => { + if let Some(s) = self.seq_pending.as_mut() { + s.2 = Some(d); + } + } + Ok(Extension::QuantMatrix(q)) => { + if let Some(m) = q.intra { + self.matrices[0] = m; + self.matrices[2] = m; + } + if let Some(m) = q.non_intra { + self.matrices[1] = m; + self.matrices[3] = m; + } + if let Some(m) = q.chroma_intra { + self.matrices[2] = m; + } + if let Some(m) = q.chroma_non_intra { + self.matrices[3] = m; + } + } + Ok(Extension::PictureCoding(p)) => { + if let Some(pd) = self.pending.as_mut() { + pd.pce = Some(p); + } + } + Ok(Extension::Other(_)) => {} + Err(e) => err = Some(e), + }, + GROUP_START => { + self.finish_picture(data, pts); + if let Ok(g) = GopHeader::parse(payload) { + self.closed_gop = g.closed_gop || g.broken_link; + } + } + PICTURE_START => { + self.finish_picture(data, pts); + match PictureHeader::parse(payload) { + Ok(ph) => self.pending = Some(Pending { ph, pce: None, slices: Vec::new() }), + Err(e) => err = Some(e), + } + } + SLICE_MIN..=SLICE_MAX => { + if let Some(pd) = self.pending.as_mut() { + pd.slices.push((code, range)); + } + } + SEQUENCE_END => self.finish_picture(data, pts), + _ => {} + } + } + self.apply_sequence(); + self.finish_picture(data, pts); + let out = std::mem::take(&mut self.out); + match err { + Some(e) if out.is_empty() && self.seq.is_none() => Err(e), + _ => Ok(out), + } + } + + /// Output the frames held back for reordering (end of stream). + pub fn flush(&mut self) -> Vec { + if let Some(b) = self.building.take() { + self.complete(b.frame, b.meta); + } + if let Some(a) = self.bwd.as_mut() + && !a.shown + { + a.shown = true; + let p = to_picture(&a.frame, &a.meta); + self.out.push(p); + } + std::mem::take(&mut self.out) + } + + /// Forget all pictures (before decoding from a new random-access point). The sequence + /// parameters are kept: a random-access point without a sequence header still decodes. + pub fn reset(&mut self) { + self.fwd = None; + self.bwd = None; + self.building = None; + self.pending = None; + self.out.clear(); + self.closed_gop = false; + } + + fn apply_sequence(&mut self) { + let Some((h, e, d)) = self.seq_pending.take() else { return }; + // a sequence header resets the quantiser matrices (§6.3.11) + let intra = h.intra_matrix.unwrap_or(headers::DEFAULT_INTRA); + let non_intra = h.non_intra_matrix.unwrap_or([16; 64]); + self.matrices = [intra, non_intra, intra, non_intra]; + match Seq::new(h, e, d) { + Ok(s) => { + let changed = self.seq.as_ref().is_none_or(|o| o.mb_width != s.mb_width || o.mb_height != s.mb_height || o.info.chroma != s.info.chroma); + if changed { + self.reset(); + } + self.seq = Some(s); + } + Err(_) => self.errors += 1, + } + } + + fn finish_picture(&mut self, data: &[u8], pts: i64) { + let Some(pd) = self.pending.take() else { return }; + let Some(seq) = self.seq.clone() else { + self.errors += 1; + return; + }; + let mpeg2 = seq.ext.is_some(); + let pce = match (mpeg2, pd.pce) { + (true, Some(p)) => p, + (true, None) => { + self.errors += 1; + return; + } + (false, _) => PictureCodingExtension::mpeg1(&pd.ph), + }; + if seq.info.chroma == ChromaFormat::Yuv444 { + self.errors += 1; + return; + } + let ptype = pd.ph.coding_type; + let (shx, shy) = seq.shifts(); + let (w, h) = (seq.mb_width * 16, seq.mb_height * 16); + let (cw, ch) = (w >> shx, h >> shy); + let structure = pce.picture_structure; + let parity = if structure == 2 { 1 } else { 0 }; + // field pairing + let mut second = None; + if structure == 3 { + if let Some(b) = self.building.take() { + self.complete(b.frame, b.meta); + } + } else if let Some(b) = self.building.take() { + if b.parity != parity && b.frame.w == w && b.frame.h == h { + second = Some(b); + } else { + self.complete(b.frame, b.meta); + } + } + // B pictures need their forward reference unless the GOP is closed + if ptype == PictureType::B && second.is_none() && (self.bwd.is_none() || (self.fwd.is_none() && !self.closed_gop)) { + return; + } + let is_second = second.is_some(); + let (mut frame, meta) = match second { + Some(b) => (b.frame, b.meta), + None => ( + Frame::new(w, h, cw, ch), + Meta { pts, ptype, temporal_reference: pd.ph.temporal_reference, pce, field_pictures: structure != 3, info: seq.info.clone() }, + ), + }; + // a P second field may predict from the first field of its own frame + let first_field = (is_second && ptype == PictureType::P).then(|| frame.clone()); + let (fwd, bwd) = match ptype { + PictureType::P => (self.bwd.as_ref().map(|a| &*a.frame), None), + PictureType::B => (self.fwd.as_ref().map(|a| &*a.frame), self.bwd.as_ref().map(|a| &*a.frame)), + _ => (None, None), + }; + let p = PicParams { + mpeg2, + ptype, + pce, + full_pel: [pd.ph.full_pel_forward_vector, pd.ph.full_pel_backward_vector], + shx, + shy, + mb_width: seq.mb_width, + mb_rows: if structure == 3 { seq.mb_height } else { seq.mb_height / 2 }, + matrices: self.matrices, + fwd, + bwd, + first_field: first_field.as_ref(), + parity, + vertical_size_extension: seq.info.height > 2800, + }; + let errors = SliceErrors::default(); + decode_picture(&p, data, &pd.slices, &mut frame, structure, self.threads, &errors); + self.errors += errors.count.load(Ordering::Relaxed); + if let Some(e) = errors.first.into_inner().unwrap_or(None) { + self.last_error = Some(e); + } + if structure != 3 && !is_second { + self.building = Some(Building { frame, parity, meta }); + } else { + self.complete(frame, meta); + } + } + + fn complete(&mut self, frame: Frame, meta: Meta) { + if meta.ptype.is_anchor() { + if let Some(mut prev) = self.bwd.take() { + if !prev.shown { + prev.shown = true; + self.out.push(to_picture(&prev.frame, &prev.meta)); + } + self.fwd = Some(prev); + } + self.bwd = Some(Anchor { frame: Arc::new(frame), meta, shown: false }); + } else { + self.out.push(to_picture(&frame, &meta)); + } + } +} + +/// Slice errors of one picture (slices may decode on several threads). +#[derive(Default)] +struct SliceErrors { + count: AtomicUsize, + first: std::sync::Mutex>, +} + +impl SliceErrors { + fn add(&self, what: &'static str) { + self.count.fetch_add(1, Ordering::Relaxed); + let mut f = self.first.lock().unwrap_or_else(|e| e.into_inner()); + f.get_or_insert(what); + } +} + +/// Decode the slices of one picture into `frame`. +fn decode_picture(p: &PicParams, data: &[u8], slices: &[(u8, std::ops::Range)], frame: &mut Frame, structure: u8, threads: bool, errors: &SliceErrors) { + let (w, cw) = (frame.w, frame.cw); + let field = (structure != 3).then_some(p.parity); + let lines = if structure == 3 { 16 } else { 32 }; + let clines = lines >> p.shy; + let [y, cb, cr] = &mut frame.planes; + if !p.mpeg2 { + // MPEG-1 slices may span rows: decode in order into the whole picture + let mut t = Target { planes: [&mut y[..], &mut cb[..], &mut cr[..]], strides: [w, cw], line0: [0, 0], field }; + for (code, r) in slices { + let payload = &data[r.clone()]; + if let Err(e) = decode_slice(p, payload, *code as usize - 1, false, &mut t) { + errors.add(e.0); + } + } + return; + } + let mut rows: Vec> = vec![Vec::new(); p.mb_rows]; + for (code, r) in slices { + let payload = &data[r.clone()]; + let row = slice_row(p.vertical_size_extension, *code, payload); + match rows.get_mut(row) { + Some(v) => v.push(payload), + None => { + errors.add("slice row out of range"); + } + } + } + let run = |r: usize, y: &mut [u8], cb: &mut [u8], cr: &mut [u8]| { + let Some(list) = rows.get(r) else { return }; + let mut t = Target { planes: [y, cb, cr], strides: [w, cw], line0: [r * lines, r * clines], field }; + for payload in list { + if let Err(e) = decode_slice(p, payload, r, true, &mut t) { + errors.add(e.0); + } + } + }; + #[cfg(feature = "threads")] + if threads && p.mb_rows >= 4 { + use rayon::prelude::*; + y.par_chunks_mut(lines * w) + .zip(cb.par_chunks_mut(clines * cw)) + .zip(cr.par_chunks_mut(clines * cw)) + .enumerate() + .for_each(|(r, ((y, cb), cr))| run(r, y, cb, cr)); + return; + } + let _ = threads; + for (r, ((y, cb), cr)) in y.chunks_mut(lines * w).zip(cb.chunks_mut(clines * cw)).zip(cr.chunks_mut(clines * cw)).enumerate() { + run(r, y, cb, cr); + } +} + +fn to_picture(f: &Frame, meta: &Meta) -> Picture { + let info = &meta.info; + let (w, h) = (info.width as usize, info.height as usize); + let (cw, ch) = match info.chroma { + ChromaFormat::Yuv420 => (w.div_ceil(2), h.div_ceil(2)), + ChromaFormat::Yuv422 => (w.div_ceil(2), h), + ChromaFormat::Yuv444 => (w, h), + }; + let crop = |src: &[u8], stride: usize, w: usize, h: usize| { + let mut v = Vec::with_capacity(w * h); + for r in 0..h { + v.extend_from_slice(&src[r * stride..r * stride + w]); + } + v + }; + let progressive_frame = meta.pce.progressive_frame; + Picture { + width: w as u32, + height: h as u32, + chroma: info.chroma, + y: crop(&f.planes[0], f.w, w, h), + cb: crop(&f.planes[1], f.cw, cw, ch), + cr: crop(&f.planes[2], f.cw, cw, ch), + pts: meta.pts, + picture_type: meta.ptype, + temporal_reference: meta.temporal_reference, + progressive_frame, + // field pictures: the first coded field is displayed first + top_field_first: if meta.field_pictures { meta.pce.picture_structure == 1 } else { meta.pce.top_field_first }, + repeat_first_field: meta.pce.repeat_first_field, + field_pictures: meta.field_pictures, + info: meta.info.clone(), + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/mpeg2v/src/mc.rs b/crates/mpeg2v/src/mc.rs new file mode 100644 index 000000000..937b4bc9a --- /dev/null +++ b/crates/mpeg2v/src/mc.rs @@ -0,0 +1,154 @@ +//! Frame buffers and motion-compensated prediction (H.262 §7.6). + +/// A decoded frame, padded to whole macroblocks. +#[derive(Clone)] +pub(crate) struct Frame { + pub planes: [Vec; 3], + /// Luma width / height (multiples of 16). + pub w: usize, + pub h: usize, + /// Chroma width / height. + pub cw: usize, + pub ch: usize, +} + +impl Frame { + pub fn new(w: usize, h: usize, cw: usize, ch: usize) -> Frame { + Frame { planes: [vec![16; w * h], vec![128; cw * ch], vec![128; cw * ch]], w, h, cw, ch } + } + + /// Plane `c` (0 Y, 1 Cb, 2 Cr) as a reference: the whole frame, or one field (0 top, 1 bottom). + #[inline] + pub fn plane(&self, c: usize, field: Option) -> RefPlane<'_> { + let (w, h) = if c == 0 { (self.w, self.h) } else { (self.cw, self.ch) }; + match field { + None => RefPlane { data: &self.planes[c], off: 0, stride: w, w, h }, + Some(p) => RefPlane { data: &self.planes[c], off: p * w, stride: 2 * w, w, h: h / 2 }, + } + } +} + +/// A plane (or a field of one) to predict from. +#[derive(Clone, Copy)] +pub(crate) struct RefPlane<'a> { + pub data: &'a [u8], + pub off: usize, + pub stride: usize, + pub w: usize, + pub h: usize, +} + +/// Form the `bw`×`bh` prediction at integer position (`x`, `y`) with half-sample flags into +/// `dst` (row `r` at `dst[doff + r·dstride..]`), or average it with what is there (`avg`). +/// Positions outside the reference are clamped to its edges (not allowed in conforming streams; +/// this keeps corrupt ones safe). +#[allow(clippy::too_many_arguments)] +#[inline] +pub(crate) fn predict(dst: &mut [u8], doff: usize, dstride: usize, src: RefPlane, x: i32, y: i32, hx: bool, hy: bool, bw: usize, bh: usize, avg: bool) { + let (ex, ey) = (hx as i32, hy as i32); + let inside = x >= 0 && y >= 0 && (x + bw as i32 + ex) as usize <= src.w && (y + bh as i32 + ey) as usize <= src.h; + let mut tmp = [0u8; 16]; + if inside { + let s = src.stride; + let base = src.off + y as usize * s + x as usize; + for r in 0..bh { + let p = base + r * s; + let a = &src.data[p..p + bw + 1 - (!hx) as usize]; + let row = &mut tmp[..bw]; + match (hx, hy) { + (false, false) => row.copy_from_slice(&a[..bw]), + (true, false) => { + for i in 0..bw { + row[i] = ((a[i] as u16 + a[i + 1] as u16 + 1) >> 1) as u8; + } + } + (false, true) => { + let b = &src.data[p + s..p + s + bw]; + for i in 0..bw { + row[i] = ((a[i] as u16 + b[i] as u16 + 1) >> 1) as u8; + } + } + (true, true) => { + let b = &src.data[p + s..p + s + bw + 1]; + for i in 0..bw { + row[i] = ((a[i] as u16 + a[i + 1] as u16 + b[i] as u16 + b[i + 1] as u16 + 2) >> 2) as u8; + } + } + } + store(dst, doff + r * dstride, row, avg); + } + return; + } + let get = |xx: i32, yy: i32| -> u16 { + let xx = xx.clamp(0, src.w as i32 - 1) as usize; + let yy = yy.clamp(0, src.h as i32 - 1) as usize; + src.data[src.off + yy * src.stride + xx] as u16 + }; + for r in 0..bh { + let yy = y + r as i32; + let row = &mut tmp[..bw]; + for (i, v) in row.iter_mut().enumerate() { + let xx = x + i as i32; + *v = match (hx, hy) { + (false, false) => get(xx, yy), + (true, false) => (get(xx, yy) + get(xx + 1, yy) + 1) >> 1, + (false, true) => (get(xx, yy) + get(xx, yy + 1) + 1) >> 1, + (true, true) => (get(xx, yy) + get(xx + 1, yy) + get(xx, yy + 1) + get(xx + 1, yy + 1) + 2) >> 2, + } as u8; + } + store(dst, doff + r * dstride, row, avg); + } +} + +#[inline(always)] +fn store(dst: &mut [u8], at: usize, row: &[u8], avg: bool) { + let d = &mut dst[at..at + row.len()]; + if avg { + for (o, &p) in d.iter_mut().zip(row) { + *o = ((*o as u16 + p as u16 + 1) >> 1) as u8; + } + } else { + d.copy_from_slice(row); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn frame() -> Frame { + let mut f = Frame::new(32, 32, 16, 16); + for (i, v) in f.planes[0].iter_mut().enumerate() { + *v = (i % 32 * 3 + i / 32 * 5) as u8; + } + f + } + + #[test] + fn half_sample_interpolation_rounds_up() { + let f = frame(); + let p = f.plane(0, None); + let mut d = [0u8; 4]; + predict(&mut d, 0, 2, p, 1, 1, true, true, 2, 2, false); + let s = |x: usize, y: usize| f.planes[0][y * 32 + x] as u16; + assert_eq!(d[0] as u16, (s(1, 1) + s(2, 1) + s(1, 2) + s(2, 2) + 2) >> 2); + predict(&mut d, 0, 2, p, 1, 1, true, false, 2, 2, false); + assert_eq!(d[3] as u16, (s(2, 2) + s(3, 2) + 1) >> 1); + } + + #[test] + fn fields_and_clamped_edges() { + let f = frame(); + let mut d = [0u8; 4]; + // bottom field row 0 = frame row 1 + predict(&mut d, 0, 2, f.plane(0, Some(1)), 0, 0, false, false, 2, 2, false); + assert_eq!(d, [f.planes[0][32], f.planes[0][33], f.planes[0][96], f.planes[0][97]]); + // outside: clamped to the corner + predict(&mut d, 0, 2, f.plane(0, None), -10, -10, false, false, 2, 2, false); + assert_eq!(d, [f.planes[0][0]; 4]); + // averaging + let mut d = [10u8; 4]; + predict(&mut d, 0, 2, f.plane(0, None), -10, -10, false, false, 2, 2, true); + assert_eq!(d[0], ((10 + f.planes[0][0] as u16 + 1) >> 1) as u8); + } +} diff --git a/crates/mpeg2v/src/slice.rs b/crates/mpeg2v/src/slice.rs new file mode 100644 index 000000000..e6c6f5549 --- /dev/null +++ b/crates/mpeg2v/src/slice.rs @@ -0,0 +1,615 @@ +//! Slice, macroblock and block decoding (H.262 §6.2.4-§6.2.6, §7.2-§7.6; ISO/IEC 11172-2 +//! §2.4.2.7-§2.4.4). + +use crate::bits::Bits; +use crate::headers::{PictureCodingExtension, PictureType, SCAN}; +use crate::idct::idct; +use crate::mc::{Frame, predict}; +use crate::vlc::{DCT_EOB, DCT_ESCAPE, MB_BWD, MB_FWD, MB_INTRA, MB_PATTERN, MB_QUANT, MBA_ESCAPE, MBA_STUFFING, tables}; + +/// Non-linear quantiser_scale (Table 7-6), indexed by quantiser_scale_code. +const NON_LINEAR_Q: [u8; 32] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 22, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80, 88, 96, 104, 112]; + +/// Motion types (frame_motion_type / field_motion_type values). +const MT_FIELD: u8 = 1; +const MT_FRAME: u8 = 2; // frame pictures; 16x8 MC in field pictures +const MT_16X8: u8 = 2; +const MT_DUAL: u8 = 3; + +/// Everything slices of one picture share (read-only while slices decode in parallel). +pub(crate) struct PicParams<'a> { + pub mpeg2: bool, + pub ptype: PictureType, + pub pce: PictureCodingExtension, + pub full_pel: [bool; 2], + /// Chroma subsampling shifts (4:2:0: 1, 1; 4:2:2: 1, 0). + pub shx: u32, + pub shy: u32, + pub mb_width: usize, + /// Macroblock rows of this picture (field rows for field pictures). + pub mb_rows: usize, + /// Intra luma, non-intra luma, intra chroma, non-intra chroma (raster order). + pub matrices: [[u8; 64]; 4], + /// Forward reference (P: the latest anchor; B: the older anchor) and backward reference. + pub fwd: Option<&'a Frame>, + pub bwd: Option<&'a Frame>, + /// The first field of this frame (when decoding its second field). + pub first_field: Option<&'a Frame>, + /// Field pictures: parity of this field (0 top, 1 bottom). + pub parity: usize, + pub vertical_size_extension: bool, +} + +impl PicParams<'_> { + fn frame_picture(&self) -> bool { + self.pce.picture_structure == 3 + } + fn chroma_blocks(&self) -> usize { + if self.shy == 1 { 2 } else { 4 } + } + /// Chroma macroblock size. + fn cmb(&self) -> (usize, usize) { + (16 >> self.shx, 16 >> self.shy) + } +} + +/// Where a row (or, for MPEG-1, the whole picture) of macroblocks is written. +pub(crate) struct Target<'a> { + pub planes: [&'a mut [u8]; 3], + pub strides: [usize; 2], + /// First luma / chroma frame line held by `planes`. + pub line0: [usize; 2], + /// Field pictures: parity written. + pub field: Option, +} + +impl Target<'_> { + fn put(&mut self, mbx: usize, mby: usize, mb: &MbBuf, cmb: (usize, usize)) { + for (c, plane) in self.planes.iter_mut().enumerate() { + let k = (c > 0) as usize; + let (bw, bh) = if c == 0 { (16, 16) } else { cmb }; + let stride = self.strides[k]; + let src = if c == 0 { &mb.y[..] } else { &mb.c[c - 1][..] }; + for l in 0..bh { + let line = match self.field { + None => bh * mby + l, + Some(p) => 2 * (bh * mby + l) + p, + }; + let Some(local) = line.checked_sub(self.line0[k]) else { continue }; + let at = local * stride + bw * mbx; + if at + bw <= plane.len() { + plane[at..at + bw].copy_from_slice(&src[l * bw..l * bw + bw]); + } + } + } + } +} + +/// One macroblock's samples (luma 16×16; chroma rows of `16 >> shx` samples). +pub(crate) struct MbBuf { + y: [u8; 256], + c: [[u8; 256]; 2], +} + +#[derive(Clone, Copy, Default)] +struct Motion { + flags: i16, + mtype: u8, + /// vectors[r][s][t] in half-sample units (vertical in field units for field prediction). + mv: [[[i32; 2]; 2]; 2], + select: [[usize; 2]; 2], + dmv: [i32; 2], +} + +struct SliceState { + qscale: i32, + dc_pred: [i32; 3], + pmv: [[[i32; 2]; 2]; 2], + last: Motion, +} + +fn dc_reset(p: &PicParams) -> i32 { + 1 << (7 + p.pce.intra_dc_precision) +} + +#[derive(Debug)] +pub(crate) struct SliceError(pub &'static str); + +type R = std::result::Result; + +/// Decode one slice (`data`: the bytes after its start code) into `t`. `row`: the slice's +/// macroblock row; `single_row`: MPEG-2 slices may not leave their row. +pub(crate) fn decode_slice(p: &PicParams, data: &[u8], row: usize, single_row: bool, t: &mut Target) -> R<()> { + let tb = tables(); + let mut b = Bits::new(data); + if p.vertical_size_extension { + b.skip(3); // already applied to `row` by the caller + } + let qcode = b.read(5) as usize; + if p.mpeg2 && b.peek(1) == 1 { + b.skip(9); // intra_slice_flag, intra_slice, reserved_bits + } + while b.bit() { + b.skip(8); // extra_information_slice + } + let mut st = SliceState { qscale: qscale(p, qcode), dc_pred: [dc_reset(p); 3], pmv: [[[0; 2]; 2]; 2], last: Motion::default() }; + let total = p.mb_width * p.mb_rows; + let row_end = if single_row { (row + 1) * p.mb_width } else { total }; + let mut addr: isize = (row * p.mb_width) as isize - 1; + let mut first = true; + let mut buf = MbBuf { y: [0; 256], c: [[0; 256]; 2] }; + let mut blk = [0i32; 64]; + loop { + // macroblock_address_increment + let mut inc = 0isize; + loop { + match tb.mba.decode(&mut b) { + Some(MBA_ESCAPE) => inc += 33, + Some(MBA_STUFFING) => {} + Some(n) => { + inc += n as isize; + break; + } + None => return Err(SliceError("bad macroblock_address_increment")), + } + if b.overrun() { + return Err(SliceError("truncated slice")); + } + } + if !first && inc > 1 { + // skipped macroblocks + for _ in 1..inc { + addr += 1; + if addr as usize >= row_end { + return Err(SliceError("macroblock address past the row")); + } + skipped_mb(p, &mut st, addr as usize, &mut buf, t)?; + } + addr += 1; + } else { + addr += inc; + } + first = false; + if addr < 0 || addr as usize >= row_end { + return Err(SliceError("macroblock address out of range")); + } + let a = addr as usize; + macroblock(p, &mut st, &mut b, a, &mut buf, &mut blk, t)?; + if b.overrun() { + return Err(SliceError("truncated macroblock")); + } + // next_start_code: 23 zero bits end the slice + if b.bits_left() <= 0 || b.peek(23) == 0 { + return Ok(()); + } + } +} + +fn qscale(p: &PicParams, code: usize) -> i32 { + if !p.mpeg2 { + code as i32 + } else if p.pce.q_scale_type { + NON_LINEAR_Q[code & 31] as i32 + } else { + 2 * code as i32 + } +} + +/// A skipped macroblock (§7.6.6): P pictures predict from the same position with a zero vector; +/// B pictures repeat the previous macroblock's prediction. +fn skipped_mb(p: &PicParams, st: &mut SliceState, addr: usize, buf: &mut MbBuf, t: &mut Target) -> R<()> { + st.dc_pred = [dc_reset(p); 3]; + let m = match p.ptype { + PictureType::P => { + st.pmv = [[[0; 2]; 2]; 2]; + let mut m = Motion { flags: MB_FWD, mtype: if p.frame_picture() { MT_FRAME } else { MT_FIELD }, ..Default::default() }; + m.select[0][0] = p.parity; + m + } + PictureType::B => { + let last = st.last; + if last.flags & MB_INTRA != 0 { + return Err(SliceError("skipped macroblock after an intra macroblock")); + } + // the previous macroblock's direction, one vector per direction (PMV[0][s]) + let mut m = Motion { flags: last.flags & (MB_FWD | MB_BWD), mtype: if p.frame_picture() { MT_FRAME } else { MT_FIELD }, ..Default::default() }; + for s in 0..2 { + m.mv[0][s] = st.pmv[0][s]; + m.select[0][s] = last.select[0][s]; + } + m + } + _ => return Err(SliceError("skipped macroblock in an intra picture")), + }; + let (mbx, mby) = (addr % p.mb_width, addr / p.mb_width); + predict_mb(p, &m, mbx, mby, buf); + t.put(mbx, mby, buf, p.cmb()); + if p.ptype == PictureType::B { + st.last = Motion { flags: m.flags, ..st.last }; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn macroblock(p: &PicParams, st: &mut SliceState, b: &mut Bits, addr: usize, buf: &mut MbBuf, blk: &mut [i32; 64], t: &mut Target) -> R<()> { + let tb = tables(); + let ti = match p.ptype { + PictureType::I => 0, + PictureType::P => 1, + PictureType::B => 2, + PictureType::D => 3, + }; + let flags = tb.mbtype[ti].decode(b).ok_or(SliceError("bad macroblock_type"))?; + let intra = flags & MB_INTRA != 0; + let frame_pic = p.frame_picture(); + let mut m = Motion { flags, ..Default::default() }; + if flags & (MB_FWD | MB_BWD) != 0 { + m.mtype = if frame_pic && (p.pce.frame_pred_frame_dct || !p.mpeg2) { MT_FRAME } else { b.read(2) as u8 }; + if m.mtype == 0 { + return Err(SliceError("reserved motion type")); + } + } else if intra && p.pce.concealment_motion_vectors { + m.mtype = if frame_pic { MT_FRAME } else { MT_FIELD }; + } + let dct_type = p.mpeg2 && frame_pic && !p.pce.frame_pred_frame_dct && (intra || flags & MB_PATTERN != 0) && b.bit(); + if flags & MB_QUANT != 0 { + st.qscale = qscale(p, b.read(5) as usize); + } + // motion vectors + let (count, field_fmt, dual) = match (frame_pic, m.mtype) { + (true, MT_FIELD) => (2, true, false), + (true, MT_DUAL) | (false, MT_DUAL) => (1, true, true), + (true, _) => (1, false, false), + (false, MT_16X8) => (2, true, false), + (false, _) => (1, true, false), + }; + let concealment = intra && p.pce.concealment_motion_vectors; + for s in 0..2 { + let present = if s == 0 { flags & MB_FWD != 0 || concealment } else { flags & MB_BWD != 0 }; + if !present { + continue; + } + for r in 0..count { + if count == 2 || (field_fmt && !dual) { + m.select[r][s] = b.read(1) as usize; + } + for c in 0..2 { + let fc = p.pce.f_code[s][c] as i32; + let code = tb.motion.decode(b).ok_or(SliceError("bad motion_code"))? as i32; + let r_size = (fc - 1).max(0); + let residual = if fc != 1 && code != 0 { b.read(r_size as u32) as i32 } else { 0 }; + if dual { + m.dmv[c] = tb.dmv.decode(b).ok_or(SliceError("bad dmvector"))? as i32; + } + let f = 1 << r_size; + let delta = if f == 1 || code == 0 { code } else { (((code.abs() - 1) * f) + residual + 1) * code.signum() }; + let halve = p.mpeg2 && field_fmt && c == 1 && frame_pic; + let pred = if halve { st.pmv[r][s][c] >> 1 } else { st.pmv[r][s][c] }; + let (low, high, range) = (-16 * f, 16 * f - 1, 32 * f); + let mut v = pred + delta; + if v < low { + v += range; + } else if v > high { + v -= range; + } + st.pmv[r][s][c] = if halve { v * 2 } else { v }; + m.mv[r][s][c] = v; + } + } + if count == 1 { + st.pmv[1][s] = st.pmv[0][s]; + } + if !p.mpeg2 && p.full_pel[s] { + for c in 0..2 { + m.mv[0][s][c] *= 2; + } + } + } + if concealment { + b.skip(1); // marker_bit + } + // coded_block_pattern + let nblocks = 4 + p.chroma_blocks(); + let cbp: u32 = if flags & MB_PATTERN != 0 { + let v = tb.cbp.decode(b).ok_or(SliceError("bad coded_block_pattern"))? as u32; + if nblocks == 8 { (v << 2) | b.read(2) } else { v } + } else if intra { + (1 << nblocks) - 1 + } else { + 0 + }; + let (mbx, mby) = (addr % p.mb_width, addr / p.mb_width); + if intra { + if !concealment { + st.pmv = [[[0; 2]; 2]; 2]; + } + } else { + st.dc_pred = [dc_reset(p); 3]; + if p.ptype == PictureType::P && flags & MB_FWD == 0 { + // "No MC": zero vector from the same position (same-parity field in field pictures) + st.pmv = [[[0; 2]; 2]; 2]; + m.flags |= MB_FWD; + m.mtype = if frame_pic { MT_FRAME } else { MT_FIELD }; + m.select[0][0] = p.parity; + } + predict_mb(p, &m, mbx, mby, buf); + } + // blocks + let (cbw, cbh) = p.cmb(); + for i in 0..nblocks { + if cbp & (1 << (nblocks - 1 - i)) == 0 { + continue; + } + let cc = if i < 4 { 0 } else { 1 + (i & 1) }; + let dc_only = block(p, st, b, cc, intra, blk)?; + idct(blk, dc_only); + // placement + let (dst, stride, x0, y0, step) = if i < 4 { + let x0 = (i & 1) * 8; + let (y0, step) = if dct_type { (i >> 1, 2) } else { ((i >> 1) * 8, 1) }; + (&mut buf.y[..], 16, x0, y0, step) + } else { + let k = (i - 4) >> 1; // 0 for 4:2:0; 0/1 (top/bottom) for 4:2:2 + let (y0, step) = if cbh == 16 && dct_type { (k, 2) } else { (k * 8, 1) }; + (&mut buf.c[cc - 1][..], cbw, 0, y0, step) + }; + for r in 0..8 { + let at = (y0 + r * step) * stride + x0; + let row = &mut dst[at..at + 8]; + let src = &blk[r * 8..r * 8 + 8]; + if intra { + for (o, &v) in row.iter_mut().zip(src) { + *o = v.clamp(0, 255) as u8; + } + } else { + for (o, &v) in row.iter_mut().zip(src) { + *o = (*o as i32 + v).clamp(0, 255) as u8; + } + } + } + } + if p.ptype == PictureType::D { + b.skip(1); // end_of_macroblock + } + t.put(mbx, mby, buf, (cbw, cbh)); + st.last = m; + Ok(()) +} + +/// Decode and dequantise one block into `blk` (raster order). Returns whether only the DC +/// coefficient is non-zero. +fn block(p: &PicParams, st: &mut SliceState, b: &mut Bits, cc: usize, intra: bool, blk: &mut [i32; 64]) -> R { + let tb = tables(); + blk.fill(0); + let scan = &SCAN[p.pce.alternate_scan as usize]; + let w = &p.matrices[(cc > 0 && p.shy == 0) as usize * 2 + (!intra) as usize]; + let qs = st.qscale; + let mut sum: i32 = 0; + let mut i: usize; + let mut ac = false; + if intra { + let size = if cc == 0 { tb.dc_luma.decode(b) } else { tb.dc_chroma.decode(b) }.ok_or(SliceError("bad dct_dc_size"))? as u32; + let diff = if size == 0 { + 0 + } else { + let v = b.read(size) as i32; + if v & (1 << (size - 1)) == 0 { v - (1 << size) + 1 } else { v } + }; + st.dc_pred[cc] += diff; + let dc = if p.mpeg2 { st.dc_pred[cc] * (8 >> p.pce.intra_dc_precision) } else { st.dc_pred[cc] * 8 }; + blk[0] = dc; + sum = dc; + if p.ptype == PictureType::D { + return Ok(true); + } + i = 0; + } else { + i = usize::MAX; // the first coefficient lands at run + // "1s": run 0, level ±1 as the first coefficient of a non-intra block + if b.peek(1) == 1 { + b.skip(1); + let level = if b.bit() { -1 } else { 1 }; + i = 0; + let v = dequant(p, level, qs, w[scan[0] as usize] as i32, false); + blk[scan[0] as usize] = v; + sum += v; + ac = scan[0] != 0; + } + } + let table = if intra && p.pce.intra_vlc_format { &tb.dct_one } else { &tb.dct_zero }; + loop { + let code = table.decode(b).ok_or(SliceError("bad DCT coefficient code"))?; + if code == DCT_EOB { + break; + } + let (run, level) = if code == DCT_ESCAPE { + let run = b.read(6) as usize; + let level = if p.mpeg2 { + b.read_signed(12) + } else { + match b.read(8) as i32 { + 0 => b.read(8) as i32, + 128 => b.read(8) as i32 - 256, + v => (v << 24) >> 24, + } + }; + if level == 0 { + return Err(SliceError("zero escape level")); + } + (run, level) + } else { + let run = (code & 63) as usize; + let level = (code >> 6) as i32; + (run, if b.bit() { -level } else { level }) + }; + i = i.wrapping_add(run + 1); + if i > 63 { + return Err(SliceError("coefficient index past 63")); + } + let pos = scan[i] as usize; + let v = dequant(p, level, qs, w[pos] as i32, intra); + blk[pos] = v; + sum += v; + ac = true; + if b.overrun() { + return Err(SliceError("truncated block")); + } + } + if p.mpeg2 && sum & 1 == 0 { + // mismatch control (§7.4.4) + blk[63] ^= 1; + ac = true; + } + Ok(!ac && blk[1..].iter().all(|&v| v == 0)) +} + +#[inline(always)] +fn dequant(p: &PicParams, level: i32, qs: i32, w: i32, intra: bool) -> i32 { + if p.mpeg2 { + let k = if intra { 0 } else { level.signum() }; + ((2 * level + k) * w * qs / 32).clamp(-2048, 2047) + } else { + let mut v = if intra { 2 * level * qs * w / 16 } else { (2 * level + level.signum()) * qs * w / 16 }; + if v & 1 == 0 { + v -= v.signum(); + } + v.clamp(-2048, 2047) + } +} + +/// The reference field (or frame) for direction `s` and field `sel` (field prediction). +fn reference<'a>(p: &PicParams<'a>, s: usize, sel: usize) -> Option<&'a Frame> { + if s == 1 { + return p.bwd.or(p.fwd); + } + if p.ptype == PictureType::P && !p.frame_picture() && sel != p.parity && p.first_field.is_some() { + return p.first_field; + } + p.fwd.or(p.bwd) +} + +/// Form the prediction of a non-intra macroblock in `buf` (§7.6). +fn predict_mb(p: &PicParams, m: &Motion, mbx: usize, mby: usize, buf: &mut MbBuf) { + let (cbw, cbh) = p.cmb(); + let frame_pic = p.frame_picture(); + let mut avg = false; + for s in 0..2 { + let dir = if s == 0 { MB_FWD } else { MB_BWD }; + if m.flags & dir == 0 { + continue; + } + if frame_pic { + match m.mtype { + MT_FIELD => { + for r in 0..2 { + let Some(f) = reference(p, s, m.select[r][s]) else { return gray(buf) }; + pred_block(p, buf, f, Some(m.select[r][s]), m.mv[r][s], mbx, mby, 8, r, 2, (cbw, cbh), avg); + } + } + MT_DUAL => { + let Some(f) = reference(p, s, 0) else { return gray(buf) }; + for par in 0..2 { + let opp = 1 - par; + let (mm, e) = if par == 0 { (if p.pce.top_field_first { 1 } else { 3 }, -1) } else { (if p.pce.top_field_first { 3 } else { 1 }, 1) }; + let d = dual_vector(m.mv[0][s], m.dmv, mm, e); + pred_block(p, buf, f, Some(par), m.mv[0][s], mbx, mby, 8, par, 2, (cbw, cbh), avg); + pred_block(p, buf, f, Some(opp), d, mbx, mby, 8, par, 2, (cbw, cbh), true); + } + } + _ => { + let Some(f) = reference(p, s, 0) else { return gray(buf) }; + pred_block(p, buf, f, None, m.mv[0][s], mbx, mby, 16, 0, 1, (cbw, cbh), avg); + } + } + } else { + match m.mtype { + MT_16X8 => { + for r in 0..2 { + let Some(f) = reference(p, s, m.select[r][s]) else { return gray(buf) }; + pred_half(p, buf, f, m.select[r][s], m.mv[r][s], mbx, mby, r, (cbw, cbh), avg); + } + } + MT_DUAL => { + let same = p.parity; + let opp = 1 - same; + let Some(fs) = reference(p, s, same) else { return gray(buf) }; + let Some(fo) = reference(p, s, opp) else { return gray(buf) }; + let e = if same == 1 { 1 } else { -1 }; + let d = dual_vector(m.mv[0][s], m.dmv, 1, e); + pred_block(p, buf, fs, Some(same), m.mv[0][s], mbx, mby, 16, 0, 1, (cbw, cbh), avg); + pred_block(p, buf, fo, Some(opp), d, mbx, mby, 16, 0, 1, (cbw, cbh), true); + } + _ => { + let Some(f) = reference(p, s, m.select[0][s]) else { return gray(buf) }; + pred_block(p, buf, f, Some(m.select[0][s]), m.mv[0][s], mbx, mby, 16, 0, 1, (cbw, cbh), avg); + } + } + } + avg = true; + } +} + +fn gray(buf: &mut MbBuf) { + buf.y.fill(128); + buf.c[0].fill(128); + buf.c[1].fill(128); +} + +/// Dual-prime opposite-parity vector (§7.6.3.6). +fn dual_vector(v: [i32; 2], dmv: [i32; 2], m: i32, e: i32) -> [i32; 2] { + let scale = |x: i32| (x * m + (x > 0) as i32) >> 1; + [scale(v[0]) + dmv[0], scale(v[1]) + e + dmv[1]] +} + +/// Predict a 16-wide luma block of `lh` lines (plus chroma) from `f` (one field when `field`), +/// into `buf` lines `first + k·step`. `lh` is 16 (frame or field-picture prediction) or 8 (one +/// field of a frame macroblock). +#[allow(clippy::too_many_arguments)] +fn pred_block( + p: &PicParams, + buf: &mut MbBuf, + f: &Frame, + field: Option, + mv: [i32; 2], + mbx: usize, + mby: usize, + lh: usize, + first: usize, + step: usize, + (cbw, cbh): (usize, usize), + avg: bool, +) { + // luma: base position in the reference plane's own (frame or field) lines + let by = if lh == 8 { mby * 8 } else { mby * 16 }; + let (x, y) = ((mbx * 16) as i32 + (mv[0] >> 1), by as i32 + (mv[1] >> 1)); + predict(&mut buf.y, first * 16, 16 * step, f.plane(0, field), x, y, mv[0] & 1 != 0, mv[1] & 1 != 0, 16, lh, avg); + let cmv = [if p.shx == 1 { mv[0] / 2 } else { mv[0] }, if p.shy == 1 { mv[1] / 2 } else { mv[1] }]; + let clh = if lh == 8 { cbh / 2 } else { cbh }; + let cby = mby * clh; + let (cx, cy) = ((mbx * cbw) as i32 + (cmv[0] >> 1), cby as i32 + (cmv[1] >> 1)); + for c in 0..2 { + predict(&mut buf.c[c], first * cbw, cbw * step, f.plane(1 + c, field), cx, cy, cmv[0] & 1 != 0, cmv[1] & 1 != 0, cbw, clh, avg); + } +} + +/// 16x8 motion compensation in a field picture: half `r` (0 upper, 1 lower) of the macroblock. +#[allow(clippy::too_many_arguments)] +fn pred_half(p: &PicParams, buf: &mut MbBuf, f: &Frame, field: usize, mv: [i32; 2], mbx: usize, mby: usize, r: usize, (cbw, cbh): (usize, usize), avg: bool) { + let (x, y) = ((mbx * 16) as i32 + (mv[0] >> 1), (mby * 16 + r * 8) as i32 + (mv[1] >> 1)); + predict(&mut buf.y, r * 8 * 16, 16, f.plane(0, Some(field)), x, y, mv[0] & 1 != 0, mv[1] & 1 != 0, 16, 8, avg); + let cmv = [if p.shx == 1 { mv[0] / 2 } else { mv[0] }, if p.shy == 1 { mv[1] / 2 } else { mv[1] }]; + let ch = cbh / 2; + let (cx, cy) = ((mbx * cbw) as i32 + (cmv[0] >> 1), (mby * cbh + r * ch) as i32 + (cmv[1] >> 1)); + for c in 0..2 { + predict(&mut buf.c[c], r * ch * cbw, cbw, f.plane(1 + c, Some(field)), cx, cy, cmv[0] & 1 != 0, cmv[1] & 1 != 0, cbw, ch, avg); + } +} + +/// The macroblock row a slice starts in (slice_vertical_position and its extension). +pub(crate) fn slice_row(vertical_size_extension: bool, code: u8, payload: &[u8]) -> usize { + let mut row = code as usize - 1; + if vertical_size_extension && !payload.is_empty() { + row += ((payload[0] >> 5) as usize) << 7; + } + row +} diff --git a/crates/mpeg2v/src/tests.rs b/crates/mpeg2v/src/tests.rs new file mode 100644 index 000000000..af6736b48 --- /dev/null +++ b/crates/mpeg2v/src/tests.rs @@ -0,0 +1,171 @@ +//! Unit tests over hand-built streams (headers, robustness). Decoding accuracy against ffmpeg is +//! in `tests/oracle.rs`. + +use super::*; + +/// A minimal bit writer for building streams. +struct W { + out: Vec, + acc: u64, + n: u32, +} + +impl W { + fn new() -> W { + W { out: Vec::new(), acc: 0, n: 0 } + } + fn put(&mut self, v: u32, bits: u32) { + for i in (0..bits).rev() { + self.acc = (self.acc << 1) | ((v >> i) & 1) as u64; + self.n += 1; + if self.n == 8 { + self.out.push(self.acc as u8); + self.acc = 0; + self.n = 0; + } + } + } + fn code(&mut self, s: &str) { + for c in s.chars().filter(|c| *c != ' ') { + self.put((c == '1') as u32, 1); + } + } + fn align(&mut self) { + while self.n != 0 { + self.put(0, 1); + } + } + fn start(&mut self, code: u8) { + self.align(); + self.out.extend_from_slice(&[0, 0, 1, code]); + } +} + +/// An MPEG-2 4:2:0 progressive 32×16 I picture whose macroblocks all have DC `dc` (luma) and +/// no AC: two macroblocks, one slice. +fn intra_stream(dc_luma: i32) -> Vec { + let mut w = W::new(); + w.start(0xB3); + w.put(32, 12); + w.put(16, 12); + w.put(1, 4); // square samples + w.put(3, 4); // 25 fps + w.put(1000, 18); + w.put(1, 1); + w.put(10, 10); + w.put(0, 1); + w.put(0, 1); + w.put(0, 1); + w.start(0xB5); + w.put(1, 4); + w.put(0x48, 8); // Main@Main + w.put(1, 1); // progressive + w.put(1, 2); // 4:2:0 + w.put(0, 2); + w.put(0, 2); + w.put(0, 12); + w.put(1, 1); + w.put(0, 8); + w.put(0, 1); + w.put(0, 2); + w.put(0, 5); + w.start(0x00); + w.put(0, 10); + w.put(1, 3); // I + w.put(0xFFFF, 16); + w.put(0, 1); // extra_bit_picture + w.start(0xB5); + w.put(8, 4); + for _ in 0..4 { + w.put(15, 4); + } + w.put(0, 2); // 8-bit DC + w.put(3, 2); // frame picture + w.put(0, 1); + w.put(1, 1); // frame_pred_frame_dct + w.put(0, 1); + w.put(0, 1); + w.put(0, 1); + w.put(0, 1); + w.put(0, 1); + w.put(1, 1); + w.put(1, 1); // progressive_frame + w.put(0, 1); + w.start(0x01); + w.put(8, 5); // quantiser_scale_code + w.put(0, 1); // extra_bit_slice + for mb in 0..2 { + w.code("1"); // address increment 1 + w.code("1"); // macroblock_type: intra + for b in 0..6 { + // DC differential: the first luma block of the slice carries dc - 128 + let diff = if b == 0 && mb == 0 { dc_luma - 128 } else { 0 }; + if b < 4 { + if diff == 0 { + w.code("100"); + } else { + // size 6 covers |diff| < 64 + w.code("1111 0"); + let v = if diff > 0 { diff as u32 } else { (diff + 63) as u32 }; + w.put(v, 6); + } + } else { + w.code("00"); + } + w.code("10"); // EOB (table zero) + } + } + w.start(0xB7); + w.out +} + +#[test] +fn decodes_a_hand_built_intra_picture() { + let s = intra_stream(160); + let info = probe(&s).unwrap(); + assert_eq!((info.width, info.height, info.mpeg2, info.frame_rate), (32, 16, true, Some((25, 1)))); + assert_eq!(info.codec_name(), "MPEG-2 Video (Main@Main)"); + let mut d = Decoder::new(); + let mut pics = d.decode(&s, 7).unwrap(); + pics.extend(d.flush()); + assert_eq!(pics.len(), 1); + let p = &pics[0]; + assert_eq!(d.errors(), 0); + assert_eq!((p.width, p.height, p.pts, p.picture_type), (32, 16, 7, PictureType::I)); + // DC 160 everywhere (the mismatch-control toggle of F[7][7] is far below rounding) + assert!(p.y.iter().all(|&v| v == 160), "{:?}", &p.y[..8]); + assert!(p.cb.iter().chain(&p.cr).all(|&v| v == 128)); + assert_eq!(p.field_order(), None); +} + +#[test] +fn truncation_and_mutation_never_panic() { + let s = intra_stream(100); + for cut in 0..s.len() { + let mut d = Decoder::new(); + let _ = d.decode(&s[..cut], 0); + let _ = d.flush(); + } + let mut x = 0x1234_5678u32; + for _ in 0..3000 { + let mut m = s.clone(); + for _ in 0..3 { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + let i = x as usize % m.len(); + m[i] ^= (x >> 8) as u8; + } + let mut d = Decoder::new(); + let _ = d.decode(&m, 0); + let _ = d.flush(); + } +} + +#[test] +fn access_unit_scan() { + let s = intra_stream(128); + let au = scan_access_unit(&s); + assert!(au.sequence_header && au.is_intra() && !au.is_disposable()); + assert_eq!(au.pictures[0].1.unwrap().picture_structure, 3); +} diff --git a/crates/mpeg2v/src/vlc.rs b/crates/mpeg2v/src/vlc.rs new file mode 100644 index 000000000..00f9552b4 --- /dev/null +++ b/crates/mpeg2v/src/vlc.rs @@ -0,0 +1,627 @@ +//! Variable-length code tables of ITU-T H.262 Annex B (B.1-B.4, B.9-B.15), which include those +//! of ISO/IEC 11172-2 Annex B. Codes are written as in the standard (spaces ignored) and turned +//! into two-level lookup tables once. + +use std::sync::OnceLock; + +use crate::bits::Bits; + +#[derive(Clone, Copy, Default)] +struct Entry { + /// Decoded value, or the subtable offset when `sub > 0`. + val: i16, + /// Code length (0: invalid code). + len: u8, + /// Index bits of the subtable this entry points to. + sub: u8, +} + +/// A two-level lookup table. +pub(crate) struct Vlc { + root: u32, + table: Vec, +} + +fn parse_code(s: &str) -> (u32, u32) { + let mut v = 0u32; + let mut n = 0u32; + for c in s.chars() { + match c { + '0' | '1' => { + v = (v << 1) | (c as u32 - '0' as u32); + n += 1; + } + ' ' => {} + _ => panic!("bad code {s}"), + } + } + (v, n) +} + +impl Vlc { + pub fn new(root: u32, codes: &[(&str, i16)]) -> Vlc { + let codes: Vec<(u32, u32, i16)> = codes + .iter() + .map(|(s, v)| { + let (c, n) = parse_code(s); + (c, n, *v) + }) + .collect(); + let mut table = vec![Entry::default(); 1 << root]; + // subtable sizes per root prefix + let mut sub_bits = vec![0u32; 1 << root]; + for &(c, n, _) in &codes { + if n > root { + let p = (c >> (n - root)) as usize; + sub_bits[p] = sub_bits[p].max(n - root); + } + } + for (p, &b) in sub_bits.iter().enumerate() { + if b > 0 { + let off = table.len(); + table.extend(std::iter::repeat_n(Entry::default(), 1 << b)); + table[p] = Entry { val: off as i16, len: 0, sub: b as u8 }; + } + } + for &(c, n, v) in &codes { + if n <= root { + let shift = root - n; + for k in 0..(1u32 << shift) { + let e = &mut table[((c << shift) | k) as usize]; + assert!(e.len == 0 && e.sub == 0, "VLC prefix clash"); + *e = Entry { val: v, len: n as u8, sub: 0 }; + } + } else { + let p = (c >> (n - root)) as usize; + let Entry { val: off, sub: b, .. } = table[p]; + let b = b as u32; + let rest = c & ((1 << (n - root)) - 1); + let shift = b - (n - root); + for k in 0..(1u32 << shift) { + let e = &mut table[off as usize + ((rest << shift) | k) as usize]; + assert!(e.len == 0, "VLC prefix clash"); + *e = Entry { val: v, len: (n - root) as u8, sub: 0 }; + } + } + } + Vlc { root, table } + } + + /// Decode one code; `None` for an invalid code. + #[inline(always)] + pub fn decode(&self, b: &mut Bits) -> Option { + let w = b.peek32(); + let e = self.table[(w >> (32 - self.root)) as usize]; + if e.sub == 0 { + if e.len == 0 { + return None; + } + b.skip(e.len as u32); + return Some(e.val); + } + let idx = ((w << self.root) >> (32 - e.sub as u32)) as usize; + let e2 = self.table[e.val as usize + idx]; + if e2.len == 0 { + return None; + } + b.skip(self.root + e2.len as u32); + Some(e2.val) + } +} + +/// macroblock_address_increment (B.1): 1..=33, [`MBA_ESCAPE`], [`MBA_STUFFING`]. +pub(crate) const MBA_ESCAPE: i16 = 34; +pub(crate) const MBA_STUFFING: i16 = 35; +const MBA: &[(&str, i16)] = &[ + ("1", 1), + ("011", 2), + ("010", 3), + ("0011", 4), + ("0010", 5), + ("0001 1", 6), + ("0001 0", 7), + ("0000 111", 8), + ("0000 110", 9), + ("0000 1011", 10), + ("0000 1010", 11), + ("0000 1001", 12), + ("0000 1000", 13), + ("0000 0111", 14), + ("0000 0110", 15), + ("0000 0101 11", 16), + ("0000 0101 10", 17), + ("0000 0101 01", 18), + ("0000 0101 00", 19), + ("0000 0100 11", 20), + ("0000 0100 10", 21), + ("0000 0100 011", 22), + ("0000 0100 010", 23), + ("0000 0100 001", 24), + ("0000 0100 000", 25), + ("0000 0011 111", 26), + ("0000 0011 110", 27), + ("0000 0011 101", 28), + ("0000 0011 100", 29), + ("0000 0011 011", 30), + ("0000 0011 010", 31), + ("0000 0011 001", 32), + ("0000 0011 000", 33), + ("0000 0001 000", MBA_ESCAPE), + ("0000 0001 111", MBA_STUFFING), +]; + +/// macroblock_type flags. +pub(crate) const MB_QUANT: i16 = 1; +pub(crate) const MB_FWD: i16 = 2; +pub(crate) const MB_BWD: i16 = 4; +pub(crate) const MB_PATTERN: i16 = 8; +pub(crate) const MB_INTRA: i16 = 16; + +/// B.2: I pictures. +const MBTYPE_I: &[(&str, i16)] = &[("1", MB_INTRA), ("01", MB_INTRA | MB_QUANT)]; +/// B.3: P pictures. +const MBTYPE_P: &[(&str, i16)] = &[ + ("1", MB_FWD | MB_PATTERN), + ("01", MB_PATTERN), + ("001", MB_FWD), + ("0001 1", MB_INTRA), + ("0001 0", MB_QUANT | MB_FWD | MB_PATTERN), + ("0000 1", MB_QUANT | MB_PATTERN), + ("0000 01", MB_QUANT | MB_INTRA), +]; +/// B.4: B pictures. +const MBTYPE_B: &[(&str, i16)] = &[ + ("10", MB_FWD | MB_BWD), + ("11", MB_FWD | MB_BWD | MB_PATTERN), + ("010", MB_BWD), + ("011", MB_BWD | MB_PATTERN), + ("0010", MB_FWD), + ("0011", MB_FWD | MB_PATTERN), + ("0001 1", MB_INTRA), + ("0001 0", MB_QUANT | MB_FWD | MB_BWD | MB_PATTERN), + ("0000 11", MB_QUANT | MB_FWD | MB_PATTERN), + ("0000 10", MB_QUANT | MB_BWD | MB_PATTERN), + ("0000 01", MB_QUANT | MB_INTRA), +]; +/// ISO/IEC 11172-2 D pictures (DC intra-coded). +const MBTYPE_D: &[(&str, i16)] = &[("1", MB_INTRA)]; + +/// B.9: coded_block_pattern (0 only in ISO/IEC 13818-2). +const CBP: &[(&str, i16)] = &[ + ("111", 60), + ("1101", 4), + ("1100", 8), + ("1011", 16), + ("1010", 32), + ("1001 1", 12), + ("1001 0", 48), + ("1000 1", 20), + ("1000 0", 40), + ("0111 1", 28), + ("0111 0", 44), + ("0110 1", 52), + ("0110 0", 56), + ("0101 1", 1), + ("0101 0", 61), + ("0100 1", 2), + ("0100 0", 62), + ("0011 11", 24), + ("0011 10", 36), + ("0011 01", 3), + ("0011 00", 63), + ("0010 111", 5), + ("0010 110", 9), + ("0010 101", 17), + ("0010 100", 33), + ("0010 011", 6), + ("0010 010", 10), + ("0010 001", 18), + ("0010 000", 34), + ("0001 1111", 7), + ("0001 1110", 11), + ("0001 1101", 19), + ("0001 1100", 35), + ("0001 1011", 13), + ("0001 1010", 49), + ("0001 1001", 21), + ("0001 1000", 41), + ("0001 0111", 14), + ("0001 0110", 50), + ("0001 0101", 22), + ("0001 0100", 42), + ("0001 0011", 15), + ("0001 0010", 51), + ("0001 0001", 23), + ("0001 0000", 43), + ("0000 1111", 25), + ("0000 1110", 37), + ("0000 1101", 26), + ("0000 1100", 38), + ("0000 1011", 29), + ("0000 1010", 45), + ("0000 1001", 53), + ("0000 1000", 57), + ("0000 0111", 30), + ("0000 0110", 46), + ("0000 0101", 54), + ("0000 0100", 58), + ("0000 0011 1", 31), + ("0000 0011 0", 47), + ("0000 0010 1", 55), + ("0000 0010 0", 59), + ("0000 0001 1", 27), + ("0000 0001 0", 39), + ("0000 0000 1", 0), +]; + +/// B.10: motion_code (-16..=16). +const MOTION: &[(&str, i16)] = &[ + ("0000 0011 001", -16), + ("0000 0011 011", -15), + ("0000 0011 101", -14), + ("0000 0011 111", -13), + ("0000 0100 001", -12), + ("0000 0100 011", -11), + ("0000 0100 11", -10), + ("0000 0101 01", -9), + ("0000 0101 11", -8), + ("0000 0111", -7), + ("0000 1001", -6), + ("0000 1011", -5), + ("0000 111", -4), + ("0001 1", -3), + ("0011", -2), + ("011", -1), + ("1", 0), + ("010", 1), + ("0010", 2), + ("0001 0", 3), + ("0000 110", 4), + ("0000 1010", 5), + ("0000 1000", 6), + ("0000 0110", 7), + ("0000 0101 10", 8), + ("0000 0101 00", 9), + ("0000 0100 10", 10), + ("0000 0100 010", 11), + ("0000 0100 000", 12), + ("0000 0011 110", 13), + ("0000 0011 100", 14), + ("0000 0011 010", 15), + ("0000 0011 000", 16), +]; + +/// B.11: dmvector. +const DMV: &[(&str, i16)] = &[("11", -1), ("0", 0), ("10", 1)]; + +/// B.12: dct_dc_size_luminance. +const DC_LUMA: &[(&str, i16)] = &[ + ("100", 0), + ("00", 1), + ("01", 2), + ("101", 3), + ("110", 4), + ("1110", 5), + ("1111 0", 6), + ("1111 10", 7), + ("1111 110", 8), + ("1111 1110", 9), + ("1111 1111 0", 10), + ("1111 1111 1", 11), +]; + +/// B.13: dct_dc_size_chrominance. +const DC_CHROMA: &[(&str, i16)] = &[ + ("00", 0), + ("01", 1), + ("10", 2), + ("110", 3), + ("1110", 4), + ("1111 0", 5), + ("1111 10", 6), + ("1111 110", 7), + ("1111 1110", 8), + ("1111 1111 0", 9), + ("1111 1111 10", 10), + ("1111 1111 11", 11), +]; + +/// DCT coefficient table values: `run | level << 6`, or these. +pub(crate) const DCT_EOB: i16 = -1; +pub(crate) const DCT_ESCAPE: i16 = -2; + +const fn rl(run: i16, level: i16) -> i16 { + run | (level << 6) +} + +/// Table zero's codes for levels 12-15 at run 0 (table one has shorter ones). +const DCT_ZERO_LONG: &[(&str, i16)] = + &[("0000 0000 1101 0", rl(0, 12)), ("0000 0000 1100 1", rl(0, 13)), ("0000 0000 1100 0", rl(0, 14)), ("0000 0000 1011 1", rl(0, 15))]; + +/// Codes from "0000 0000 1" on, common to tables zero and one (sign bit excluded). +const DCT_LONG: &[(&str, i16)] = &[ + ("0000 0000 1011 0", rl(1, 6)), + ("0000 0000 1010 1", rl(1, 7)), + ("0000 0000 1010 0", rl(2, 5)), + ("0000 0000 1001 1", rl(3, 4)), + ("0000 0000 1001 0", rl(5, 3)), + ("0000 0000 1000 1", rl(9, 2)), + ("0000 0000 1000 0", rl(10, 2)), + ("0000 0000 1111 1", rl(22, 1)), + ("0000 0000 1111 0", rl(23, 1)), + ("0000 0000 1110 1", rl(24, 1)), + ("0000 0000 1110 0", rl(25, 1)), + ("0000 0000 1101 1", rl(26, 1)), + ("0000 0000 0111 11", rl(0, 16)), + ("0000 0000 0111 10", rl(0, 17)), + ("0000 0000 0111 01", rl(0, 18)), + ("0000 0000 0111 00", rl(0, 19)), + ("0000 0000 0110 11", rl(0, 20)), + ("0000 0000 0110 10", rl(0, 21)), + ("0000 0000 0110 01", rl(0, 22)), + ("0000 0000 0110 00", rl(0, 23)), + ("0000 0000 0101 11", rl(0, 24)), + ("0000 0000 0101 10", rl(0, 25)), + ("0000 0000 0101 01", rl(0, 26)), + ("0000 0000 0101 00", rl(0, 27)), + ("0000 0000 0100 11", rl(0, 28)), + ("0000 0000 0100 10", rl(0, 29)), + ("0000 0000 0100 01", rl(0, 30)), + ("0000 0000 0100 00", rl(0, 31)), + ("0000 0000 0011 000", rl(0, 32)), + ("0000 0000 0010 111", rl(0, 33)), + ("0000 0000 0010 110", rl(0, 34)), + ("0000 0000 0010 101", rl(0, 35)), + ("0000 0000 0010 100", rl(0, 36)), + ("0000 0000 0010 011", rl(0, 37)), + ("0000 0000 0010 010", rl(0, 38)), + ("0000 0000 0010 001", rl(0, 39)), + ("0000 0000 0010 000", rl(0, 40)), + ("0000 0000 0011 111", rl(1, 8)), + ("0000 0000 0011 110", rl(1, 9)), + ("0000 0000 0011 101", rl(1, 10)), + ("0000 0000 0011 100", rl(1, 11)), + ("0000 0000 0011 011", rl(1, 12)), + ("0000 0000 0011 010", rl(1, 13)), + ("0000 0000 0011 001", rl(1, 14)), + ("0000 0000 0001 0011", rl(1, 15)), + ("0000 0000 0001 0010", rl(1, 16)), + ("0000 0000 0001 0001", rl(1, 17)), + ("0000 0000 0001 0000", rl(1, 18)), + ("0000 0000 0001 0100", rl(6, 3)), + ("0000 0000 0001 1010", rl(11, 2)), + ("0000 0000 0001 1001", rl(12, 2)), + ("0000 0000 0001 1000", rl(13, 2)), + ("0000 0000 0001 0111", rl(14, 2)), + ("0000 0000 0001 0110", rl(15, 2)), + ("0000 0000 0001 0101", rl(16, 2)), + ("0000 0000 0001 1111", rl(27, 1)), + ("0000 0000 0001 1110", rl(28, 1)), + ("0000 0000 0001 1101", rl(29, 1)), + ("0000 0000 0001 1100", rl(30, 1)), + ("0000 0000 0001 1011", rl(31, 1)), +]; + +/// "0000 0001 xxxx" codes of both tables (table one replaces the six marked `*` with shorter ones). +const DCT_MID_SHARED: &[(&str, i16)] = &[ + ("0000 0001 1100", rl(3, 3)), + ("0000 0001 0010", rl(4, 3)), + ("0000 0001 1110", rl(6, 2)), + ("0000 0001 0101", rl(7, 2)), + ("0000 0001 0001", rl(8, 2)), + ("0000 0001 1111", rl(17, 1)), + ("0000 0001 1010", rl(18, 1)), + ("0000 0001 1001", rl(19, 1)), + ("0000 0001 0111", rl(20, 1)), + ("0000 0001 0110", rl(21, 1)), +]; + +/// B.14: DCT coefficients table zero (the "1s" first-coefficient code is handled by the caller). +const DCT_ZERO: &[(&str, i16)] = &[ + ("10", DCT_EOB), + ("11", rl(0, 1)), + ("011", rl(1, 1)), + ("0100", rl(0, 2)), + ("0101", rl(2, 1)), + ("0010 1", rl(0, 3)), + ("0011 1", rl(3, 1)), + ("0011 0", rl(4, 1)), + ("0001 10", rl(1, 2)), + ("0001 11", rl(5, 1)), + ("0001 01", rl(6, 1)), + ("0001 00", rl(7, 1)), + ("0000 110", rl(0, 4)), + ("0000 100", rl(2, 2)), + ("0000 111", rl(8, 1)), + ("0000 101", rl(9, 1)), + ("0000 01", DCT_ESCAPE), + ("0010 0110", rl(0, 5)), + ("0010 0001", rl(0, 6)), + ("0010 0101", rl(1, 3)), + ("0010 0100", rl(3, 2)), + ("0010 0111", rl(10, 1)), + ("0010 0011", rl(11, 1)), + ("0010 0010", rl(12, 1)), + ("0010 0000", rl(13, 1)), + ("0000 0010 10", rl(0, 7)), + ("0000 0011 00", rl(1, 4)), + ("0000 0010 11", rl(2, 3)), + ("0000 0011 11", rl(4, 2)), + ("0000 0010 01", rl(5, 2)), + ("0000 0011 10", rl(14, 1)), + ("0000 0011 01", rl(15, 1)), + ("0000 0010 00", rl(16, 1)), + ("0000 0001 1101", rl(0, 8)), + ("0000 0001 1000", rl(0, 9)), + ("0000 0001 0011", rl(0, 10)), + ("0000 0001 0000", rl(0, 11)), + ("0000 0001 1011", rl(1, 5)), + ("0000 0001 0100", rl(2, 4)), +]; + +/// B.15: DCT coefficients table one (intra blocks with intra_vlc_format = 1). +const DCT_ONE: &[(&str, i16)] = &[ + ("0110", DCT_EOB), + ("10", rl(0, 1)), + ("010", rl(1, 1)), + ("110", rl(0, 2)), + ("0010 1", rl(2, 1)), + ("0111", rl(0, 3)), + ("0011 1", rl(3, 1)), + ("0001 10", rl(4, 1)), + ("0011 0", rl(1, 2)), + ("0001 11", rl(5, 1)), + ("0000 110", rl(6, 1)), + ("0000 100", rl(7, 1)), + ("1110 0", rl(0, 4)), + ("0000 111", rl(2, 2)), + ("0000 101", rl(8, 1)), + ("1111 000", rl(9, 1)), + ("0000 01", DCT_ESCAPE), + ("1110 1", rl(0, 5)), + ("0001 01", rl(0, 6)), + ("1111 001", rl(1, 3)), + ("0010 0110", rl(3, 2)), + ("1111 010", rl(10, 1)), + ("0010 0001", rl(11, 1)), + ("0010 0101", rl(12, 1)), + ("0010 0100", rl(13, 1)), + ("0001 00", rl(0, 7)), + ("0010 0111", rl(1, 4)), + ("1111 1100", rl(2, 3)), + ("1111 1101", rl(4, 2)), + ("0000 0010 0", rl(5, 2)), + ("0000 0010 1", rl(14, 1)), + ("0000 0011 1", rl(15, 1)), + ("0000 0011 01", rl(16, 1)), + ("1111 011", rl(0, 8)), + ("1111 100", rl(0, 9)), + ("0010 0011", rl(0, 10)), + ("0010 0010", rl(0, 11)), + ("0010 0000", rl(1, 5)), + ("0000 0011 00", rl(2, 4)), + ("1111 1010", rl(0, 12)), + ("1111 1011", rl(0, 13)), + ("1111 1110", rl(0, 14)), + ("1111 1111", rl(0, 15)), +]; + +pub(crate) struct Tables { + pub mba: Vlc, + pub mbtype: [Vlc; 4], + pub cbp: Vlc, + pub motion: Vlc, + pub dmv: Vlc, + pub dc_luma: Vlc, + pub dc_chroma: Vlc, + pub dct_zero: Vlc, + pub dct_one: Vlc, +} + +/// A complete DCT coefficient table: its own short codes plus the shared long ones. +fn dct_table(short: &'static [(&'static str, i16)], extra: &'static [(&'static str, i16)]) -> Vec<(&'static str, i16)> { + let mut v = short.to_vec(); + v.extend_from_slice(extra); + v.extend_from_slice(DCT_MID_SHARED); + v.extend_from_slice(DCT_LONG); + v +} + +pub(crate) fn tables() -> &'static Tables { + static T: OnceLock = OnceLock::new(); + T.get_or_init(|| Tables { + mba: Vlc::new(8, MBA), + mbtype: [Vlc::new(2, MBTYPE_I), Vlc::new(6, MBTYPE_P), Vlc::new(6, MBTYPE_B), Vlc::new(1, MBTYPE_D)], + cbp: Vlc::new(9, CBP), + motion: Vlc::new(8, MOTION), + dmv: Vlc::new(2, DMV), + dc_luma: Vlc::new(9, DC_LUMA), + dc_chroma: Vlc::new(10, DC_CHROMA), + dct_zero: Vlc::new(9, &dct_table(DCT_ZERO, DCT_ZERO_LONG)), + dct_one: Vlc::new(9, &dct_table(DCT_ONE, &[])), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Kraft sum of a code list (2^-len summed). + fn kraft(codes: &[(&str, i16)]) -> f64 { + codes.iter().map(|(c, _)| 0.5f64.powi(parse_code(c).1 as i32)).sum() + } + + #[test] + fn tables_build_without_prefix_clashes() { + let _ = tables(); + } + + #[test] + fn kraft_sums_match_the_reserved_codes() { + // B.1: "0000 0000", "0000 0010" and "0000 0001 001/010/011/100/101/110" are unused + let unused = 2.0 * 0.5f64.powi(8) + 6.0 * 0.5f64.powi(11); + assert!((kraft(MBA) + unused - 1.0).abs() < 1e-12); + assert!((kraft(MBTYPE_I) - 0.75).abs() < 1e-12, "01 is the last I code: 00 is forbidden"); + assert!((kraft(MBTYPE_P) + 0.5f64.powi(6) - 1.0).abs() < 1e-12); + assert!((kraft(MBTYPE_B) + 0.5f64.powi(6) - 1.0).abs() < 1e-12); + // B.9: only "0000 0000 0" is forbidden + assert!((kraft(CBP) + 0.5f64.powi(9) - 1.0).abs() < 1e-12); + // B.10: "0000 0000", "0000 0001" and "0000 0010" are unused + assert!((kraft(MOTION) + 3.0 * 0.5f64.powi(8) - 1.0).abs() < 1e-12); + assert!((kraft(DMV) - 1.0).abs() < 1e-12); + assert!((kraft(DC_LUMA) - 1.0).abs() < 1e-12); + assert!((kraft(DC_CHROMA) - 1.0).abs() < 1e-12); + // table zero is complete except "0000 0000 0000 xxxx" + let z = kraft(&dct_table(DCT_ZERO, DCT_ZERO_LONG)); + assert!((z + 0.5f64.powi(12) - 1.0).abs() < 1e-12, "{z}"); + // table one leaves six 12-bit and four 13-bit codes of table zero unused + let o = kraft(&dct_table(DCT_ONE, &[])); + assert!((o + 0.5f64.powi(12) + 6.0 * 0.5f64.powi(12) + 4.0 * 0.5f64.powi(13) - 1.0).abs() < 1e-12, "{o}"); + } + + #[test] + fn every_value_appears_once() { + let mut seen = [false; 64]; + for &(_, v) in CBP { + assert!(!seen[v as usize]); + seen[v as usize] = true; + } + assert!(seen.iter().all(|&s| s)); + let mut m: Vec = MOTION.iter().map(|x| x.1).collect(); + m.sort(); + assert_eq!(m, (-16..=16).collect::>()); + // motion codes are the address increment codes with a trailing sign bit + for &(c, v) in MOTION { + if v == 0 { + continue; + } + let (code, n) = parse_code(c); + let mba = MBA.iter().find(|(m, _)| parse_code(m) == (code, n)).map(|m| m.1).unwrap(); + assert_eq!(mba / 2, v.abs()); + assert_eq!(mba % 2 == 0, v < 0, "{c}"); + } + // run/level pairs are distinct within each table + for t in [dct_table(DCT_ZERO, DCT_ZERO_LONG), dct_table(DCT_ONE, &[])] { + let mut vals: Vec = t.iter().map(|x| x.1).filter(|&v| v >= 0).collect(); + let n = vals.len(); + vals.sort(); + vals.dedup(); + assert_eq!(vals.len(), n); + assert_eq!(n, 111, "111 run/level pairs besides EOB and escape"); + } + } + + #[test] + fn decodes_codes() { + let t = tables(); + // 0000 0011 000 (33) then 1 (1) then 0000 0001 000 (escape) + let d = [0b0000_0011, 0b0001_0000, 0b0001_0000, 0]; + let mut b = Bits::new(&d); + assert_eq!(t.mba.decode(&mut b), Some(33)); + assert_eq!(t.mba.decode(&mut b), Some(1)); + assert_eq!(t.mba.decode(&mut b), Some(MBA_ESCAPE)); + let d = [0b0000_0000, 0b0001_1111, 0]; + let mut b = Bits::new(&d); + assert_eq!(t.dct_zero.decode(&mut b), Some(rl(27, 1))); + } +} diff --git a/crates/mpeg2v/tests/common/mod.rs b/crates/mpeg2v/tests/common/mod.rs new file mode 100644 index 000000000..1b032206c --- /dev/null +++ b/crates/mpeg2v/tests/common/mod.rs @@ -0,0 +1,129 @@ +//! Shared helpers for the MPEG video oracle tests. ffmpeg is used only as an external fixture +//! generator and reference decoder (never linked). +#![allow(dead_code)] + +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use filmcraft_mpeg2v::{ChromaFormat, Decoder, Picture}; + +pub fn fixture_dir() -> PathBuf { + filmcraft_testkit::fixtures_dir("mpeg2v") +} + +/// An elementary-stream fixture: a lavfi source encoded by ffmpeg's MPEG-1/2 video encoder. +pub struct Spec { + pub name: &'static str, + pub lavfi: &'static str, + pub frames: u32, + pub enc: &'static [&'static str], + /// ffmpeg rawvideo pixel format of the decoded frames. + pub pix_fmt: &'static str, +} + +pub fn path(spec: &Spec) -> PathBuf { + fixture_dir().join(format!("{}.{}", spec.name, if spec.enc.contains(&"mpeg1video") { "m1v" } else { "m2v" })) +} + +/// Generate (or reuse) the fixture. +pub fn make(ff: &Path, spec: &Spec) -> Option { + let out = path(spec); + filmcraft_testkit::fixtures::generate(&out, |tmp| { + let frames = spec.frames.to_string(); + let fmt = if spec.enc.contains(&"mpeg1video") { "mpeg1video" } else { "mpeg2video" }; + let st = Command::new(ff) + .args(["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", spec.lavfi, "-frames:v", &frames]) + .args(spec.enc) + .args(["-f", fmt]) + .arg(tmp) + .stdin(Stdio::null()) + .status(); + matches!(st, Ok(s) if s.success()) + }) +} + +/// ffmpeg's decode of `file` as raw frames. +pub fn ffmpeg_frames(ff: &Path, file: &Path, pix_fmt: &str) -> Vec { + let o = Command::new(ff) + .args(["-hide_banner", "-loglevel", "error", "-i"]) + .arg(file) + .args(["-fps_mode", "passthrough", "-f", "rawvideo", "-pix_fmt", pix_fmt, "-"]) + .stdin(Stdio::null()) + .output() + .expect("run ffmpeg"); + assert!(o.status.success(), "ffmpeg: {}", String::from_utf8_lossy(&o.stderr)); + o.stdout +} + +/// Decode an elementary stream access unit by access unit (pts = index), flushing at the end. +pub fn decode_es(es: &[u8], threads: bool) -> (Vec, Decoder) { + let mut d = Decoder::new(); + d.set_threads(threads); + let mut out = Vec::new(); + for (i, r) in filmcraft_mpeg2v::access_units(es).into_iter().enumerate() { + out.extend(d.decode(&es[r], i as i64).expect("decode")); + } + out.extend(d.flush()); + (out, d) +} + +/// Per-frame comparison statistics against raw planes. +#[derive(Default, Debug, Clone, Copy)] +pub struct Stats { + pub max: u8, + pub sse: f64, + pub n: u64, + /// Samples differing at all. + pub diff: u64, +} + +impl Stats { + pub fn psnr(&self) -> f64 { + if self.sse == 0.0 { f64::INFINITY } else { 10.0 * (255.0 * 255.0 * self.n as f64 / self.sse).log10() } + } + pub fn add(&mut self, a: &[u8], b: &[u8]) { + assert_eq!(a.len(), b.len(), "plane size"); + for (x, y) in a.iter().zip(b) { + let d = x.abs_diff(*y); + self.max = self.max.max(d); + self.sse += (d as f64) * (d as f64); + self.diff += (d != 0) as u64; + } + self.n += a.len() as u64; + } + pub fn merge(&mut self, o: &Stats) { + self.max = self.max.max(o.max); + self.sse += o.sse; + self.n += o.n; + self.diff += o.diff; + } +} + +/// Compare decoded pictures with ffmpeg's raw frames; returns per-frame stats. +pub fn compare(pics: &[Picture], raw: &[u8]) -> Vec { + let p0 = &pics[0]; + let (w, h) = (p0.width as usize, p0.height as usize); + let (cw, ch) = (p0.chroma_width() as usize, p0.chroma_height() as usize); + let fsize = w * h + 2 * cw * ch; + assert_eq!(raw.len() % fsize, 0, "raw size"); + assert_eq!(raw.len() / fsize, pics.len(), "frame count = ffmpeg's"); + pics.iter() + .enumerate() + .map(|(i, p)| { + let f = &raw[i * fsize..(i + 1) * fsize]; + let mut s = Stats::default(); + s.add(&p.y, &f[..w * h]); + s.add(&p.cb, &f[w * h..w * h + cw * ch]); + s.add(&p.cr, &f[w * h + cw * ch..]); + s + }) + .collect() +} + +pub fn pix_fmt_of(c: ChromaFormat) -> &'static str { + match c { + ChromaFormat::Yuv420 => "yuv420p", + ChromaFormat::Yuv422 => "yuv422p", + ChromaFormat::Yuv444 => "yuv444p", + } +} diff --git a/crates/mpeg2v/tests/oracle.rs b/crates/mpeg2v/tests/oracle.rs new file mode 100644 index 000000000..975184f1d --- /dev/null +++ b/crates/mpeg2v/tests/oracle.rs @@ -0,0 +1,234 @@ +//! MPEG-1/2 video against ffmpeg: ffmpeg encodes lavfi test patterns into elementary streams, +//! ffmpeg's decode of the same file is the reference. MPEG-2's IDCT is not bit-exact by +//! definition (any IEEE 1180-accurate IDCT conforms), so frames are compared within a tolerance; +//! frame count and display order are exact. + +mod common; + +use common::*; + +const SPECS: &[Spec] = &[ + // progressive 4:2:0 IBBP, default VLCs + Spec { + name: "m2v_cif_ibbp", + lavfi: "testsrc2=size=352x288:rate=25", + frames: 30, + enc: &["-c:v", "mpeg2video", "-bf", "2", "-g", "12", "-b:v", "3M"], + pix_fmt: "yuv420p", + }, + // interlaced (frame pictures, field MC + field DCT), top field first, alternate scan + Spec { + name: "m2v_576i_tff", + lavfi: "testsrc2=size=720x576:rate=50,tinterlace=mode=interleave_top,setfield=tff", + frames: 16, + enc: &["-c:v", "mpeg2video", "-flags", "+ilme+ildct", "-alternate_scan", "1", "-bf", "2", "-g", "8", "-b:v", "6M"], + pix_fmt: "yuv420p", + }, + // bottom field first, intra VLC table one, non-linear quantiser, 10-bit DC + Spec { + name: "m2v_480i_bff_vlc1", + lavfi: "testsrc2=size=720x480:rate=60000/1001,tinterlace=mode=interleave_bottom,setfield=bff", + frames: 12, + enc: &[ + "-c:v", + "mpeg2video", + "-flags", + "+ilme+ildct", + "-intra_vlc", + "1", + "-non_linear_quant", + "1", + "-qmax", + "28", + "-dc", + "10", + "-bf", + "1", + "-g", + "6", + "-b:v", + "8M", + ], + pix_fmt: "yuv420p", + }, + // 4:2:2 intra-only, IMX / D-10 style (720x608, 9-bit... DC precision 10, low delay) + Spec { + name: "m2v_imx_422_intra", + lavfi: "testsrc2=size=720x608:rate=25", + frames: 6, + enc: &[ + "-c:v", + "mpeg2video", + "-pix_fmt", + "yuv422p", + "-g", + "1", + "-intra_vlc", + "1", + "-non_linear_quant", + "1", + "-dc", + "10", + "-flags", + "+ildct+low_delay", + "-qmin", + "1", + "-qmax", + "3", + "-b:v", + "30M", + "-minrate", + "30M", + "-maxrate", + "30M", + "-bufsize", + "1200000", + ], + pix_fmt: "yuv422p", + }, + // 4:2:2 long GOP interlaced HD (XDCAM HD422 style) + Spec { + name: "m2v_hd422_longgop", + lavfi: "testsrc2=size=1920x1080:rate=50,tinterlace=mode=interleave_top,setfield=tff", + frames: 8, + enc: &["-c:v", "mpeg2video", "-pix_fmt", "yuv422p", "-flags", "+ilme+ildct", "-bf", "2", "-g", "6", "-b:v", "50M"], + pix_fmt: "yuv422p", + }, + // low quantiser: large levels (escape codes), custom quantiser matrices + Spec { + name: "m2v_escapes_qmatrix", + lavfi: "mandelbrot=size=320x240:rate=25", + frames: 10, + enc: &[ + "-c:v", + "mpeg2video", + "-qscale:v", + "1", + "-qmin", + "1", + "-bf", + "2", + "-g", + "5", + "-intra_matrix", + "8,16,16,16,17,18,21,24,16,16,16,16,17,19,22,25,16,16,17,18,20,22,25,29,16,16,18,21,24,27,31,36,17,17,20,24,30,35,41,47,18,19,22,27,35,44,54,65,21,22,25,31,41,54,70,88,24,25,29,36,47,65,88,115", + "-inter_matrix", + "18,18,18,18,19,21,23,27,18,18,18,18,19,21,24,29,18,18,19,20,22,24,28,32,18,18,20,24,27,30,35,40,19,19,22,27,33,39,46,53,21,21,24,30,39,50,61,73,23,24,28,35,46,61,79,98,27,29,32,40,53,73,98,129", + ], + pix_fmt: "yuv420p", + }, + // no B pictures + Spec { name: "m2v_ipp", lavfi: "testsrc2=size=176x144:rate=30", frames: 20, enc: &["-c:v", "mpeg2video", "-bf", "0", "-g", "10"], pix_fmt: "yuv420p" }, + // MPEG-1 IBBP, and MPEG-1 at q 1 (8/16-bit escapes) + Spec { + name: "m1v_sif", + lavfi: "testsrc2=size=352x240:rate=30000/1001", + frames: 24, + enc: &["-c:v", "mpeg1video", "-bf", "2", "-g", "12", "-b:v", "1500k"], + pix_fmt: "yuv420p", + }, + Spec { + name: "m1v_q1", + lavfi: "mandelbrot=size=192x144:rate=25", + frames: 8, + enc: &["-c:v", "mpeg1video", "-qscale:v", "1", "-qmin", "1", "-bf", "1", "-g", "4"], + pix_fmt: "yuv420p", + }, +]; + +/// 1080i 4:2:0 (decode benchmark; Main@High). +pub const HD1080I: Spec = Spec { + name: "m2v_1080i", + lavfi: "testsrc2=size=1920x1080:rate=50,tinterlace=mode=interleave_top,setfield=tff", + frames: 25, + enc: &["-c:v", "mpeg2video", "-flags", "+ilme+ildct", "-bf", "2", "-g", "12", "-b:v", "25M", "-maxrate", "25M", "-bufsize", "9M"], + pix_fmt: "yuv420p", +}; + +/// Tolerance against ffmpeg (whose integer IDCT differs from our IEEE 1180-accurate one, and the +/// difference propagates through prediction): per frame max |Δ| ≤ 4 and PSNR ≥ 58 dB; overall +/// at most 3 % of samples differ at all. +pub const TOL_MAX: u8 = 4; +pub const MIN_PSNR: f64 = 58.0; +pub const MAX_DIFFERING: f64 = 0.03; + +/// Per displayed frame: (pict_type, interlaced_frame, top_field_first) from ffprobe. +fn ffprobe_frames(file: &std::path::Path) -> Option> { + let fp = filmcraft_testkit::ffprobe()?; + let o = std::process::Command::new(fp) + .args(["-v", "error", "-select_streams", "v:0", "-show_entries", "frame=pict_type,interlaced_frame,top_field_first", "-of", "csv=p=0"]) + .arg(file) + .output() + .ok()?; + Some( + String::from_utf8_lossy(&o.stdout) + .lines() + .filter(|l| !l.is_empty()) + .map(|l| { + let v: Vec<&str> = l.split(',').collect(); + (v[0].to_string(), v.get(1) == Some(&"1"), v.get(2) == Some(&"1")) + }) + .collect(), + ) +} + +fn check(spec: &Spec) { + let ff = filmcraft_testkit::require_ffmpeg!(); + let f = make(&ff, spec).unwrap_or_else(|| panic!("could not generate {}", spec.name)); + let es = std::fs::read(&f).unwrap(); + let (pics, dec) = decode_es(&es, true); + assert_eq!(dec.errors(), 0, "{}: slice errors ({:?})", spec.name, dec.last_error()); + let raw = ffmpeg_frames(&ff, &f, spec.pix_fmt); + let stats = compare(&pics, &raw); + let mut all = Stats::default(); + for (i, s) in stats.iter().enumerate() { + all.merge(s); + assert!(s.max <= TOL_MAX && s.psnr() >= MIN_PSNR, "{} frame {i} ({:?}): max {} psnr {:.2}", spec.name, pics[i].picture_type, s.max, s.psnr()); + } + assert!((all.diff as f64) / (all.n as f64) <= MAX_DIFFERING, "{}: {} of {} samples differ", spec.name, all.diff, all.n); + // display order: picture types, interlacing and field order as ffprobe reports them + if let Some(fr) = ffprobe_frames(&f) { + assert_eq!(fr.len(), pics.len()); + for (i, (p, (t, il, tff))) in pics.iter().zip(&fr).enumerate() { + assert_eq!(format!("{:?}", p.picture_type), *t, "{} frame {i} type", spec.name); + assert_eq!(!p.progressive_frame, *il, "{} frame {i} interlaced", spec.name); + if *il { + assert_eq!(p.top_field_first, *tff, "{} frame {i} field order", spec.name); + } + } + } + println!( + "{}: {} frames, {} ({}), max diff {}, {:.4}% samples differ, PSNR {:.2} dB", + spec.name, + pics.len(), + pics[0].info.codec_name(), + pix_fmt_of(pics[0].chroma), + all.max, + 100.0 * all.diff as f64 / all.n as f64, + all.psnr() + ); + // the single-threaded decode is identical + let (single, _) = decode_es(&es, false); + assert!(single.iter().zip(&pics).all(|(a, b)| a.y == b.y && a.cb == b.cb && a.cr == b.cr)); +} + +#[test] +fn ffmpeg_fixtures_match() { + for s in SPECS { + check(s); + } +} + +#[test] +fn hd_1080i_matches() { + check(&HD1080I); +} + +#[test] +#[ignore] +fn generate_fixtures() { + let Some(ff) = filmcraft_testkit::ffmpeg_or_skip("mpeg2v fixtures") else { return }; + for s in SPECS.iter().chain([&HD1080I]) { + filmcraft_testkit::fixtures::generate_and_report(&format!("mpeg2v/{}", s.name), &[path(s)], || make(&ff, s)); + } +} diff --git a/crates/mpegts/Cargo.toml b/crates/mpegts/Cargo.toml new file mode 100644 index 000000000..9ec521f4f --- /dev/null +++ b/crates/mpegts/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "filmcraft-mpegts" +description = "Clean-room MPEG-2 Systems (ISO/IEC 13818-1) demuxer: Transport Stream (188-byte, 192-byte M2TS) and Program Stream (MPEG-1/2 PS, VOB, MOD)" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +thiserror = { workspace = true } + +[dev-dependencies] +filmcraft-testkit = { workspace = true } diff --git a/crates/mpegts/src/lib.rs b/crates/mpegts/src/lib.rs new file mode 100644 index 000000000..69fb707ab --- /dev/null +++ b/crates/mpegts/src/lib.rs @@ -0,0 +1 @@ +//! MPEG-2 Systems demuxer (work in progress). diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 569aba086..64fb81e96 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -31,6 +31,9 @@ const LAYERS: &[(&str, u8)] = &[ ("isobmff", 0), ("matroska", 0), ("mxf", 0), + ("mpegts", 0), + ("mpeg2v", 0), + ("ac3", 0), ("ogg", 0), ("riff", 0), ("h264", 0),