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 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 02:43:29 -04:00
co-authored by Claude Opus 5.5
parent ac4d458712
commit 2cbf91dded
16 changed files with 3528 additions and 0 deletions
Generated
+17
View File
@@ -2012,6 +2012,23 @@ dependencies = [
"thiserror 2.0.21", "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]] [[package]]
name = "filmcraft-mxf" name = "filmcraft-mxf"
version = "0.1.0" version = "0.1.0"
+2
View File
@@ -19,6 +19,8 @@ filmcraft-bitstream = { path = "crates/bitstream" }
filmcraft-isobmff = { path = "crates/isobmff" } filmcraft-isobmff = { path = "crates/isobmff" }
filmcraft-matroska = { path = "crates/matroska" } filmcraft-matroska = { path = "crates/matroska" }
filmcraft-mxf = { path = "crates/mxf" } filmcraft-mxf = { path = "crates/mxf" }
filmcraft-mpegts = { path = "crates/mpegts" }
filmcraft-mpeg2v = { path = "crates/mpeg2v" }
filmcraft-ogg = { path = "crates/ogg" } filmcraft-ogg = { path = "crates/ogg" }
filmcraft-h264 = { path = "crates/h264" } filmcraft-h264 = { path = "crates/h264" }
filmcraft-hevc = { path = "crates/hevc" } filmcraft-hevc = { path = "crates/hevc" }
+21
View File
@@ -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 }
+93
View File
@@ -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());
}
}
+466
View File
@@ -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<Self> {
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<QuantMatrixExtension>),
PictureCoding(PictureCodingExtension),
/// Scalability, copyright, picture display… (not needed to decode the base layer).
Other(u8),
}
impl Extension {
pub fn parse(payload: &[u8]) -> Result<Self> {
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<Self> {
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<Self> {
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<usize> = (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<usize> = (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");
}
}
+179
View File
@@ -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::<f64>() * 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::<f64>() * 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::<f64>() / 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::<f64>() / 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::<i64>() as f64 / (64.0 * n) <= 0.02);
assert!((sum_err.iter().sum::<i64>() 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}");
}
}
}
+800
View File
@@ -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<u8>, 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<T> = std::result::Result<T, Error>;
#[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<u8>,
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<u8>,
/// 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<u8>,
pub cb: Vec<u8>,
pub cr: Vec<u8>,
/// 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<SequenceInfo>,
}
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<FieldOrder> {
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<SequenceExtension>,
display: Option<DisplayExtension>,
info: Arc<SequenceInfo>,
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<SequenceExtension>, display: Option<DisplayExtension>) -> Result<Seq> {
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<SequenceInfo> {
let codes = start_codes(data);
let mut seq: Option<(SequenceHeader, Option<SequenceExtension>, Option<DisplayExtension>)> = 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<GopHeader>,
pub pictures: Vec<(PictureHeader, Option<PictureCodingExtension>)>,
}
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<std::ops::Range<usize>> {
let codes = start_codes(es);
let mut out = Vec::new();
let mut cur: Option<usize> = None;
let mut header: Option<usize> = None;
let mut open_field: Option<u8> = 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<PictureCodingExtension>,
/// (start code, payload range).
slices: Vec<(u8, std::ops::Range<usize>)>,
}
#[derive(Clone)]
struct Meta {
pts: i64,
ptype: PictureType,
temporal_reference: u16,
pce: PictureCodingExtension,
field_pictures: bool,
info: Arc<SequenceInfo>,
}
/// A frame whose first field has been decoded.
struct Building {
frame: Frame,
parity: usize,
meta: Meta,
}
struct Anchor {
frame: Arc<Frame>,
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<Seq>,
/// A sequence header is waiting for its extensions (applied at the next non-extension unit).
seq_pending: Option<(SequenceHeader, Option<SequenceExtension>, Option<DisplayExtension>)>,
matrices: [[u8; 64]; 4],
closed_gop: bool,
/// Older and newer anchor (reference) frames.
fwd: Option<Anchor>,
bwd: Option<Anchor>,
building: Option<Building>,
pending: Option<Pending>,
out: Vec<Picture>,
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<Vec<Picture>> {
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<Picture> {
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<Option<&'static str>>,
}
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<usize>)], 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<&[u8]>> = 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;
+154
View File
@@ -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<u8>; 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<usize>) -> 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);
}
}
+615
View File
@@ -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<usize>,
}
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<T> = std::result::Result<T, SliceError>;
/// 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<bool> {
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<usize>,
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
}
+171
View File
@@ -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<u8>,
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<u8> {
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);
}
+627
View File
@@ -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<Entry>,
}
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<i16> {
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<Tables> = 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<i16> = MOTION.iter().map(|x| x.1).collect();
m.sort();
assert_eq!(m, (-16..=16).collect::<Vec<_>>());
// 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<i16> = 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)));
}
}
+129
View File
@@ -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<PathBuf> {
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<u8> {
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<Picture>, 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<Stats> {
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",
}
}
+234
View File
@@ -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<Vec<(String, bool, bool)>> {
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));
}
}
+16
View File
@@ -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 }
+1
View File
@@ -0,0 +1 @@
//! MPEG-2 Systems demuxer (work in progress).
+3
View File
@@ -31,6 +31,9 @@ const LAYERS: &[(&str, u8)] = &[
("isobmff", 0), ("isobmff", 0),
("matroska", 0), ("matroska", 0),
("mxf", 0), ("mxf", 0),
("mpegts", 0),
("mpeg2v", 0),
("ac3", 0),
("ogg", 0), ("ogg", 0),
("riff", 0), ("riff", 0),
("h264", 0), ("h264", 0),