#[allow(unused_imports)]
use alloc::vec;
#[allow(unused_imports)]
use alloc::vec::Vec;
use crate::config::EncoderConfig;
use crate::RefFrame;
use rusty_h264_common::inter::mc_luma;
use rusty_h264_common::transform::hadamard_4x4;
use rusty_h264_common::{YuvFrame, YuvPlanes};
fn satd4(res: &[i32; 16]) -> i64 {
hadamard_4x4(res)
.iter()
.map(|&v| v.unsigned_abs() as i64)
.sum()
}
fn coded_luma(cfg: &EncoderConfig, frame: &YuvPlanes<'_>) -> (Vec<u8>, usize, usize) {
let (cw, ch) = (cfg.mb_width() * 16, cfg.mb_height() * 16);
let (w, h) = (frame.width, frame.height);
let mut y = vec![0u8; cw * ch];
let wc = w.min(cw);
for j in 0..ch {
let src = &frame.y[j.min(h - 1) * w..][..w];
let dst = &mut y[j * cw..][..cw];
dst[..wc].copy_from_slice(&src[..wc]);
dst[wc..].fill(src[w - 1]);
}
(y, cw, ch)
}
pub fn complexity(cfg: &EncoderConfig, frame: &YuvPlanes<'_>, reference: Option<&RefFrame>) -> f64 {
let (sy, cw, ch) = coded_luma(cfg, frame);
let (mb_w, mb_h) = (cfg.mb_width(), cfg.mb_height());
let mut total = 0i64;
for mb_y in 0..mb_h {
for mb_x in 0..mb_w {
total += match reference {
None => intra_activity(&sy, cw, mb_x, mb_y),
Some(r) => inter_activity(&sy, cw, ch, &r.y, mb_x, mb_y),
};
}
}
(total as f64).max(1.0)
}
fn intra_activity(sy: &[u8], cw: usize, mb_x: usize, mb_y: usize) -> i64 {
let mut s = 0;
for by in 0..4 {
for bx in 0..4 {
let mut blk = [0i32; 16];
for dy in 0..4 {
for dx in 0..4 {
blk[dy * 4 + dx] =
sy[(mb_y * 16 + by * 4 + dy) * cw + mb_x * 16 + bx * 4 + dx] as i32;
}
}
let h = hadamard_4x4(&blk);
s += h[1..].iter().map(|&v| v.unsigned_abs() as i64).sum::<i64>();
}
}
s
}
pub(crate) const SCENECUT_SPIKE: f64 = 0.25;
pub(crate) fn is_scene_cut(cfg: &EncoderConfig, r: f64, prev1: f64, prev2: f64) -> bool {
let thresh = 1.0 - (cfg.scenecut.min(100) as f64 / 100.0);
r >= thresh && r >= prev1.min(prev2) + SCENECUT_SPIKE
}
pub(crate) fn all_pair_ratios(cfg: &EncoderConfig, frames: &[YuvFrame]) -> Vec<f64> {
let mut out = Vec::with_capacity(frames.len().saturating_sub(1));
let mut prev = match frames.first() {
Some(f) => crate::mbtree::pair_prep(cfg, &f.as_planes()),
None => return out,
};
for f in &frames[1..] {
let cur = crate::mbtree::pair_prep(cfg, &f.as_planes());
out.push(crate::mbtree::pair_ratio_prepped(cfg, &cur, &prev));
prev = cur;
}
out
}
pub(crate) fn segment_gops(cfg: &EncoderConfig, frames: &[YuvFrame]) -> Vec<usize> {
let keyint = cfg.gop_size.max(1) as usize;
let minki = (cfg.min_keyint.max(1) as usize).min(keyint);
let detect = cfg.scenecut > 0 && keyint > 1 && frames.len() > 1;
let mut scores = vec![f64::NAN; frames.len().saturating_sub(1)];
let mut scored_to = 0usize; let mut prep: Option<(usize, crate::mbtree::PairPrep)> = None; let mut starts = vec![0usize];
let mut last = 0usize;
for i in 1..frames.len() {
let force = i - last >= keyint;
let cut = !force && detect && i - last >= minki && {
if scored_to < i.saturating_sub(3) {
scored_to = i - 3;
prep = None;
}
while scored_to < i {
let p = scored_to;
let prev_prep = match prep.take() {
Some((idx, pp)) if idx == p => pp,
_ => crate::mbtree::pair_prep(cfg, &frames[p].as_planes()),
};
let cur_prep = crate::mbtree::pair_prep(cfg, &frames[p + 1].as_planes());
scores[p] = crate::mbtree::pair_ratio_prepped(cfg, &cur_prep, &prev_prep);
prep = Some((p + 1, cur_prep));
scored_to = p + 1;
}
let r = scores[i - 1];
let prev1 = if i >= 2 { scores[i - 2] } else { 1.0 };
let prev2 = if i >= 3 { scores[i - 3] } else { prev1 };
is_scene_cut(cfg, r, prev1, prev2)
};
if force || cut {
starts.push(i);
last = i;
}
}
starts
}
fn inter_activity(sy: &[u8], cw: usize, ch: usize, ref_y: &[u8], mb_x: usize, mb_y: usize) -> i64 {
const CANDS: [(i32, i32); 9] = [
(0, 0),
(4, 0),
(-4, 0),
(0, 4),
(0, -4),
(8, 0),
(-8, 0),
(0, 8),
(0, -8),
];
let mut best = i64::MAX;
for &(mvx, mvy) in &CANDS {
let mut pred = [0u8; 256];
mc_luma(
ref_y,
cw,
ch,
mb_x * 16,
mb_y * 16,
16,
16,
mvx,
mvy,
&mut pred,
);
let mut s = 0;
for by in 0..4 {
for bx in 0..4 {
let mut res = [0i32; 16];
for dy in 0..4 {
for dx in 0..4 {
res[dy * 4 + dx] =
sy[(mb_y * 16 + by * 4 + dy) * cw + mb_x * 16 + bx * 4 + dx] as i32
- pred[(by * 4 + dy) * 16 + (bx * 4 + dx)] as i32;
}
}
s += satd4(&res);
}
}
best = best.min(s);
}
best
}
#[cfg(test)]
mod scenecut_tests {
use super::*;
#[allow(unused_imports)]
use alloc::{
boxed::Box,
format,
string::{String, ToString},
vec,
vec::Vec,
};
#[allow(unused_imports)]
use rusty_h264_common::once::OnceLock;
fn frame(w: usize, h: usize, style: u8, t: usize) -> YuvFrame {
let mut y = vec![0u8; w * h];
for j in 0..h {
for i in 0..w {
y[j * w + i] = match style {
0 => 60 + (((i + t * 2) * 7 + j * 13) % 120) as u8,
_ => 200u8.wrapping_sub((((i / 8 + j / 8) % 2) as u8) * 140),
};
}
}
YuvFrame {
width: w,
height: h,
y,
u: vec![128; (w / 2) * (h / 2)],
v: vec![128; (w / 2) * (h / 2)],
}
}
#[test]
fn segmentation_cuts_at_the_splice_and_only_there() {
let (w, h) = (96, 80);
let mut cfg = EncoderConfig::new(w, h);
cfg.gop_size = 250;
cfg.min_keyint = 5;
cfg.scenecut = 40;
let spliced: Vec<YuvFrame> = (0..12)
.map(|t| frame(w, h, 0, t))
.chain((0..12).map(|t| frame(w, h, 1, t)))
.collect();
assert_eq!(
segment_gops(&cfg, &spliced),
vec![0, 12],
"cut exactly at the splice"
);
let pan: Vec<YuvFrame> = (0..24).map(|t| frame(w, h, 0, t)).collect();
assert_eq!(
segment_gops(&cfg, &pan),
vec![0],
"no cut on a continuous pan"
);
cfg.min_keyint = 20;
assert_eq!(
segment_gops(&cfg, &spliced),
vec![0],
"min_keyint suppresses the cut"
);
cfg.min_keyint = 5;
cfg.gop_size = 10;
assert_eq!(
segment_gops(&cfg, &pan),
vec![0, 10, 20],
"forced refresh at keyint"
);
cfg.scenecut = 0;
assert_eq!(
segment_gops(&cfg, &spliced),
vec![0, 10, 20],
"scenecut=0 is fixed cadence"
);
}
#[test]
fn streaming_equals_batch_with_a_firing_cut() {
let (w, h) = (96, 80);
let mut cfg = EncoderConfig::new(w, h);
cfg.gop_size = 250;
cfg.min_keyint = 5;
cfg.scenecut = 40;
let spliced: Vec<YuvFrame> = (0..12)
.map(|t| frame(w, h, 0, t))
.chain((0..12).map(|t| frame(w, h, 1, t)))
.collect();
let batch: Vec<u8> = crate::Encoder::new(cfg.clone())
.unwrap()
.encode_all(&spliced)
.unwrap()
.concat();
let mut enc = crate::Encoder::new(cfg).unwrap();
let mut stream = Vec::new();
for f in &spliced {
stream.extend_from_slice(&enc.try_encode(f).unwrap());
}
stream.extend_from_slice(&enc.flush());
assert_eq!(
stream, batch,
"streaming must reproduce the batch bytes under a cut"
);
let idrs = stream
.windows(5)
.filter(|w| w[..4] == [0, 0, 0, 1] && w[4] & 0x1f == 5)
.count();
assert_eq!(
idrs, 2,
"the splice cut must actually fire (start IDR + cut IDR)"
);
}
}