use bytes::Bytes;
use moq_mux::codec::annexb;
use moq_net::Timestamp;
use super::backend::{self, Backend, Codec};
use super::{Config, Kind};
use crate::Frame;
use crate::frame::I420;
const FRAME_MICROS: u64 = 33_333;
enum Expect {
Solid { y: u8, u: u8, v: u8 },
Reference(&'static [u8]),
}
struct Vector {
name: &'static str,
bitstream: &'static [u8],
width: u32,
height: u32,
pictures: usize,
expect: Expect,
}
const IDR_BLUE: Vector = Vector {
name: "idr_64x64_blue_2f.h264",
bitstream: include_bytes!("test_data/idr_64x64_blue_2f.h264"),
width: 64,
height: 64,
pictures: 2,
expect: Expect::Solid { y: 41, u: 240, v: 110 },
};
const SEQ_PATTERN: Vector = Vector {
name: "seq_64x64_pattern_5f.h264",
bitstream: include_bytes!("test_data/seq_64x64_pattern_5f.h264"),
width: 64,
height: 64,
pictures: 5,
expect: Expect::Reference(include_bytes!("test_data/seq_64x64_pattern_5f.yuv")),
};
const MAIN_YELLOW: Vector = Vector {
name: "main_64x64_yellow_5f.h264",
bitstream: include_bytes!("test_data/main_64x64_yellow_5f.h264"),
width: 64,
height: 64,
pictures: 5,
expect: Expect::Solid { y: 210, u: 16, v: 146 },
};
const NON_SQUARE: Vector = Vector {
name: "seq_100x66_pattern_5f.h264",
bitstream: include_bytes!("test_data/seq_100x66_pattern_5f.h264"),
width: 100,
height: 66,
pictures: 5,
expect: Expect::Reference(include_bytes!("test_data/seq_100x66_pattern_5f.yuv")),
};
const VECTORS: &[&Vector] = &[&IDR_BLUE, &SEQ_PATTERN, &MAIN_YELLOW, &NON_SQUARE];
const CANDIDATES: &[&str] = &[
"openh264",
#[cfg(target_os = "macos")]
"videotoolbox",
#[cfg(target_os = "windows")]
"mediafoundation",
#[cfg(target_os = "android")]
"mediacodec",
#[cfg(all(target_os = "linux", feature = "nvidia"))]
"nvdec",
#[cfg(all(target_os = "linux", feature = "vaapi"))]
"vaapi",
#[cfg(all(target_os = "linux", feature = "v4l2"))]
"v4l2",
];
fn open(name: &str) -> Option<Box<dyn Backend>> {
let config = Config {
kind: Kind::Named(name.to_owned()),
..Config::new()
};
backend::open(Codec::H264, &config).ok()
}
fn decoders() -> Vec<&'static str> {
let open: Vec<&'static str> = CANDIDATES.iter().copied().filter(|name| open(name).is_some()).collect();
assert!(open.contains(&"openh264"), "the software H.264 decoder would not open");
open
}
fn nal_type(nal: &Bytes) -> u8 {
nal.first().map_or(0, |b| b & 0x1f)
}
struct AccessUnit {
payload: Bytes,
timestamp: Timestamp,
keyframe: bool,
}
fn access_units(vector: &Vector) -> Vec<AccessUnit> {
let mut buf = Bytes::from_static(vector.bitstream);
let mut iter = annexb::NalIterator::new(&mut buf);
let mut nals: Vec<Bytes> = iter
.by_ref()
.map(|nal| nal.expect("fixture is valid Annex-B"))
.collect();
nals.extend(iter.flush().expect("fixture is valid Annex-B"));
let is_slice = |nal: &Bytes| matches!(nal_type(nal), 1 | 5);
let is_parameter_set = |nal: &Bytes| matches!(nal_type(nal), 7 | 8);
let mut units: Vec<AccessUnit> = Vec::new();
let mut current: Vec<Bytes> = Vec::new();
for nal in nals {
if current.iter().any(is_slice) && (is_slice(&nal) || is_parameter_set(&nal)) {
units.push(access_unit(¤t, units.len()));
current.clear();
}
current.push(nal);
}
if current.iter().any(is_slice) {
units.push(access_unit(¤t, units.len()));
}
assert_eq!(
units.len(),
vector.pictures,
"{} splits into {} access units, expected {}",
vector.name,
units.len(),
vector.pictures
);
units
}
fn access_unit(nals: &[Bytes], index: usize) -> AccessUnit {
AccessUnit {
payload: annexb::build_prefix(nals.iter()),
timestamp: Timestamp::from_micros(index as u64 * FRAME_MICROS).expect("fixture timestamp"),
keyframe: nals.iter().any(|nal| nal_type(nal) == 5),
}
}
#[test]
fn repeated_parameter_sets_start_the_following_access_unit() {
let units = access_units(&IDR_BLUE);
let mut payload = units[1].payload.clone();
let mut iter = annexb::NalIterator::new(&mut payload);
let mut types: Vec<u8> = iter
.by_ref()
.map(|nal| nal_type(&nal.expect("access unit is valid Annex-B")))
.collect();
types.extend(iter.flush().expect("access unit is valid Annex-B").iter().map(nal_type));
assert_eq!(types, [7, 8, 5]);
}
fn decode(name: &str, vector: &Vector) -> Vec<Frame> {
let mut decoder = open(name).expect("backend opened during probing");
let mut decoded = Vec::new();
for unit in access_units(vector) {
let frames = decoder
.decode(unit.payload, unit.timestamp, unit.keyframe)
.unwrap_or_else(|e| panic!("{name} failed to decode {}: {e}", vector.name));
decoded.extend(frames);
}
decoded
}
fn pictures(name: &str, vector: &Vector) -> Vec<I420> {
let decoded = decode(name, vector);
assert!(!decoded.is_empty(), "{name} decoded no pictures from {}", vector.name);
assert!(
decoded.len() <= vector.pictures && vector.pictures - decoded.len() <= 1,
"{name} returned {} pictures from {}, which codes {}",
decoded.len(),
vector.name,
vector.pictures
);
decoded
.into_iter()
.enumerate()
.map(|(i, frame)| {
assert_eq!(
frame.timestamp.as_micros(),
i as u128 * FRAME_MICROS as u128,
"{name}: {} picture {i} lost its timestamp",
vector.name
);
let i420 = frame
.surface
.to_i420()
.unwrap_or_else(|e| panic!("{name}: {} picture {i} would not download: {e}", vector.name))
.into_owned();
assert_eq!(
(i420.width(), i420.height()),
(vector.width, vector.height),
"{name}: {} picture {i} is the wrong size",
vector.name
);
i420
})
.collect()
}
fn plane_diff(a: &[u8], b: &[u8]) -> (f64, f64) {
assert_eq!(a.len(), b.len());
let n = a.len() as f64;
let mae = a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() as f64 / n;
let bias = (a.iter().map(|&x| x as i64).sum::<i64>() - b.iter().map(|&x| x as i64).sum::<i64>()) as f64 / n;
(mae, bias)
}
fn describe(got: &I420, want: &I420) -> String {
["Y", "U", "V"]
.into_iter()
.zip([(got.y(), want.y()), (got.u(), want.u()), (got.v(), want.v())])
.map(|(label, (a, b))| {
let (mae, bias) = plane_diff(a, b);
format!("{label}: mae {mae:.2}, bias {bias:+.2}")
})
.collect::<Vec<_>>()
.join("; ")
}
fn check(vector: &Vector) {
for name in decoders() {
for (i, got) in pictures(name, vector).into_iter().enumerate() {
match vector.expect {
Expect::Solid { y, u, v } => {
for (label, plane, value) in [("Y", got.y(), y), ("U", got.u(), u), ("V", got.v(), v)] {
if let Some((at, &sample)) = plane.iter().enumerate().find(|&(_, &b)| b != value) {
panic!(
"{name}: {} picture {i} {label} sample {at} is {sample}, expected {value}",
vector.name
);
}
}
}
Expect::Reference(reference) => {
let len = I420::len(vector.width, vector.height);
assert_eq!(
reference.len(),
len * vector.pictures,
"the reference decode for {} is not {} pictures of {}x{}",
vector.name,
vector.pictures,
vector.width,
vector.height
);
let want = I420::new(vector.width, vector.height, reference[i * len..(i + 1) * len].to_vec())
.expect("reference picture");
assert!(
got.data() == want.data(),
"{name}: {} picture {i} differs from the reference decode ({})",
vector.name,
describe(&got, &want)
);
}
}
}
}
}
#[test]
fn idr_only_baseline() {
check(&IDR_BLUE);
}
#[test]
fn multi_frame_baseline() {
check(&SEQ_PATTERN);
}
#[test]
fn main_profile_cabac() {
check(&MAIN_YELLOW);
}
#[test]
fn non_square_cropped() {
check(&NON_SQUARE);
}
#[test]
fn backends_agree() {
let backends = decoders();
if backends.len() < 2 {
return;
}
let (anchor, others) = backends.split_first().expect("at least two backends");
for vector in VECTORS {
let reference = pictures(anchor, vector);
for name in others {
for (i, (got, want)) in pictures(name, vector).iter().zip(&reference).enumerate() {
assert!(
got.data() == want.data(),
"{name} and {anchor} disagree on {} picture {i} ({})",
vector.name,
describe(got, want)
);
}
}
}
}