use oxideav_core::bits::BitWriter;
use oxideav_core::{Error, Result};
use crate::fdct::{fdct_intra, fdct_signed};
use crate::idct::{idct_intra, idct_signed};
use crate::mb::Picture;
use crate::picture::SourceFormat;
use crate::quant::{quant_ac, quant_intra_dc};
use crate::tables::{
encode_cbp, encode_mba_diff, encode_mvd, lookup_tcoeff, MBA_STUFFING, MTYPE_INTER,
MTYPE_INTER_MC_CBP, MTYPE_INTER_MC_CBP_MQUANT, MTYPE_INTER_MC_FIL_CBP,
MTYPE_INTER_MC_FIL_CBP_MQUANT, MTYPE_INTER_MC_FIL_ONLY, MTYPE_INTER_MC_ONLY,
MTYPE_INTER_MQUANT, MTYPE_INTRA, MTYPE_INTRA_MQUANT, ZIGZAG,
};
pub const DEFAULT_QUANT: u32 = 8;
const MV_MAX: i32 = 15;
const ME_SEARCH_RADIUS: i32 = MV_MAX;
struct MqRateCtrl {
base_quant: u32,
quant_in_effect: u32,
target_per_mb: u32,
cumulative: u64,
min_quant: u32,
max_quant: u32,
}
impl MqRateCtrl {
fn new(base_quant: u32, target_per_mb: u32) -> Self {
const WIN: u32 = 6;
let min_quant = base_quant.saturating_sub(WIN).max(1);
let max_quant = (base_quant + WIN).min(31);
Self {
base_quant,
quant_in_effect: base_quant,
target_per_mb,
cumulative: 0,
min_quant,
max_quant,
}
}
fn desired(&self, mb_idx: u32) -> u32 {
let expected = (mb_idx as u64).saturating_mul(self.target_per_mb as u64);
let slack = (self.target_per_mb as u64).saturating_mul(16);
let cur = self.quant_in_effect as i32;
let bumped = if self.cumulative > expected.saturating_add(slack) {
cur + 1
} else if self.cumulative + slack * 2 < expected {
cur - 1
} else {
match cur.cmp(&(self.base_quant as i32)) {
std::cmp::Ordering::Greater => cur - 1,
std::cmp::Ordering::Less => cur,
std::cmp::Ordering::Equal => cur,
}
};
(bumped.max(self.min_quant as i32) as u32).min(self.max_quant)
}
fn account(&mut self, bits: u64) {
self.cumulative = self.cumulative.saturating_add(bits);
}
fn commit_quant(&mut self, q: u32) {
self.quant_in_effect = q;
}
}
pub fn encode_intra_picture(
fmt: SourceFormat,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
quant: u32,
temporal_reference: u8,
) -> Result<Vec<u8>> {
let (bytes, _recon) = encode_intra_picture_with_recon(
fmt,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
quant,
temporal_reference,
)?;
Ok(bytes)
}
pub fn encode_intra_picture_with_recon(
fmt: SourceFormat,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
quant: u32,
temporal_reference: u8,
) -> Result<(Vec<u8>, Picture)> {
validate_inputs(
fmt,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
quant,
temporal_reference,
)?;
let (w, h) = fmt.dimensions();
let mut recon = Picture::new(w as usize, h as usize);
let mut bw = BitWriter::with_capacity(4096);
write_picture_header(&mut bw, fmt, temporal_reference);
for &gn in fmt.gob_numbers() {
write_gob_header(&mut bw, gn, quant);
let (gob_x, gob_y) = gob_origin_luma(fmt, gn);
encode_gob_intra(
&mut bw, y, y_stride, cb, cb_stride, cr, cr_stride, gob_x, gob_y, quant, &mut recon,
);
}
bw.align_to_byte();
Ok((bw.finish(), recon))
}
#[allow(clippy::too_many_arguments)]
pub fn encode_inter_picture(
fmt: SourceFormat,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
quant: u32,
temporal_reference: u8,
reference: &Picture,
) -> Result<(Vec<u8>, Picture)> {
validate_inputs(
fmt,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
quant,
temporal_reference,
)?;
let (w, h) = fmt.dimensions();
if reference.width != w as usize || reference.height != h as usize {
return Err(Error::invalid(format!(
"h261 encode: reference dims {}x{} mismatch picture {}x{}",
reference.width, reference.height, w, h
)));
}
let mut recon = Picture::new(w as usize, h as usize);
let mut bw = BitWriter::with_capacity(8192);
write_picture_header(&mut bw, fmt, temporal_reference);
for &gn in fmt.gob_numbers() {
write_gob_header(&mut bw, gn, quant);
let (gob_x, gob_y) = gob_origin_luma(fmt, gn);
encode_gob_inter(
&mut bw, y, y_stride, cb, cb_stride, cr, cr_stride, gob_x, gob_y, quant, reference,
&mut recon,
);
}
bw.align_to_byte();
Ok((bw.finish(), recon))
}
pub struct H261Encoder {
fmt: SourceFormat,
quant: u32,
next_tr: u8,
reference: Option<Picture>,
intra_period: u32,
frames_since_intra: u32,
}
impl H261Encoder {
pub fn new(fmt: SourceFormat, quant: u32) -> Self {
debug_assert!((1..=31).contains(&quant));
Self {
fmt,
quant,
next_tr: 0,
reference: None,
intra_period: 30, frames_since_intra: 0,
}
}
pub fn with_intra_period(mut self, period: u32) -> Self {
self.intra_period = period;
self
}
#[allow(clippy::too_many_arguments)]
pub fn encode_frame(
&mut self,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
) -> Result<Vec<u8>> {
let force_intra = self.reference.is_none()
|| (self.intra_period != 0 && self.frames_since_intra >= self.intra_period);
let (bytes, recon) = if force_intra {
self.frames_since_intra = 1;
encode_intra_picture_with_recon(
self.fmt,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
self.quant,
self.next_tr,
)?
} else {
self.frames_since_intra += 1;
let reference = self
.reference
.as_ref()
.expect("reference must exist by now");
encode_inter_picture(
self.fmt,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
self.quant,
self.next_tr,
reference,
)?
};
self.reference = Some(recon);
self.next_tr = self.next_tr.wrapping_add(1) & 0x1F;
Ok(bytes)
}
}
#[allow(clippy::too_many_arguments)]
fn validate_inputs(
fmt: SourceFormat,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
quant: u32,
temporal_reference: u8,
) -> Result<()> {
if !(1..=31).contains(&quant) {
return Err(Error::invalid(format!(
"h261 encode: QUANT out of range: {quant}"
)));
}
if temporal_reference > 31 {
return Err(Error::invalid(format!(
"h261 encode: TR out of range: {temporal_reference}"
)));
}
let (_w, h) = fmt.dimensions();
let h = h as usize;
if y.len() < y_stride * h || cb.len() < cb_stride * (h / 2) || cr.len() < cr_stride * (h / 2) {
return Err(Error::invalid("h261 encode: input plane too short"));
}
Ok(())
}
pub fn write_picture_header(bw: &mut BitWriter, fmt: SourceFormat, tr: u8) {
bw.write_u32(0x00010, 20); bw.write_u32(tr as u32, 5); bw.write_u32(0, 1);
bw.write_u32(0, 1);
bw.write_u32(0, 1);
let fmt_bit = match fmt {
SourceFormat::Qcif => 0,
SourceFormat::Cif => 1,
};
bw.write_u32(fmt_bit, 1);
bw.write_u32(1, 1);
bw.write_u32(1, 1);
bw.write_u32(0, 1);
}
pub fn write_gob_header(bw: &mut BitWriter, gn: u8, gquant: u32) {
debug_assert!((1..=12).contains(&gn));
debug_assert!((1..=31).contains(&gquant));
bw.write_u32(0x0001, 16); bw.write_u32(gn as u32, 4);
bw.write_u32(gquant, 5);
bw.write_u32(0, 1);
}
fn gob_origin_luma(fmt: SourceFormat, gn: u8) -> (usize, usize) {
match fmt {
SourceFormat::Cif => crate::gob::cif_gob_origin_luma(gn),
SourceFormat::Qcif => crate::gob::qcif_gob_origin_luma(gn),
}
}
#[allow(clippy::too_many_arguments)]
fn encode_gob_intra(
bw: &mut BitWriter,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
gob_x: usize,
gob_y: usize,
quant: u32,
recon: &mut Picture,
) {
let mut prev_mba: u8 = 0;
for mba in 1u8..=33 {
let diff = mba - prev_mba;
let (bits, code) = encode_mba_diff(diff);
bw.write_u32(code, bits as u32);
bw.write_u32(MTYPE_INTRA.1, MTYPE_INTRA.0 as u32);
let mb_col = (mba - 1) as usize % 11;
let mb_row = (mba - 1) as usize / 11;
let luma_x = gob_x + mb_col * 16;
let luma_y = gob_y + mb_row * 16;
encode_intra_mb_blocks(
bw, y, y_stride, cb, cb_stride, cr, cr_stride, luma_x, luma_y, quant, recon,
);
prev_mba = mba;
}
}
#[allow(clippy::too_many_arguments)]
fn encode_gob_inter(
bw: &mut BitWriter,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
gob_x: usize,
gob_y: usize,
quant: u32,
reference: &Picture,
recon: &mut Picture,
) {
let mut prev_mba: u8 = 0;
let mut pred_mv: (i32, i32) = (0, 0);
let mut prev_was_mc = false;
let rate_ctrl_enabled = std::env::var("OXIDEAV_H261_NO_MQUANT").is_err();
let target_per_mb = 32 + 4 * quant;
let mut rc = MqRateCtrl::new(quant, target_per_mb);
for mba in 1u8..=33 {
let mb_col = (mba - 1) as usize % 11;
let mb_row = (mba - 1) as usize / 11;
let luma_x = gob_x + mb_col * 16;
let luma_y = gob_y + mb_row * 16;
let bits_before_mb = bw.bit_position();
let mut blocks_pels: [[u8; 64]; 6] = [[0u8; 64]; 6];
for (b, (sub_x, sub_y)) in [(0, 0), (8, 0), (0, 8), (8, 8)].iter().enumerate() {
extract_block(
y,
y_stride,
luma_x + *sub_x,
luma_y + *sub_y,
&mut blocks_pels[b],
);
}
let cx = luma_x / 2;
let cy = luma_y / 2;
extract_block(cb, cb_stride, cx, cy, &mut blocks_pels[4]);
extract_block(cr, cr_stride, cx, cy, &mut blocks_pels[5]);
let (mvx, mvy) = if matches!(mba, 11 | 22) {
(0, 0)
} else {
motion_estimate_luma(y, y_stride, reference, luma_x, luma_y)
};
let mut blocks_pred: [[u8; 64]; 6] = [[0u8; 64]; 6];
for (b, (sub_x, sub_y)) in [(0, 0), (8, 0), (0, 8), (8, 8)].iter().enumerate() {
extract_block_mv(
&reference.y,
reference.y_stride,
luma_x + *sub_x,
luma_y + *sub_y,
mvx,
mvy,
&mut blocks_pred[b],
);
}
let cmvx = mvx / 2;
let cmvy = mvy / 2;
extract_block_mv(
&reference.cb,
reference.c_stride,
cx,
cy,
cmvx,
cmvy,
&mut blocks_pred[4],
);
extract_block_mv(
&reference.cr,
reference.c_stride,
cx,
cy,
cmvx,
cmvy,
&mut blocks_pred[5],
);
let cur_q = rc.quant_in_effect;
let (cbp_nf, mut q_levels_nf, mut recon_nf, resid_bits_nf) =
quantise_residual(&blocks_pels, &blocks_pred, cur_q);
let mut blocks_pred_fil: [[u8; 64]; 6] = [[0u8; 64]; 6];
for b in 0..6 {
blocks_pred_fil[b] = apply_loop_filter_block(&blocks_pred[b]);
}
let (cbp_fil, mut q_levels_fil, mut recon_fil, resid_bits_fil) =
quantise_residual(&blocks_pels, &blocks_pred_fil, cur_q);
let mv_is_zero = mvx == 0 && mvy == 0;
let allow_mc = !matches!(mba, 11 | 22);
let mv_l1 = (mvx.abs() + mvy.abs()) as u32;
let cbp_vlc_bits = |c: u8| -> u32 {
if c == 0 {
0
} else {
6
}
};
let mvd_bits = |d: i32| -> u32 { (d.unsigned_abs() * 2).saturating_add(1) };
let mvd_total = if mv_is_zero {
0
} else {
mvd_bits(mvx) + mvd_bits(mvy)
};
const COST_INF: u32 = u32::MAX / 4;
let mut best_cost = COST_INF;
let mut best_mode: u8 = 0;
if mv_is_zero && cbp_nf == 0 {
best_cost = 0; best_mode = 0;
}
if mv_is_zero && cbp_nf != 0 {
let c = MTYPE_INTER.0 as u32 + cbp_vlc_bits(cbp_nf) + resid_bits_nf;
if c < best_cost {
best_cost = c;
best_mode = 1;
}
}
if allow_mc && !mv_is_zero && cbp_nf == 0 {
let c = MTYPE_INTER_MC_ONLY.0 as u32 + mvd_total;
if c < best_cost {
best_cost = c;
best_mode = 2;
}
}
if allow_mc && !mv_is_zero && cbp_nf != 0 {
let c = MTYPE_INTER_MC_CBP.0 as u32 + mvd_total + cbp_vlc_bits(cbp_nf) + resid_bits_nf;
if c < best_cost {
best_cost = c;
best_mode = 3;
}
}
let allow_fil = std::env::var("OXIDEAV_H261_NO_FIL").is_err();
if allow_fil && allow_mc && cbp_fil == 0 {
let c = MTYPE_INTER_MC_FIL_ONLY.0 as u32 + mvd_total;
if c < best_cost {
best_cost = c;
best_mode = 4;
}
}
if allow_fil && allow_mc && cbp_fil != 0 {
let c = MTYPE_INTER_MC_FIL_CBP.0 as u32
+ mvd_total
+ cbp_vlc_bits(cbp_fil)
+ resid_bits_fil;
if c < best_cost {
best_cost = c;
best_mode = 5;
}
}
if best_mode == 0 {
pred_mv = (0, 0);
prev_was_mc = false;
for b in 0..6 {
write_block_to_picture(recon, b, luma_x, luma_y, &recon_nf[b]);
}
let _ = (best_cost, mv_l1);
continue;
}
let mode_supports_mquant = matches!(best_mode, 1 | 3 | 5);
let desired_q = if rate_ctrl_enabled {
rc.desired((mba - 1) as u32)
} else {
cur_q
};
let mut emit_q = cur_q;
let mut emit_mquant = false;
if mode_supports_mquant && desired_q != cur_q {
let (pred_blocks, _is_fil) = match best_mode {
1 => (&blocks_pred, false),
3 => (&blocks_pred, false),
5 => (&blocks_pred_fil, true),
_ => unreachable!(),
};
let (new_cbp, new_levels, new_recon, _new_bits) =
quantise_residual(&blocks_pels, pred_blocks, desired_q);
if new_cbp != 0 {
emit_q = desired_q;
emit_mquant = true;
if matches!(best_mode, 1 | 3) {
q_levels_nf = new_levels;
recon_nf = new_recon;
let _ = new_cbp; } else {
q_levels_fil = new_levels;
recon_fil = new_recon;
}
}
}
let cbp_for_emit = if emit_mquant {
let mut c: u8 = 0;
for b in 0..6 {
let levels = match best_mode {
1 | 3 => &q_levels_nf[b],
5 => &q_levels_fil[b],
_ => unreachable!(),
};
if levels.iter().any(|&l| l != 0) {
c |= 1 << (5 - b);
}
}
c
} else {
match best_mode {
1 | 3 => cbp_nf,
5 => cbp_fil,
_ => 0,
}
};
let (emit_q, emit_mquant) = if matches!(best_mode, 1 | 3 | 5) && cbp_for_emit == 0 {
(cur_q, false)
} else {
(emit_q, emit_mquant)
};
let diff = mba - prev_mba;
let (bits, code) = encode_mba_diff(diff);
bw.write_u32(code, bits as u32);
let mvd_reset = matches!(mba, 1 | 12 | 23) || diff != 1 || !prev_was_mc;
let pred_for_mvd = if mvd_reset { (0, 0) } else { pred_mv };
match best_mode {
1 => {
debug_assert_ne!(cbp_for_emit, 0);
if emit_mquant {
bw.write_u32(MTYPE_INTER_MQUANT.1, MTYPE_INTER_MQUANT.0 as u32);
bw.write_u32(emit_q, 5);
} else {
bw.write_u32(MTYPE_INTER.1, MTYPE_INTER.0 as u32);
}
let (cbits, ccode) = encode_cbp(cbp_for_emit);
bw.write_u32(ccode, cbits as u32);
for b in 0..6 {
if cbp_for_emit & (1 << (5 - b)) != 0 {
emit_inter_block_levels(bw, &q_levels_nf[b]);
}
write_block_to_picture(recon, b, luma_x, luma_y, &recon_nf[b]);
}
pred_mv = (0, 0);
prev_was_mc = false;
}
2 => {
bw.write_u32(MTYPE_INTER_MC_ONLY.1, MTYPE_INTER_MC_ONLY.0 as u32);
let dx = mvx - pred_for_mvd.0;
let dy = mvy - pred_for_mvd.1;
let (xb, xc) = encode_mvd(dx);
bw.write_u32(xc, xb as u32);
let (yb, yc) = encode_mvd(dy);
bw.write_u32(yc, yb as u32);
for b in 0..6 {
write_block_to_picture(recon, b, luma_x, luma_y, &recon_nf[b]);
}
pred_mv = (mvx, mvy);
prev_was_mc = true;
}
3 => {
if emit_mquant {
bw.write_u32(
MTYPE_INTER_MC_CBP_MQUANT.1,
MTYPE_INTER_MC_CBP_MQUANT.0 as u32,
);
bw.write_u32(emit_q, 5);
} else {
bw.write_u32(MTYPE_INTER_MC_CBP.1, MTYPE_INTER_MC_CBP.0 as u32);
}
let dx = mvx - pred_for_mvd.0;
let dy = mvy - pred_for_mvd.1;
let (xb, xc) = encode_mvd(dx);
bw.write_u32(xc, xb as u32);
let (yb, yc) = encode_mvd(dy);
bw.write_u32(yc, yb as u32);
let (cbits, ccode) = encode_cbp(cbp_for_emit);
bw.write_u32(ccode, cbits as u32);
for b in 0..6 {
if cbp_for_emit & (1 << (5 - b)) != 0 {
emit_inter_block_levels(bw, &q_levels_nf[b]);
}
write_block_to_picture(recon, b, luma_x, luma_y, &recon_nf[b]);
}
pred_mv = (mvx, mvy);
prev_was_mc = true;
}
4 => {
bw.write_u32(MTYPE_INTER_MC_FIL_ONLY.1, MTYPE_INTER_MC_FIL_ONLY.0 as u32);
let dx = mvx - pred_for_mvd.0;
let dy = mvy - pred_for_mvd.1;
let (xb, xc) = encode_mvd(dx);
bw.write_u32(xc, xb as u32);
let (yb, yc) = encode_mvd(dy);
bw.write_u32(yc, yb as u32);
for b in 0..6 {
write_block_to_picture(recon, b, luma_x, luma_y, &recon_fil[b]);
}
pred_mv = (mvx, mvy);
prev_was_mc = true;
}
5 => {
if emit_mquant {
bw.write_u32(
MTYPE_INTER_MC_FIL_CBP_MQUANT.1,
MTYPE_INTER_MC_FIL_CBP_MQUANT.0 as u32,
);
bw.write_u32(emit_q, 5);
} else {
bw.write_u32(MTYPE_INTER_MC_FIL_CBP.1, MTYPE_INTER_MC_FIL_CBP.0 as u32);
}
let dx = mvx - pred_for_mvd.0;
let dy = mvy - pred_for_mvd.1;
let (xb, xc) = encode_mvd(dx);
bw.write_u32(xc, xb as u32);
let (yb, yc) = encode_mvd(dy);
bw.write_u32(yc, yb as u32);
let (cbits, ccode) = encode_cbp(cbp_for_emit);
bw.write_u32(ccode, cbits as u32);
for b in 0..6 {
if cbp_for_emit & (1 << (5 - b)) != 0 {
emit_inter_block_levels(bw, &q_levels_fil[b]);
}
write_block_to_picture(recon, b, luma_x, luma_y, &recon_fil[b]);
}
pred_mv = (mvx, mvy);
prev_was_mc = true;
}
_ => unreachable!("bad best_mode {best_mode}"),
}
prev_mba = mba;
if emit_mquant {
rc.commit_quant(emit_q);
}
let bits_after_mb = bw.bit_position();
rc.account(bits_after_mb - bits_before_mb);
}
}
fn sad16x16(
src: &[u8],
src_stride: usize,
reference: &Picture,
sx: usize,
sy: usize,
mvx: i32,
mvy: i32,
) -> u32 {
let ref_w = reference.y_stride as i32;
let ref_h = (reference.y.len() / reference.y_stride) as i32;
let mut sad: u32 = 0;
for j in 0..16i32 {
let ry = (sy as i32 + j + mvy).clamp(0, ref_h - 1) as usize;
let sy_row = sy + j as usize;
for i in 0..16i32 {
let rx = (sx as i32 + i + mvx).clamp(0, ref_w - 1) as usize;
let s = src[sy_row * src_stride + sx + i as usize] as i32;
let r = reference.y[ry * reference.y_stride + rx] as i32;
sad += (s - r).unsigned_abs();
}
}
sad
}
fn motion_estimate_luma(
src: &[u8],
src_stride: usize,
reference: &Picture,
sx: usize,
sy: usize,
) -> (i32, i32) {
let zero_sad = sad16x16(src, src_stride, reference, sx, sy, 0, 0);
if zero_sad == 0 {
return (0, 0);
}
let mut best_mv = (0i32, 0i32);
let mv_cost = |mvx: i32, mvy: i32| -> u32 { ((mvx.abs() + mvy.abs()) as u32) * 2 };
let mut best_cost = zero_sad.saturating_add(mv_cost(0, 0));
for mvy in -ME_SEARCH_RADIUS..=ME_SEARCH_RADIUS {
for mvx in -ME_SEARCH_RADIUS..=ME_SEARCH_RADIUS {
if mvx == 0 && mvy == 0 {
continue;
}
let s = sad16x16(src, src_stride, reference, sx, sy, mvx, mvy);
let cost = s.saturating_add(mv_cost(mvx, mvy));
if cost < best_cost {
best_cost = cost;
best_mv = (mvx, mvy);
}
}
}
best_mv
}
fn apply_loop_filter_block(src: &[u8; 64]) -> [u8; 64] {
let mut h = [0i32; 64];
for j in 0..8 {
for i in 0..8 {
let v = if i == 0 || i == 7 {
src[j * 8 + i] as i32
} else {
let a = src[j * 8 + i - 1] as i32;
let b = src[j * 8 + i] as i32;
let c = src[j * 8 + i + 1] as i32;
(a + 2 * b + c + 2) >> 2
};
h[j * 8 + i] = v;
}
}
let mut out = [0u8; 64];
for i in 0..8 {
for j in 0..8 {
let v = if j == 0 || j == 7 {
h[j * 8 + i]
} else {
let a = h[(j - 1) * 8 + i];
let b = h[j * 8 + i];
let c = h[(j + 1) * 8 + i];
(a + 2 * b + c + 2) >> 2
};
out[j * 8 + i] = v.clamp(0, 255) as u8;
}
}
out
}
fn quantise_residual(
blocks_pels: &[[u8; 64]; 6],
blocks_pred: &[[u8; 64]; 6],
quant: u32,
) -> (u8, [[i32; 64]; 6], [[u8; 64]; 6], u32) {
let mut cbp: u8 = 0;
let mut q_levels: [[i32; 64]; 6] = [[0i32; 64]; 6];
let mut recon_blocks: [[u8; 64]; 6] = [[0u8; 64]; 6];
let mut bit_estimate: u32 = 0;
for b in 0..6 {
let mut resid = [0i32; 64];
for i in 0..64 {
resid[i] = blocks_pels[b][i] as i32 - blocks_pred[b][i] as i32;
}
let mut coeffs = [0i32; 64];
fdct_signed(&resid, &mut coeffs);
let mut levels = [0i32; 64];
let mut any_nonzero = false;
for i in 0..64 {
let l = quant_ac(coeffs[i], quant);
levels[i] = l;
if l != 0 {
any_nonzero = true;
}
}
q_levels[b] = levels;
if any_nonzero {
cbp |= 1 << (5 - b);
let mut dequant = [0i32; 64];
for i in 0..64 {
dequant[i] = crate::block::dequant_ac(levels[i], quant);
}
let mut residual = [0i32; 64];
idct_signed(&dequant, &mut residual);
for i in 0..64 {
let v = blocks_pred[b][i] as i32 + residual[i];
recon_blocks[b][i] = v.clamp(0, 255) as u8;
}
let mut nz: u32 = 0;
for &l in levels.iter() {
if l != 0 {
nz += 1;
}
}
bit_estimate += nz * 4 + 2;
} else {
recon_blocks[b] = blocks_pred[b];
}
}
(cbp, q_levels, recon_blocks, bit_estimate)
}
#[allow(clippy::too_many_arguments)]
fn extract_block_mv(
plane: &[u8],
stride: usize,
x: usize,
y: usize,
mvx: i32,
mvy: i32,
out: &mut [u8; 64],
) {
let plane_w = stride as i32;
let plane_h = (plane.len() / stride) as i32;
for j in 0..8 {
for i in 0..8 {
let sx = (x as i32 + i + mvx).clamp(0, plane_w - 1) as usize;
let sy = (y as i32 + j + mvy).clamp(0, plane_h - 1) as usize;
out[(j as usize) * 8 + i as usize] = plane[sy * stride + sx];
}
}
}
#[allow(clippy::too_many_arguments)]
fn encode_intra_mb_blocks(
bw: &mut BitWriter,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
luma_x: usize,
luma_y: usize,
quant: u32,
recon: &mut Picture,
) {
for (b, (sub_x, sub_y)) in [(0, 0), (8, 0), (0, 8), (8, 8)].iter().enumerate() {
let mut pels = [0u8; 64];
extract_block(y, y_stride, luma_x + *sub_x, luma_y + *sub_y, &mut pels);
let mut out = [0u8; 64];
encode_intra_block(bw, &pels, quant, &mut out);
write_block_to_picture(recon, b, luma_x, luma_y, &out);
}
let cx = luma_x / 2;
let cy = luma_y / 2;
let mut cb_pels = [0u8; 64];
extract_block(cb, cb_stride, cx, cy, &mut cb_pels);
let mut cb_out = [0u8; 64];
encode_intra_block(bw, &cb_pels, quant, &mut cb_out);
write_block_to_picture(recon, 4, luma_x, luma_y, &cb_out);
let mut cr_pels = [0u8; 64];
extract_block(cr, cr_stride, cx, cy, &mut cr_pels);
let mut cr_out = [0u8; 64];
encode_intra_block(bw, &cr_pels, quant, &mut cr_out);
write_block_to_picture(recon, 5, luma_x, luma_y, &cr_out);
}
fn extract_block(plane: &[u8], stride: usize, x: usize, y: usize, out: &mut [u8; 64]) {
for j in 0..8 {
for i in 0..8 {
let px = (y + j) * stride + (x + i);
out[j * 8 + i] = plane.get(px).copied().unwrap_or(0);
}
}
}
fn write_block_to_picture(
pic: &mut Picture,
block_idx: usize,
luma_x: usize,
luma_y: usize,
out: &[u8; 64],
) {
let (plane, stride, px, py): (&mut [u8], usize, usize, usize) = match block_idx {
0 => (pic.y.as_mut_slice(), pic.y_stride, luma_x, luma_y),
1 => (pic.y.as_mut_slice(), pic.y_stride, luma_x + 8, luma_y),
2 => (pic.y.as_mut_slice(), pic.y_stride, luma_x, luma_y + 8),
3 => (pic.y.as_mut_slice(), pic.y_stride, luma_x + 8, luma_y + 8),
4 => (pic.cb.as_mut_slice(), pic.c_stride, luma_x / 2, luma_y / 2),
5 => (pic.cr.as_mut_slice(), pic.c_stride, luma_x / 2, luma_y / 2),
_ => unreachable!(),
};
for j in 0..8 {
for i in 0..8 {
plane[(py + j) * stride + (px + i)] = out[j * 8 + i];
}
}
}
fn encode_intra_block(bw: &mut BitWriter, pels: &[u8; 64], quant: u32, recon_out: &mut [u8; 64]) {
let mut coeffs = [0i32; 64];
fdct_intra(pels, &mut coeffs);
let dc_code = quant_intra_dc(coeffs[0]);
bw.write_u32(dc_code as u32, 8);
let dc_rec: i32 = if dc_code == 0xFF {
1024
} else {
(dc_code as i32) * 8
};
let mut zz_levels = [0i32; 63];
for i in 1..64 {
zz_levels[i - 1] = quant_ac(coeffs[ZIGZAG[i]], quant);
}
let mut run: u32 = 0;
for &lvl in zz_levels.iter() {
if lvl == 0 {
run += 1;
continue;
}
emit_runlevel(bw, run as u8, lvl, false);
run = 0;
}
bw.write_u32(0b10, 2);
let mut rec_coeffs = [0i32; 64];
rec_coeffs[0] = dc_rec;
for i in 1..64 {
rec_coeffs[ZIGZAG[i]] = crate::block::dequant_ac(zz_levels[i - 1], quant);
}
idct_intra(&rec_coeffs, recon_out);
}
fn emit_inter_block_levels(bw: &mut BitWriter, levels: &[i32; 64]) {
let mut run: u32 = 0;
let mut first = true;
for i in 0..64 {
let lvl = levels[ZIGZAG[i]];
if lvl == 0 {
run += 1;
continue;
}
emit_runlevel(bw, run as u8, lvl, first);
first = false;
run = 0;
}
bw.write_u32(0b10, 2); }
fn emit_runlevel(bw: &mut BitWriter, run: u8, level: i32, is_first_inter: bool) {
debug_assert_ne!(level, 0);
let abs = level.unsigned_abs() as u8;
let sign = if level < 0 { 1 } else { 0 };
if run == 0 && abs == 1 {
if is_first_inter {
bw.write_u32(1, 1); } else {
bw.write_u32(0b11, 2); }
bw.write_u32(sign, 1);
return;
}
if let Some((bits, code)) = lookup_tcoeff(run, abs) {
bw.write_u32(code, bits as u32);
bw.write_u32(sign, 1);
return;
}
bw.write_u32(0b0000_01, 6);
bw.write_u32(run as u32 & 0x3F, 6);
let enc = if level < 0 {
(level + 256) as u32
} else {
level as u32
};
bw.write_u32(enc & 0xFF, 8);
}
#[allow(dead_code)]
fn _unused_refs() {
let _ = MBA_STUFFING;
let _ = MTYPE_INTRA_MQUANT;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decoder::{decode_picture_body, pic_to_video_frame, H261Decoder};
use crate::picture::parse_picture_header;
use oxideav_core::bits::BitReader;
use oxideav_core::packet::PacketFlags;
use oxideav_core::Decoder;
use oxideav_core::{CodecId, Frame, Packet, TimeBase};
fn neutral_qcif() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let y = vec![128u8; 176 * 144];
let cb = vec![128u8; 88 * 72];
let cr = vec![128u8; 88 * 72];
(y, cb, cr)
}
fn gradient_qcif() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let w = 176usize;
let h = 144usize;
let mut y = vec![0u8; w * h];
for j in 0..h {
for i in 0..w {
y[j * w + i] = (32 + (i * 192) / w) as u8;
}
}
let cb = vec![128u8; (w / 2) * (h / 2)];
let cr = vec![128u8; (w / 2) * (h / 2)];
(y, cb, cr)
}
fn psnr(a: &[u8], b: &[u8]) -> f64 {
assert_eq!(a.len(), b.len());
if a.is_empty() {
return f64::INFINITY;
}
let mut sse = 0.0f64;
for (x, y) in a.iter().zip(b.iter()) {
let d = *x as f64 - *y as f64;
sse += d * d;
}
let mse = sse / a.len() as f64;
if mse <= 0.0 {
return f64::INFINITY;
}
10.0 * (255.0f64 * 255.0 / mse).log10()
}
fn decode_one(bytes: Vec<u8>) -> oxideav_core::VideoFrame {
let codec_id = CodecId::new(crate::CODEC_ID_STR);
let mut decoder = H261Decoder::new(codec_id);
let pkt = Packet {
stream_index: 0,
data: bytes,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
match decoder.receive_frame().expect("frame") {
Frame::Video(v) => v,
_ => panic!("video"),
}
}
#[test]
fn picture_header_roundtrip() {
let mut bw = BitWriter::new();
write_picture_header(&mut bw, SourceFormat::Qcif, 7);
let bytes = bw.finish();
let mut br = BitReader::new(&bytes);
let hdr = parse_picture_header(&mut br).expect("parse");
assert_eq!(hdr.temporal_reference, 7);
assert_eq!(hdr.source_format, SourceFormat::Qcif);
assert_eq!(hdr.width, 176);
assert_eq!(hdr.height, 144);
}
#[test]
fn gob_header_roundtrip() {
let mut bw = BitWriter::new();
write_gob_header(&mut bw, 3, 8);
let bytes = bw.finish();
let mut br = BitReader::new(&bytes);
let hdr = crate::gob::parse_gob_header(&mut br).expect("parse GOB");
assert_eq!(hdr.gn, 3);
assert_eq!(hdr.gquant, 8);
}
#[test]
fn encode_qcif_grey_roundtrips_through_our_decoder() {
let (y, cb, cr) = neutral_qcif();
let bytes = encode_intra_picture(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 0)
.expect("encode");
assert!(!bytes.is_empty());
let vf = decode_one(bytes);
assert_eq!(vf.planes[0].stride, 176);
assert_eq!(vf.planes[0].data.len(), 176 * 144);
let y_plane = &vf.planes[0].data;
let mut max_err = 0i32;
for &p in y_plane {
max_err = max_err.max((p as i32 - 128).abs());
}
assert!(max_err <= 2, "max Y error was {max_err}");
for &p in &vf.planes[1].data {
assert!((p as i32 - 128).abs() <= 2);
}
for &p in &vf.planes[2].data {
assert!((p as i32 - 128).abs() <= 2);
}
}
#[test]
fn encode_cif_grey_roundtrips() {
let y = vec![128u8; 352 * 288];
let cb = vec![128u8; 176 * 144];
let cr = vec![128u8; 176 * 144];
let bytes = encode_intra_picture(SourceFormat::Cif, &y, 352, &cb, 176, &cr, 176, 8, 0)
.expect("encode cif");
assert!(!bytes.is_empty());
let mut br = BitReader::new(&bytes);
let hdr = parse_picture_header(&mut br).expect("pic header");
let pic = decode_picture_body(&mut br, &hdr, &bytes, None).expect("body");
let vf = pic_to_video_frame(&pic, Some(0), TimeBase::new(1, 30_000));
assert_eq!(vf.planes[0].stride, 352);
assert_eq!(vf.planes[0].data.len(), 352 * 288);
for &p in &vf.planes[0].data {
assert!((p as i32 - 128).abs() <= 2, "Y pel {p} too far from 128");
}
}
#[test]
fn encode_qcif_gradient_plausible_decode() {
let (y, cb, cr) = gradient_qcif();
let bytes = encode_intra_picture(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 0)
.expect("encode gradient");
let vf = decode_one(bytes);
let yp = &vf.planes[0].data;
let w = 176usize;
let sample = |x: usize, yy: usize| yp[yy * w + x] as i32;
let expected = |x: usize| 32 + (x * 192) as i32 / w as i32;
for &x in &[24usize, 80, 152] {
let got = sample(x, 72);
let want = expected(x);
assert!(
(got - want).abs() <= 40,
"gradient at x={x}: got {got}, want ~{want}"
);
}
}
#[test]
fn intra_local_recon_matches_decoder() {
let (y, cb, cr) = gradient_qcif();
let (bytes, recon) =
encode_intra_picture_with_recon(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 0)
.expect("encode");
let vf = decode_one(bytes);
let dy = &vf.planes[0].data;
let dcb = &vf.planes[1].data;
let dcr = &vf.planes[2].data;
let w = 176usize;
let h = 144usize;
let mut diff_count = 0;
for j in 0..h {
for i in 0..w {
let a = recon.y[j * recon.y_stride + i] as i32;
let b = dy[j * w + i] as i32;
if a != b {
diff_count += 1;
}
}
}
assert_eq!(diff_count, 0, "intra recon Y mismatch in {diff_count} pels");
for j in 0..(h / 2) {
for i in 0..(w / 2) {
let a = recon.cb[j * recon.c_stride + i];
let b = dcb[j * (w / 2) + i];
assert_eq!(a, b, "Cb mismatch at ({i},{j})");
let a2 = recon.cr[j * recon.c_stride + i];
let b2 = dcr[j * (w / 2) + i];
assert_eq!(a2, b2, "Cr mismatch at ({i},{j})");
}
}
}
#[test]
fn sequence_ipi_qcif_roundtrip() {
let (y0, cb0, cr0) = gradient_qcif();
let (y1, cb1, cr1) = (y0.clone(), cb0.clone(), cr0.clone());
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let pkt0 = enc
.encode_frame(&y0, 176, &cb0, 88, &cr0, 88)
.expect("frame 0");
let pkt1 = enc
.encode_frame(&y1, 176, &cb1, 88, &cr1, 88)
.expect("frame 1");
let mut stream = Vec::new();
stream.extend_from_slice(&pkt0);
stream.extend_from_slice(&pkt1);
let codec_id = CodecId::new(crate::CODEC_ID_STR);
let mut decoder = H261Decoder::new(codec_id);
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
let f0 = match decoder.receive_frame().expect("f0") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let f1 = match decoder.receive_frame().expect("f1") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let y_size = 176 * 144;
let p0 = psnr(&f0.planes[0].data, &y0);
let p1 = psnr(&f1.planes[0].data, &y1);
assert!(p0 >= 28.0, "I-frame Y PSNR too low: {p0:.2} dB");
assert!(p1 >= 28.0, "P-frame Y PSNR too low: {p1:.2} dB");
assert!(
pkt1.len() < pkt0.len() / 2,
"expected P-frame ({}) to be much smaller than I-frame ({})",
pkt1.len(),
pkt0.len()
);
let _ = y_size;
}
#[test]
fn inter_picture_self_predict_is_mostly_skips() {
let (y, cb, cr) = gradient_qcif();
let (_iframe, recon) =
encode_intra_picture_with_recon(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 0)
.expect("intra");
let (pframe, _new_recon) =
encode_inter_picture(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 1, &recon)
.expect("inter");
assert!(
pframe.len() < 100,
"self-predict P-frame too big: {} bytes",
pframe.len()
);
}
fn pattern_qcif(shift_x: i32, shift_y: i32) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let w = 176usize;
let h = 144usize;
let mut y = vec![0u8; w * h];
for j in 0..h {
for i in 0..w {
let xi = (i as i32 - shift_x).rem_euclid(w as i32);
let yi = (j as i32 - shift_y).rem_euclid(h as i32);
let stripes = if (xi / 8) % 2 == 0 { 60 } else { 200 };
let diag = ((xi + yi) % 32) as i32;
let v = (stripes + diag).clamp(0, 255);
y[j * w + i] = v as u8;
}
}
let cb = vec![128u8; (w / 2) * (h / 2)];
let cr = vec![128u8; (w / 2) * (h / 2)];
(y, cb, cr)
}
#[test]
fn motion_compensation_beats_zero_mv() {
let (y0, cb, cr) = pattern_qcif(0, 0);
let (y1, _, _) = pattern_qcif(5, 0);
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let _ = enc.encode_frame(&y0, 176, &cb, 88, &cr, 88).expect("f0");
let p1 = enc.encode_frame(&y1, 176, &cb, 88, &cr, 88).expect("f1");
let (i_only, _) =
encode_intra_picture_with_recon(SourceFormat::Qcif, &y1, 176, &cb, 88, &cr, 88, 8, 1)
.expect("intra");
assert!(
p1.len() < i_only.len() / 2,
"MC P-frame ({}) should be at most half the I-frame size ({})",
p1.len(),
i_only.len()
);
}
#[test]
fn me_finds_known_translation() {
let w = 176usize;
let h = 144usize;
let mut src = vec![0u8; w * h];
for j in 0..h {
for i in 0..w {
let v = ((i.wrapping_mul(73) ^ j.wrapping_mul(151)) & 0xFF) as u8;
src[j * w + i] = v;
}
}
let mut reference = Picture::new(w, h);
for j in 0..h {
for i in 0..w {
let si = (i as i32 - 3).clamp(0, w as i32 - 1) as usize;
let sj = (j as i32 - 1).clamp(0, h as i32 - 1) as usize;
reference.y[j * reference.y_stride + i] = src[sj * w + si];
}
}
let (mvx, mvy) = motion_estimate_luma(&src, w, &reference, 32, 32);
assert_eq!((mvx, mvy), (3, 1), "ME picked ({mvx},{mvy})");
}
#[test]
fn inter_local_recon_matches_decoder_with_motion() {
let (y0, cb0, cr0) = pattern_qcif(0, 0);
let (y1, _, _) = pattern_qcif(4, 2);
let (i_bytes, recon_i) =
encode_intra_picture_with_recon(SourceFormat::Qcif, &y0, 176, &cb0, 88, &cr0, 88, 8, 0)
.expect("intra");
let (p_bytes, recon_p) = encode_inter_picture(
SourceFormat::Qcif,
&y1,
176,
&cb0,
88,
&cr0,
88,
8,
1,
&recon_i,
)
.expect("inter");
let mut stream = Vec::new();
stream.extend_from_slice(&i_bytes);
stream.extend_from_slice(&p_bytes);
let mut decoder = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
let _f0 = decoder.receive_frame().expect("f0");
let f1 = match decoder.receive_frame().expect("f1") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let dy = &f1.planes[0].data;
let w = 176usize;
let h = 144usize;
let mut bad = 0usize;
for j in 0..h {
for i in 0..w {
let a = recon_p.y[j * recon_p.y_stride + i];
let b = dy[j * w + i];
if a != b {
bad += 1;
}
}
}
assert_eq!(bad, 0, "P recon Y mismatch in {bad} pels");
}
#[test]
fn translating_sequence_decodes_through_our_decoder() {
let frames: Vec<_> = (0..4)
.map(|f| {
let w = 176usize;
let h = 144usize;
let mut y = vec![0u8; w * h];
let shift = f as i32 * 2;
for j in 0..h {
for i in 0..w {
let xi = (i as i32 - shift).rem_euclid(w as i32);
let stripe = if (xi / 8) % 2 == 0 { 60 } else { 200 };
let diag = ((xi + j as i32) % 32) as i32;
y[j * w + i] = (stripe + diag).clamp(0, 255) as u8;
}
}
let cb = vec![128u8; (w / 2) * (h / 2)];
let cr = vec![128u8; (w / 2) * (h / 2)];
(y, cb, cr)
})
.collect();
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let mut stream = Vec::new();
let mut sizes = Vec::new();
for (y, cb, cr) in &frames {
let p = enc.encode_frame(y, 176, cb, 88, cr, 88).expect("enc");
sizes.push(p.len());
stream.extend_from_slice(&p);
}
let mut decoder = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
let mut psnrs = Vec::new();
for (y, _, _) in &frames {
let f = match decoder.receive_frame().expect("frame") {
Frame::Video(v) => v,
_ => panic!("video"),
};
psnrs.push(psnr(y, &f.planes[0].data));
}
for (i, p) in psnrs.iter().enumerate() {
assert!(
*p >= 27.0,
"frame {i}: local PSNR {p:.2} dB too low (PSNRs={psnrs:?} sizes={sizes:?})"
);
}
}
#[test]
fn motion_estimation_finds_translation() {
let (y0, cb, cr) = pattern_qcif(0, 0);
let (y1, _, _) = pattern_qcif(4, 2);
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let pkt0 = enc.encode_frame(&y0, 176, &cb, 88, &cr, 88).expect("f0");
let pkt1 = enc.encode_frame(&y1, 176, &cb, 88, &cr, 88).expect("f1");
let mut stream = Vec::new();
stream.extend_from_slice(&pkt0);
stream.extend_from_slice(&pkt1);
let mut decoder = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
let _f0 = decoder.receive_frame().expect("f0");
let f1 = match decoder.receive_frame().expect("f1") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let p1 = psnr(&f1.planes[0].data, &y1);
assert!(
p1 >= 30.0,
"P-frame Y PSNR with motion too low: {p1:.2} dB ({} bytes)",
pkt1.len()
);
}
#[test]
fn inter_picture_adapts_to_change() {
let (y0, cb0, cr0) = gradient_qcif();
let mut y1 = y0.clone();
for j in 32..64 {
for i in 32..96 {
y1[j * 176 + i] = y1[j * 176 + i].saturating_add(32);
}
}
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let pkt0 = enc.encode_frame(&y0, 176, &cb0, 88, &cr0, 88).expect("f0");
let pkt1 = enc.encode_frame(&y1, 176, &cb0, 88, &cr0, 88).expect("f1");
let mut stream = Vec::new();
stream.extend_from_slice(&pkt0);
stream.extend_from_slice(&pkt1);
let mut decoder = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
let _f0 = decoder.receive_frame().expect("f0");
let f1 = match decoder.receive_frame().expect("f1") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let p1 = psnr(&f1.planes[0].data, &y1);
assert!(p1 >= 26.0, "P-frame adapted Y PSNR too low: {p1:.2} dB");
}
fn testsrc_qcif(n_frames: usize) -> Vec<(Vec<u8>, Vec<u8>, Vec<u8>)> {
let w = 176usize;
let h = 144usize;
let mut out = Vec::with_capacity(n_frames);
for f in 0..n_frames {
let shift = f as i32 * 2;
let mut y = vec![0u8; w * h];
for j in 0..h {
for i in 0..w {
let xi = (i as i32 - shift).rem_euclid(w as i32);
let stripe = if (xi / 4) % 2 == 0 { 60 } else { 196 };
let diag = ((xi + j as i32) % 16) * 2;
let grad = (j * 60) / h;
let v = (stripe + diag + grad as i32).clamp(0, 255);
y[j * w + i] = v as u8;
}
}
let cb = vec![128u8; (w / 2) * (h / 2)];
let cr = vec![128u8; (w / 2) * (h / 2)];
out.push((y, cb, cr));
}
out
}
#[test]
fn fil_self_decode_one_pframe_byte_tight() {
let (y0, cb, cr) = pattern_qcif(0, 0);
let (y1, _, _) = pattern_qcif(4, 2);
let (i_bytes, recon_i) =
encode_intra_picture_with_recon(SourceFormat::Qcif, &y0, 176, &cb, 88, &cr, 88, 8, 0)
.expect("intra");
let (p_bytes, recon_p) = encode_inter_picture(
SourceFormat::Qcif,
&y1,
176,
&cb,
88,
&cr,
88,
8,
1,
&recon_i,
)
.expect("inter");
let mut stream = Vec::new();
stream.extend_from_slice(&i_bytes);
stream.extend_from_slice(&p_bytes);
let mut decoder = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
let _f0 = decoder.receive_frame().expect("f0");
let f1 = match decoder.receive_frame().expect("f1") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let w = 176usize;
let h = 144usize;
let mut bad = 0;
for j in 0..h {
for i in 0..w {
let a = recon_p.y[j * recon_p.y_stride + i];
let b = f1.planes[0].data[j * w + i];
if a != b {
bad += 1;
}
}
}
assert_eq!(bad, 0, "FIL P recon mismatch in {bad} pels");
}
#[test]
#[ignore]
fn bench_testsrc_psnr_no_fil() {
std::env::set_var("OXIDEAV_H261_NO_FIL", "1");
bench_testsrc_psnr_inner("no-FIL");
std::env::remove_var("OXIDEAV_H261_NO_FIL");
}
#[test]
#[ignore]
fn bench_testsrc_psnr() {
bench_testsrc_psnr_inner("FIL");
}
fn bench_testsrc_psnr_inner(tag: &str) {
let raw = match std::fs::read("/tmp/testsrc.yuv") {
Ok(b) => b,
Err(_) => {
eprintln!("no /tmp/testsrc.yuv — generate with ffmpeg first");
return;
}
};
let frame_size = 176 * 144 * 3 / 2;
let n = raw.len() / frame_size;
let mut frames: Vec<(Vec<u8>, Vec<u8>, Vec<u8>)> = Vec::with_capacity(n);
for i in 0..n {
let off = i * frame_size;
let y = raw[off..off + 176 * 144].to_vec();
let cb = raw[off + 176 * 144..off + 176 * 144 + 88 * 72].to_vec();
let cr = raw[off + 176 * 144 + 88 * 72..off + frame_size].to_vec();
frames.push((y, cb, cr));
}
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let mut stream = Vec::new();
for (y, cb, cr) in &frames {
let p = enc.encode_frame(y, 176, cb, 88, cr, 88).unwrap();
stream.extend_from_slice(&p);
}
eprintln!("[{tag}] stream: {} bytes for {} frames", stream.len(), n);
std::fs::write("/tmp/oxide_testsrc.h261", &stream).unwrap();
let _ = std::process::Command::new("/opt/homebrew/bin/ffmpeg")
.args([
"-y",
"-hide_banner",
"-loglevel",
"error",
"-f",
"h261",
"-i",
"/tmp/oxide_testsrc.h261",
"-f",
"rawvideo",
"-pix_fmt",
"yuv420p",
"/tmp/oxide_testsrc_dec.yuv",
])
.status();
let dec = std::fs::read("/tmp/oxide_testsrc_dec.yuv").unwrap();
let dec_n = dec.len() / frame_size;
let mut psnrs = Vec::new();
for i in 0..n.min(dec_n) {
let dec_off = i * frame_size;
psnrs.push(psnr(&frames[i].0, &dec[dec_off..dec_off + 176 * 144]));
}
eprintln!(
"[{tag}] PSNRs: {:?}",
psnrs.iter().map(|p| format!("{p:.2}")).collect::<Vec<_>>()
);
let avg: f64 = psnrs.iter().sum::<f64>() / psnrs.len() as f64;
eprintln!("[{tag}] avg PSNR: {avg:.2} dB");
}
#[test]
fn fil_path_is_exercised_on_high_freq_content() {
let frames = testsrc_qcif(4);
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let mut total_size = 0usize;
for (y, cb, cr) in &frames {
let p = enc.encode_frame(y, 176, cb, 88, cr, 88).expect("enc");
total_size += p.len();
}
assert!(total_size > 0);
}
#[test]
fn mquant_delta_self_decode_byte_tight() {
let frames = testsrc_qcif(4);
let mut local_recons: Vec<Picture> = Vec::with_capacity(frames.len());
let mut stream = Vec::new();
let (b0, r0) = encode_intra_picture_with_recon(
SourceFormat::Qcif,
&frames[0].0,
176,
&frames[0].1,
88,
&frames[0].2,
88,
8,
0,
)
.expect("intra");
stream.extend_from_slice(&b0);
local_recons.push(r0);
for (i, (y, cb, cr)) in frames.iter().enumerate().skip(1) {
let prev = local_recons.last().unwrap();
let (b, r) =
encode_inter_picture(SourceFormat::Qcif, y, 176, cb, 88, cr, 88, 8, i as u8, prev)
.expect("inter");
stream.extend_from_slice(&b);
local_recons.push(r);
}
let mut dec = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
dec.send_packet(&pkt).expect("send");
dec.flush().ok();
for i in 0..frames.len() {
let f = match dec.receive_frame().expect("frame") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let recon = &local_recons[i];
let dy = &f.planes[0].data;
let mut bad_y = 0usize;
for j in 0..144 {
for ix in 0..176 {
if recon.y[j * recon.y_stride + ix] != dy[j * 176 + ix] {
bad_y += 1;
}
}
}
assert_eq!(
bad_y, 0,
"frame {i}: MQUANT-delta decode produced {bad_y} bad Y pels — \
indicates decoder is not honouring MQUANT or encoder/decoder \
disagree on which MB the MQUANT applies to"
);
}
}
#[test]
fn mquant_delta_does_not_grow_stream() {
let frames = testsrc_qcif(4);
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let mut with_mq_size = 0usize;
for (y, cb, cr) in &frames {
let p = enc.encode_frame(y, 176, cb, 88, cr, 88).unwrap();
with_mq_size += p.len();
}
std::env::set_var("OXIDEAV_H261_NO_MQUANT", "1");
let mut enc2 = H261Encoder::new(SourceFormat::Qcif, 8);
let mut no_mq_size = 0usize;
for (y, cb, cr) in &frames {
let p = enc2.encode_frame(y, 176, cb, 88, cr, 88).unwrap();
no_mq_size += p.len();
}
std::env::remove_var("OXIDEAV_H261_NO_MQUANT");
assert!(
with_mq_size <= no_mq_size + 20,
"MQUANT-on bigger than MQUANT-off: {with_mq_size} vs {no_mq_size}"
);
}
#[test]
fn chained_p_self_decode_byte_tight() {
let frames = testsrc_qcif(5);
let mut local_recons: Vec<Picture> = Vec::with_capacity(frames.len());
let mut stream = Vec::new();
let (b0, r0) = encode_intra_picture_with_recon(
SourceFormat::Qcif,
&frames[0].0,
176,
&frames[0].1,
88,
&frames[0].2,
88,
8,
0,
)
.expect("intra");
stream.extend_from_slice(&b0);
local_recons.push(r0);
for (i, (y, cb, cr)) in frames.iter().enumerate().skip(1) {
let prev = local_recons.last().unwrap();
let (b, r) =
encode_inter_picture(SourceFormat::Qcif, y, 176, cb, 88, cr, 88, 8, i as u8, prev)
.expect("inter");
stream.extend_from_slice(&b);
local_recons.push(r);
}
let mut dec = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
dec.send_packet(&pkt).expect("send");
dec.flush().ok();
for i in 0..frames.len() {
let f = match dec.receive_frame().expect("frame") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let recon = &local_recons[i];
let dy = &f.planes[0].data;
let mut bad_y = 0usize;
for j in 0..144 {
for ix in 0..176 {
if recon.y[j * recon.y_stride + ix] != dy[j * 176 + ix] {
bad_y += 1;
}
}
}
assert_eq!(
bad_y, 0,
"frame {i}: Y plane mismatch in {bad_y} pels (chained-P bug pre-r14 \
manifested as ~230 bad pels at GOB 5 MBA=33)"
);
for plane in 1..=2usize {
let stride = recon.c_stride;
let src = if plane == 1 { &recon.cb } else { &recon.cr };
let dst = &f.planes[plane].data;
for j in 0..72 {
for ix in 0..88 {
assert_eq!(
src[j * stride + ix],
dst[j * 88 + ix],
"frame {i}: chroma plane {plane} mismatch at ({ix},{j})"
);
}
}
}
}
}
#[test]
fn fil_lifts_psnr_on_testsrc_qcif() {
let frames = testsrc_qcif(4);
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8);
let mut stream = Vec::new();
for (y, cb, cr) in &frames {
let p = enc.encode_frame(y, 176, cb, 88, cr, 88).expect("enc");
stream.extend_from_slice(&p);
}
let mut decoder = H261Decoder::new(CodecId::new(crate::CODEC_ID_STR));
let pkt = Packet {
stream_index: 0,
data: stream,
pts: Some(0),
dts: Some(0),
duration: None,
time_base: TimeBase::new(1, 30_000),
flags: PacketFlags {
keyframe: true,
..Default::default()
},
};
decoder.send_packet(&pkt).expect("send");
decoder.flush().ok();
let mut psnrs = Vec::new();
for (y, _, _) in &frames {
let f = match decoder.receive_frame().expect("frame") {
Frame::Video(v) => v,
_ => panic!("video"),
};
psnrs.push(psnr(y, &f.planes[0].data));
}
assert!(psnrs[0] >= 30.0, "I-frame PSNR {} too low", psnrs[0]);
let avg_p: f64 = psnrs[1..].iter().copied().sum::<f64>() / (psnrs.len() - 1) as f64;
assert!(
avg_p >= 27.0,
"average P-frame PSNR {avg_p:.2} dB too low (psnrs={psnrs:?})"
);
}
#[test]
fn no_pframe_drift_on_testsrc_qcif() {
let frames = testsrc_qcif(10);
let mut local_recons: Vec<Picture> = Vec::with_capacity(frames.len());
let (b0, r0) = encode_intra_picture_with_recon(
SourceFormat::Qcif,
&frames[0].0,
176,
&frames[0].1,
88,
&frames[0].2,
88,
8,
0,
)
.expect("intra");
local_recons.push(r0);
let _ = b0; for (i, (y, cb, cr)) in frames.iter().enumerate().skip(1) {
let prev = local_recons.last().unwrap();
let (_b, r) = encode_inter_picture(
SourceFormat::Qcif,
y,
176,
cb,
88,
cr,
88,
8,
(i as u8) & 0x1F,
prev,
)
.expect("inter");
local_recons.push(r);
}
let mut psnrs = Vec::new();
for (i, recon) in local_recons.iter().enumerate() {
let src = &frames[i].0;
let mut packed = vec![0u8; 176 * 144];
for j in 0..144 {
packed[j * 176..j * 176 + 176]
.copy_from_slice(&recon.y[j * recon.y_stride..j * recon.y_stride + 176]);
}
psnrs.push(psnr(src, &packed));
}
let i_psnr = psnrs[0];
let min_p: f64 = psnrs[1..].iter().copied().fold(f64::INFINITY, f64::min);
let drop = i_psnr - min_p;
assert!(
drop < 1.5,
"P-frame PSNR drop {drop:.2} dB exceeds 1.5 dB bound — \
possible IDCT-residual drift regression. PSNRs: {psnrs:?}"
);
let slope = (psnrs[9] - psnrs[1]) / 8.0;
assert!(
slope > -0.15,
"P-frame PSNR slope {slope:.3} dB/frame indicates drift. PSNRs: {psnrs:?}"
);
}
#[test]
#[ignore]
fn diag_long_pchain_drift() {
let frames = testsrc_qcif(30);
let mut local_recons: Vec<Picture> = Vec::with_capacity(frames.len());
let mut stream = Vec::new();
let (b0, r0) = encode_intra_picture_with_recon(
SourceFormat::Qcif,
&frames[0].0,
176,
&frames[0].1,
88,
&frames[0].2,
88,
8,
0,
)
.expect("intra");
stream.extend_from_slice(&b0);
local_recons.push(r0);
for (i, (y, cb, cr)) in frames.iter().enumerate().skip(1) {
let prev = local_recons.last().unwrap();
let (b, r) = encode_inter_picture(
SourceFormat::Qcif,
y,
176,
cb,
88,
cr,
88,
8,
(i as u8) & 0x1F,
prev,
)
.expect("inter");
stream.extend_from_slice(&b);
local_recons.push(r);
}
let mut psnrs = Vec::new();
for (i, recon) in local_recons.iter().enumerate() {
let src = &frames[i].0;
let mut packed = vec![0u8; 176 * 144];
for j in 0..144 {
packed[j * 176..j * 176 + 176]
.copy_from_slice(&recon.y[j * recon.y_stride..j * recon.y_stride + 176]);
}
psnrs.push(psnr(src, &packed));
}
eprintln!("[diag] testsrc_qcif 30-frame local-recon Y PSNRs:");
for (i, p) in psnrs.iter().enumerate() {
eprintln!(" frame {i:2}: {p:.2} dB");
}
eprintln!("[diag] stream bytes: {}", stream.len());
let max_p = psnrs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let min_p = psnrs.iter().copied().fold(f64::INFINITY, f64::min);
eprintln!("[diag] PSNR spread: {:.2} dB", max_p - min_p);
}
}