use std::collections::VecDeque;
use oxideav_core::bits::BitWriter;
use oxideav_core::packet::PacketFlags;
use oxideav_core::{
CodecId, CodecParameters, Encoder, Error, Frame, MediaType, Packet, Result, TimeBase,
};
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)> {
encode_inter_picture_forced_update(
fmt,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
quant,
temporal_reference,
reference,
&[],
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_inter_picture_forced_update(
fmt: SourceFormat,
y: &[u8],
y_stride: usize,
cb: &[u8],
cb_stride: usize,
cr: &[u8],
cr_stride: usize,
quant: u32,
temporal_reference: u8,
reference: &Picture,
forced_mbs: &[u32],
) -> 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 (gob_idx, &gn) in fmt.gob_numbers().iter().enumerate() {
write_gob_header(&mut bw, gn, quant);
let (gob_x, gob_y) = gob_origin_luma(fmt, gn);
let mb_base = (gob_idx * 33) as u32;
let mut forced_intra = [false; 33];
for &gm in forced_mbs {
if gm >= mb_base && gm < mb_base + 33 {
forced_intra[(gm - mb_base) as usize] = true;
}
}
encode_gob_inter(
&mut bw,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
gob_x,
gob_y,
quant,
reference,
&mut recon,
&forced_intra,
);
}
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,
forced_update_period: u32,
mb_since_intra: Vec<u32>,
forced_update_cursor: usize,
}
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,
forced_update_period: 132, mb_since_intra: Vec::new(),
forced_update_cursor: 0,
}
}
pub fn with_intra_period(mut self, period: u32) -> Self {
self.intra_period = period;
self
}
pub fn with_forced_update_period(mut self, period: u32) -> Self {
self.forced_update_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 total_mbs = self.fmt.gob_numbers().len() * 33;
if self.mb_since_intra.len() != total_mbs {
self.mb_since_intra = vec![0u32; total_mbs];
self.forced_update_cursor = 0;
}
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;
for c in self.mb_since_intra.iter_mut() {
*c = 0;
}
self.forced_update_cursor = 0;
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 forced = self.compute_forced_update_set(total_mbs);
let reference = self
.reference
.as_ref()
.expect("reference must exist by now");
let result = encode_inter_picture_forced_update(
self.fmt,
y,
y_stride,
cb,
cb_stride,
cr,
cr_stride,
self.quant,
self.next_tr,
reference,
&forced,
)?;
for (i, c) in self.mb_since_intra.iter_mut().enumerate() {
if forced.binary_search(&(i as u32)).is_ok() {
*c = 0;
} else {
*c = c.saturating_add(1);
}
}
result
};
self.reference = Some(recon);
self.next_tr = self.next_tr.wrapping_add(1) & 0x1F;
Ok(bytes)
}
fn compute_forced_update_set(&mut self, total_mbs: usize) -> Vec<u32> {
let period = self.forced_update_period;
if period == 0 || total_mbs == 0 {
return Vec::new();
}
let mut forced: Vec<u32> = Vec::new();
for (i, &c) in self.mb_since_intra.iter().enumerate() {
if c + 1 >= period {
forced.push(i as u32);
}
}
let per_frame = total_mbs.div_ceil(period as usize).max(1);
for _ in 0..per_frame {
forced.push(self.forced_update_cursor as u32);
self.forced_update_cursor = (self.forced_update_cursor + 1) % total_mbs;
}
forced.sort_unstable();
forced.dedup();
forced
}
}
#[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(())
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Ptype {
pub split_screen: bool,
pub document_camera: bool,
pub freeze_picture_release: bool,
}
pub fn write_picture_header(bw: &mut BitWriter, fmt: SourceFormat, tr: u8) {
write_picture_header_full(bw, fmt, tr, true);
}
pub fn write_picture_header_full(bw: &mut BitWriter, fmt: SourceFormat, tr: u8, hi_res_off: bool) {
write_picture_header_ptype(bw, fmt, tr, hi_res_off, Ptype::default());
}
pub fn write_picture_header_ptype(
bw: &mut BitWriter,
fmt: SourceFormat,
tr: u8,
hi_res_off: bool,
ptype: Ptype,
) {
bw.write_u32(0x00010, 20); bw.write_u32(tr as u32, 5); bw.write_u32(ptype.split_screen as u32, 1);
bw.write_u32(ptype.document_camera as u32, 1);
bw.write_u32(ptype.freeze_picture_release as u32, 1);
let fmt_bit = match fmt {
SourceFormat::Qcif => 0,
SourceFormat::Cif => 1,
};
bw.write_u32(fmt_bit, 1);
bw.write_u32(if hi_res_off { 1 } else { 0 }, 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,
forced_intra: &[bool; 33],
) {
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]);
if forced_intra[(mba - 1) as usize] {
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);
encode_intra_mb_blocks(
bw, y, y_stride, cb, cb_stride, cr, cr_stride, luma_x, luma_y, quant, recon,
);
prev_mba = mba;
pred_mv = (0, 0);
prev_was_mc = false;
let bits_after_mb = bw.bit_position();
rc.account(bits_after_mb - bits_before_mb);
continue;
}
let (mvx, mvy) = 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 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 !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 !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 && 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 && 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 mv_cost = |mvx: i32, mvy: i32| -> u32 { ((mvx.abs() + mvy.abs()) as u32) * 2 };
let cost_at = |mvx: i32, mvy: i32| -> u32 {
sad16x16(src, src_stride, reference, sx, sy, mvx, mvy).saturating_add(mv_cost(mvx, mvy))
};
let mut best_mv = (0i32, 0i32);
let mut best_cost = cost_at(0, 0);
if best_cost == 0 {
return (0, 0);
}
let mut no_improve_rings: u32 = 0;
for r in 1i32..=ME_SEARCH_RADIUS {
let ring_start_cost = best_cost;
for dx in -r..=r {
for &dy in &[-r, r] {
if dx.abs() <= ME_SEARCH_RADIUS && dy.abs() <= ME_SEARCH_RADIUS {
let c = cost_at(dx, dy);
if c < best_cost {
best_cost = c;
best_mv = (dx, dy);
}
}
}
}
for dy in -(r - 1)..=(r - 1) {
for &dx in &[-r, r] {
if dx.abs() <= ME_SEARCH_RADIUS && dy.abs() <= ME_SEARCH_RADIUS {
let c = cost_at(dx, dy);
if c < best_cost {
best_cost = c;
best_mv = (dx, dy);
}
}
}
}
if best_cost < ring_start_cost {
no_improve_rings = 0;
} else {
no_improve_rings += 1;
if no_improve_rings >= 2 {
break; }
}
}
let (wx, wy) = best_mv;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
if dx == 0 && dy == 0 {
continue;
}
let nx = (wx + dx).clamp(-ME_SEARCH_RADIUS, ME_SEARCH_RADIUS);
let ny = (wy + dy).clamp(-ME_SEARCH_RADIUS, ME_SEARCH_RADIUS);
let c = cost_at(nx, ny);
if c < best_cost {
best_cost = c;
best_mv = (nx, ny);
}
}
}
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);
}
fn quant_from_bit_rate(bit_rate_bps: u64, fmt: SourceFormat) -> u32 {
if bit_rate_bps == 0 {
return DEFAULT_QUANT;
}
let bits_per_frame_at_q1: u64 = match fmt {
SourceFormat::Qcif => 20_000,
SourceFormat::Cif => 80_000,
};
let fps = 30u64;
let bits_per_frame = bit_rate_bps / fps;
if bits_per_frame == 0 {
return 31;
}
let q = bits_per_frame_at_q1 / bits_per_frame;
q.clamp(1, 31) as u32
}
pub struct H261RegistryEncoder {
inner: H261Encoder,
output_params: CodecParameters,
time_base: TimeBase,
frame_index: u64,
intra_period: u32,
pending: VecDeque<Packet>,
eof: bool,
}
impl H261RegistryEncoder {
fn new(inner: H261Encoder, output_params: CodecParameters, intra_period: u32) -> Self {
Self {
inner,
output_params,
time_base: TimeBase::new(1, 30_000),
frame_index: 0,
intra_period,
pending: VecDeque::new(),
eof: false,
}
}
}
impl Encoder for H261RegistryEncoder {
fn codec_id(&self) -> &CodecId {
&self.output_params.codec_id
}
fn output_params(&self) -> &CodecParameters {
&self.output_params
}
fn send_frame(&mut self, frame: &Frame) -> Result<()> {
if self.eof {
return Err(Error::invalid("h261 encoder: send_frame after flush"));
}
let vf = match frame {
Frame::Video(v) => v,
_ => {
return Err(Error::invalid(
"h261 encoder: expected VideoFrame, got audio",
))
}
};
if vf.planes.len() < 3 {
return Err(Error::invalid(format!(
"h261 encoder: need 3 YUV planes, got {}",
vf.planes.len()
)));
}
let y = &vf.planes[0].data;
let y_stride = vf.planes[0].stride;
let cb = &vf.planes[1].data;
let cb_stride = vf.planes[1].stride;
let cr = &vf.planes[2].data;
let cr_stride = vf.planes[2].stride;
let data = self
.inner
.encode_frame(y, y_stride, cb, cb_stride, cr, cr_stride)?;
let is_keyframe = self.frame_index == 0
|| (self.intra_period != 0 && (self.frame_index % self.intra_period as u64) == 0);
let pts = vf.pts.unwrap_or(self.frame_index as i64);
let pkt = Packet {
stream_index: 0,
time_base: self.time_base,
pts: Some(pts),
dts: Some(pts),
duration: Some(1),
flags: PacketFlags {
keyframe: is_keyframe,
..Default::default()
},
data,
};
self.pending.push_back(pkt);
self.frame_index += 1;
Ok(())
}
fn receive_packet(&mut self) -> Result<Packet> {
self.pending.pop_front().ok_or(Error::NeedMore)
}
fn flush(&mut self) -> Result<()> {
self.eof = true;
Ok(())
}
}
pub fn make_encoder(params: &CodecParameters) -> Result<Box<dyn Encoder>> {
let fmt = match (params.width, params.height) {
(Some(352), Some(288)) => SourceFormat::Cif,
(Some(176), Some(144)) => SourceFormat::Qcif,
(None, None) => SourceFormat::Qcif,
(w, h) => {
return Err(Error::unsupported(format!(
"h261 encoder: unsupported dimensions {}x{}; H.261 supports QCIF (176x144) and CIF (352x288)",
w.unwrap_or(0), h.unwrap_or(0)
)))
}
};
let bit_rate = params.bit_rate.unwrap_or(0);
let quant = quant_from_bit_rate(bit_rate, fmt).clamp(1, 31);
let mut out_params = params.clone();
out_params.media_type = MediaType::Video;
let (w, h) = fmt.dimensions();
out_params.width = Some(w);
out_params.height = Some(h);
out_params.bit_rate = if bit_rate > 0 { Some(bit_rate) } else { None };
let intra_period = 30u32; let inner = H261Encoder::new(fmt, quant);
Ok(Box::new(H261RegistryEncoder::new(
inner,
out_params,
intra_period,
)))
}
#[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 picture_header_default_ptype_flags_all_off() {
let mut bw = BitWriter::new();
write_picture_header(&mut bw, SourceFormat::Cif, 3);
let bytes = bw.finish();
let mut br = BitReader::new(&bytes);
let hdr = parse_picture_header(&mut br).expect("parse");
assert!(!hdr.split_screen);
assert!(!hdr.document_camera);
assert!(!hdr.freeze_release);
assert!(hdr.hi_res_off);
assert_eq!(hdr.source_format, SourceFormat::Cif);
}
#[test]
fn picture_header_ptype_each_flag_roundtrips() {
let cases = [
Ptype {
split_screen: true,
..Ptype::default()
},
Ptype {
document_camera: true,
..Ptype::default()
},
Ptype {
freeze_picture_release: true,
..Ptype::default()
},
];
for ptype in cases {
let mut bw = BitWriter::new();
write_picture_header_ptype(&mut bw, SourceFormat::Qcif, 5, true, ptype);
let bytes = bw.finish();
let mut br = BitReader::new(&bytes);
let hdr = parse_picture_header(&mut br).expect("parse");
assert_eq!(hdr.split_screen, ptype.split_screen);
assert_eq!(hdr.document_camera, ptype.document_camera);
assert_eq!(hdr.freeze_release, ptype.freeze_picture_release);
assert_eq!(hdr.temporal_reference, 5);
assert_eq!(hdr.source_format, SourceFormat::Qcif);
assert!(hdr.hi_res_off);
}
}
#[test]
fn picture_header_ptype_all_flags_set_roundtrips() {
let ptype = Ptype {
split_screen: true,
document_camera: true,
freeze_picture_release: true,
};
let mut bw = BitWriter::new();
write_picture_header_ptype(&mut bw, SourceFormat::Cif, 2, false, ptype);
let bytes = bw.finish();
let mut br = BitReader::new(&bytes);
let hdr = parse_picture_header(&mut br).expect("parse");
assert!(hdr.split_screen);
assert!(hdr.document_camera);
assert!(hdr.freeze_release);
assert!(!hdr.hi_res_off);
assert_eq!(hdr.source_format, SourceFormat::Cif);
}
#[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]
fn make_encoder_derives_quant_from_bit_rate() {
use oxideav_core::{CodecId as CoreCodecId, CodecParameters, VideoFrame, VideoPlane};
let mut params = CodecParameters::video(CoreCodecId::new(crate::CODEC_ID_STR));
params.width = Some(176);
params.height = Some(144);
params.bit_rate = Some(64_000);
let mut enc = make_encoder(¶ms).expect("make_encoder at 64kbit/s");
let w = 176usize;
let h = 144usize;
let n_frames = 8usize;
let mut stream_bytes = Vec::new();
let mut sources: Vec<Vec<u8>> = Vec::new();
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) as usize;
y[j * w + i] = ((32 + (xi * 180) / w + (j * 40) / h).clamp(0, 255)) as u8;
}
}
let cb = vec![128u8; (w / 2) * (h / 2)];
let cr = vec![128u8; (w / 2) * (h / 2)];
sources.push(y.clone());
let vf = VideoFrame {
pts: Some(f as i64),
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: w / 2,
data: cb,
},
VideoPlane {
stride: w / 2,
data: cr,
},
],
};
enc.send_frame(&Frame::Video(vf)).expect("send_frame");
let pkt = enc.receive_packet().expect("receive_packet");
stream_bytes.extend_from_slice(&pkt.data);
}
let total_bits = stream_bytes.len() * 8;
let bitrate_bps = (total_bits * 30) / n_frames; eprintln!(
"[make_encoder 64k] stream: {} bytes / {} frames = {} bps",
stream_bytes.len(),
n_frames,
bitrate_bps
);
let codec_id = CodecId::new(crate::CODEC_ID_STR);
let mut decoder = H261Decoder::new(codec_id);
let pkt = Packet {
stream_index: 0,
data: stream_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();
let mut frame_psnrs: Vec<f64> = Vec::new();
for (i, src_y) in sources.iter().enumerate() {
match decoder.receive_frame() {
Ok(Frame::Video(vf)) => {
let p = psnr(src_y, &vf.planes[0].data);
eprintln!("[make_encoder 64k] frame {i}: PSNR_Y = {p:.2} dB");
frame_psnrs.push(p);
}
_ => break,
}
}
let avg = if frame_psnrs.is_empty() {
0.0
} else {
frame_psnrs.iter().sum::<f64>() / frame_psnrs.len() as f64
};
eprintln!("[make_encoder 64k] avg PSNR_Y = {avg:.2} dB");
assert!(
avg >= 35.0,
"make_encoder at 64kbit/s: avg PSNR_Y {avg:.2} dB < 35.0 dB"
);
}
#[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);
}
#[test]
fn forced_update_intra_mb_decodes_and_refreshes() {
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 forced: &[u32] = &[0, 1, 17, 50, 98];
let (pframe, _r) = encode_inter_picture_forced_update(
SourceFormat::Qcif,
&y,
176,
&cb,
88,
&cr,
88,
8,
1,
&recon,
forced,
)
.expect("inter forced");
let mut stream = Vec::new();
let iframe =
encode_intra_picture(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 0).unwrap();
stream.extend_from_slice(&iframe);
stream.extend_from_slice(&pframe);
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 = decoder.receive_frame().expect("f0");
let f1 = match decoder.receive_frame().expect("f1") {
Frame::Video(v) => v,
_ => panic!("video"),
};
let p = psnr(&f1.planes[0].data, &y);
assert!(p >= 28.0, "forced-update P-frame Y PSNR too low: {p:.2} dB");
}
#[test]
fn forced_update_empty_set_matches_plain_inter() {
let (y, cb, cr) = gradient_qcif();
let (_i, recon) =
encode_intra_picture_with_recon(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 0)
.unwrap();
let (plain, _r1) =
encode_inter_picture(SourceFormat::Qcif, &y, 176, &cb, 88, &cr, 88, 8, 1, &recon)
.unwrap();
let (forced_empty, _r2) = encode_inter_picture_forced_update(
SourceFormat::Qcif,
&y,
176,
&cb,
88,
&cr,
88,
8,
1,
&recon,
&[],
)
.unwrap();
let (forced_oob, _r3) = encode_inter_picture_forced_update(
SourceFormat::Qcif,
&y,
176,
&cb,
88,
&cr,
88,
8,
1,
&recon,
&[99, 100, 1_000_000],
)
.unwrap();
assert_eq!(plain, forced_empty);
assert_eq!(plain, forced_oob);
}
#[test]
fn forced_update_scheduler_covers_every_mb_within_period() {
let period = 16u32; let mut enc = H261Encoder::new(SourceFormat::Qcif, 8)
.with_intra_period(0)
.with_forced_update_period(period);
let total_mbs = SourceFormat::Qcif.gob_numbers().len() * 33; enc.mb_since_intra = vec![0u32; total_mbs];
let mut ever_forced = vec![false; total_mbs];
let mut max_count = 0u32;
for _frame in 0..period {
let forced = enc.compute_forced_update_set(total_mbs);
for &m in &forced {
ever_forced[m as usize] = true;
}
for (i, c) in enc.mb_since_intra.iter_mut().enumerate() {
if forced.binary_search(&(i as u32)).is_ok() {
*c = 0;
} else {
*c = c.saturating_add(1);
}
}
max_count = max_count.max(enc.mb_since_intra.iter().copied().max().unwrap());
}
assert!(
ever_forced.iter().all(|&b| b),
"some MB was never forcibly updated within {period} frames"
);
assert!(
max_count < period,
"an MB counter reached {max_count} (>= period {period})"
);
}
#[test]
fn forced_update_period_zero_disables() {
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8)
.with_intra_period(0)
.with_forced_update_period(0);
let total_mbs = SourceFormat::Qcif.gob_numbers().len() * 33;
enc.mb_since_intra = vec![0u32; total_mbs];
let forced = enc.compute_forced_update_set(total_mbs);
assert!(forced.is_empty());
}
#[test]
fn forced_update_sequence_stays_healthy() {
let period = 8u32;
let (y, cb, cr) = gradient_qcif();
let mut enc = H261Encoder::new(SourceFormat::Qcif, 8)
.with_intra_period(0)
.with_forced_update_period(period);
let frames = (2 * period) as usize;
let mut stream = Vec::new();
for _ in 0..frames {
let b = enc.encode_frame(&y, 176, &cb, 88, &cr, 88).expect("frame");
stream.extend_from_slice(&b);
}
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 mut last = None;
for _ in 0..frames {
match decoder.receive_frame() {
Ok(Frame::Video(v)) => last = Some(v),
Ok(_) => {}
Err(_) => break,
}
}
let last = last.expect("at least one decoded frame");
let p = psnr(&last.planes[0].data, &y);
assert!(
p >= 28.0,
"forced-update sequence final Y PSNR too low: {p:.2} dB"
);
}
}