use oxideav_core::{
CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, CodecTag, Decoder,
Encoder, Error as CoreError, Frame as CoreFrame, Packet, PixelFormat, Result as CoreResult,
RuntimeContext, TimeBase, VideoFrame, VideoPlane,
};
use crate::codec_config::Av1CodecConfig;
use crate::decoder::{SpecDecodeSession, SpecFrame};
use crate::encoder::{
encode_key_frame_yuv_with_options, encode_still_yuv, quality_to_base_q_idx, ChromaFormat,
StillOptions, StillSpeed, YuvFrame, DEFAULT_STILL_BASE_Q_IDX,
};
use oxideav_core::ExecutionContext;
pub const CODEC_ID_STR: &str = "av1";
pub fn register_codecs(reg: &mut CodecRegistry) {
let caps = CodecCapabilities::video("av1_sw")
.with_decode()
.with_encode();
reg.register(
CodecInfo::new(CodecId::new(CODEC_ID_STR))
.capabilities(caps)
.decoder(make_decoder)
.encoder(make_encoder)
.tags([CodecTag::fourcc(b"AV01"), CodecTag::matroska("V_AV1")]),
);
}
pub fn register(ctx: &mut RuntimeContext) {
register_codecs(&mut ctx.codecs);
}
pub fn make_decoder(params: &CodecParameters) -> CoreResult<Box<dyn Decoder>> {
let mut session = SpecDecodeSession::new();
let config_obus = codec_config_obus(¶ms.extradata);
if !config_obus.is_empty() {
session
.decode_temporal_unit(config_obus)
.map_err(|e| CoreError::invalid(format!("oxideav-av1: extradata: {e}")))?;
}
Ok(Box::new(Av1Decoder {
codec_id: params.codec_id.clone(),
session,
queue: std::collections::VecDeque::new(),
eof: false,
}))
}
fn codec_config_obus(extradata: &[u8]) -> &[u8] {
if let Some(cfg) = crate::codec_config::Av1CodecConfig::parse(extradata) {
let n = cfg.config_obus.len();
return &extradata[extradata.len() - n..];
}
match extradata.first() {
Some(&b) if b & 0x80 == 0 && matches!((b >> 3) & 0xF, 1 | 2) => extradata,
_ => &[],
}
}
struct Av1Decoder {
codec_id: CodecId,
session: SpecDecodeSession,
queue: std::collections::VecDeque<VideoFrame>,
eof: bool,
}
impl std::fmt::Debug for Av1Decoder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Av1Decoder")
.field("codec_id", &self.codec_id)
.field("queued", &self.queue.len())
.field("eof", &self.eof)
.finish()
}
}
const IVF_MAGIC: &[u8; 4] = b"DKIF";
impl Decoder for Av1Decoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> CoreResult<()> {
let mut push = |frames: Vec<SpecFrame>| {
for frame in frames {
self.queue
.push_back(spec_frame_to_video_frame(frame, packet.pts));
}
};
if packet.data.len() >= 4 && &packet.data[..4] == IVF_MAGIC {
let reader = crate::encoder::ivf::IvfReader::new(&packet.data)
.map_err(|e| CoreError::invalid(format!("oxideav-av1: {e:?}")))?;
let records = reader
.read_all()
.map_err(|e| CoreError::invalid(format!("oxideav-av1: {e:?}")))?;
for record in records {
let frames = self
.session
.decode_temporal_unit(&record.payload)
.map_err(|e| CoreError::invalid(format!("oxideav-av1: {e}")))?;
push(frames);
}
} else {
let frames = self
.session
.decode_temporal_unit(&packet.data)
.map_err(|e| CoreError::invalid(format!("oxideav-av1: {e}")))?;
push(frames);
}
Ok(())
}
fn receive_frame(&mut self) -> CoreResult<CoreFrame> {
match self.queue.pop_front() {
Some(vf) => Ok(CoreFrame::Video(vf)),
None => {
if self.eof {
Err(CoreError::Eof)
} else {
Err(CoreError::NeedMore)
}
}
}
}
fn flush(&mut self) -> CoreResult<()> {
self.eof = true;
Ok(())
}
fn reset(&mut self) -> CoreResult<()> {
self.queue.clear();
self.session.reset_references();
self.eof = false;
Ok(())
}
fn set_execution_context(&mut self, ctx: &ExecutionContext) {
self.session.set_threads(ctx.threads.max(1));
}
}
fn spec_frame_to_video_frame(frame: SpecFrame, pts: Option<i64>) -> VideoFrame {
let bytes_per_sample: usize = if frame.bit_depth > 8 { 2 } else { 1 };
let planes = frame
.planes
.into_iter()
.zip(frame.plane_dims.iter())
.map(|(data, &(w, _h))| VideoPlane {
stride: (w as usize) * bytes_per_sample,
data,
})
.collect();
VideoFrame { pts, planes }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Av1EncoderOptions {
pub still: bool,
pub base_q_idx: u8,
pub speed: StillSpeed,
pub tile_cols_log2: u32,
pub tile_rows_log2: u32,
pub full_range: bool,
pub color_description: Option<(u8, u8, u8)>,
pub threads: Option<usize>,
pub auto_tiles: bool,
}
impl Av1EncoderOptions {
pub fn from_params(params: &CodecParameters) -> CoreResult<Self> {
let mut o = Self {
still: false,
base_q_idx: DEFAULT_STILL_BASE_Q_IDX,
speed: StillSpeed::Fast,
tile_cols_log2: 0,
tile_rows_log2: 0,
full_range: matches!(
params.pixel_format,
Some(PixelFormat::YuvJ420P | PixelFormat::YuvJ422P | PixelFormat::YuvJ444P)
),
color_description: None,
threads: None,
auto_tiles: true,
};
let mut cicp: [Option<u8>; 3] = [None; 3];
let mut explicit_tiles = false;
let bad = |k: &str, v: &str| CoreError::invalid(format!("oxideav-av1: option {k}={v:?}"));
let parse_bool = |k: &str, v: &str| -> CoreResult<bool> {
match v {
"true" | "1" | "yes" => Ok(true),
"false" | "0" | "no" => Ok(false),
_ => Err(bad(k, v)),
}
};
let mut quality: Option<u8> = None;
let mut lossless = false;
for (k, v) in params.options.iter() {
match k {
"still" => o.still = parse_bool(k, v)?,
"q" | "base_q_idx" => o.base_q_idx = v.parse().map_err(|_| bad(k, v))?,
"quality" => {
let q: u8 = v.parse().map_err(|_| bad(k, v))?;
if q > 100 {
return Err(bad(k, v));
}
quality = Some(q);
}
"lossless" => lossless = parse_bool(k, v)?,
"speed" => {
o.speed = match v {
"fast" => StillSpeed::Fast,
"balanced" => StillSpeed::Balanced,
"thorough" => StillSpeed::Thorough,
_ => return Err(bad(k, v)),
}
}
"tile_cols_log2" => {
o.tile_cols_log2 = v.parse().map_err(|_| bad(k, v))?;
explicit_tiles = true;
}
"tile_rows_log2" => {
o.tile_rows_log2 = v.parse().map_err(|_| bad(k, v))?;
explicit_tiles = true;
}
"threads" => {
o.threads = if v == "auto" {
None
} else {
Some(v.parse::<usize>().map_err(|_| bad(k, v))?.max(1))
}
}
"full_range" => o.full_range = parse_bool(k, v)?,
"color_primaries" => cicp[0] = Some(v.parse().map_err(|_| bad(k, v))?),
"transfer_characteristics" => cicp[1] = Some(v.parse().map_err(|_| bad(k, v))?),
"matrix_coefficients" => cicp[2] = Some(v.parse().map_err(|_| bad(k, v))?),
_ => {
return Err(CoreError::invalid(format!(
"oxideav-av1: unknown option {k:?}"
)))
}
}
}
match cicp {
[None, None, None] => {}
[Some(cp), Some(tc), Some(mc)] => o.color_description = Some((cp, tc, mc)),
_ => {
return Err(CoreError::invalid(
"oxideav-av1: color_primaries / transfer_characteristics / \
matrix_coefficients must be given together",
))
}
}
if let Some(q) = quality {
o.base_q_idx = quality_to_base_q_idx(q);
}
if lossless {
o.base_q_idx = 0;
}
o.auto_tiles = !explicit_tiles;
Ok(o)
}
fn still_options(&self, budget: usize) -> StillOptions {
let threads = self.threads.unwrap_or(budget).max(1);
StillOptions {
base_q_idx: self.base_q_idx,
tile_cols_log2: self.tile_cols_log2,
tile_rows_log2: self.tile_rows_log2,
speed: self.speed,
full_range: self.full_range,
reduced_header: true,
color_description: self.color_description,
threads,
auto_tiles: self.auto_tiles && threads > 1,
}
}
}
fn pixel_format_layout(pf: PixelFormat) -> Option<(u8, ChromaFormat)> {
Some(match pf {
PixelFormat::Yuv420P | PixelFormat::YuvJ420P => (8, ChromaFormat::Yuv420),
PixelFormat::Yuv422P | PixelFormat::YuvJ422P => (8, ChromaFormat::Yuv422),
PixelFormat::Yuv444P | PixelFormat::YuvJ444P => (8, ChromaFormat::Yuv444),
PixelFormat::Gray8 => (8, ChromaFormat::Monochrome),
PixelFormat::Yuv420P10Le => (10, ChromaFormat::Yuv420),
PixelFormat::Yuv422P10Le => (10, ChromaFormat::Yuv422),
PixelFormat::Yuv444P10Le => (10, ChromaFormat::Yuv444),
PixelFormat::Gray10Le => (10, ChromaFormat::Monochrome),
PixelFormat::Yuv420P12Le => (12, ChromaFormat::Yuv420),
PixelFormat::Yuv422P12Le => (12, ChromaFormat::Yuv422),
PixelFormat::Yuv444P12Le => (12, ChromaFormat::Yuv444),
PixelFormat::Gray12Le => (12, ChromaFormat::Monochrome),
_ => return None,
})
}
pub fn make_encoder(params: &CodecParameters) -> CoreResult<Box<dyn Encoder>> {
let width = params
.width
.ok_or_else(|| CoreError::invalid("oxideav-av1: encoder needs width"))?;
let height = params
.height
.ok_or_else(|| CoreError::invalid("oxideav-av1: encoder needs height"))?;
let pf = params.pixel_format.unwrap_or(PixelFormat::Yuv420P);
let (bit_depth, format) = pixel_format_layout(pf).ok_or_else(|| {
CoreError::invalid(format!("oxideav-av1: unsupported pixel format {pf:?}"))
})?;
let opts = Av1EncoderOptions::from_params(params)?;
YuvFrame::filled(width, height, bit_depth, format, 0)
.validate()
.map_err(|e| CoreError::invalid(format!("oxideav-av1: unsupported geometry: {e}")))?;
let mut seq = crate::encoder::build_intra_only_seq_yuv(width, height, bit_depth, format)
.map_err(|e| CoreError::invalid(format!("oxideav-av1: {e}")))?;
if opts.still {
crate::encoder::still::apply_still_picture_shape(&mut seq);
}
seq.color_config.color_range = opts.full_range;
if let Some((cp, tc, mc)) = opts.color_description {
seq.color_config.color_description_present_flag = true;
seq.color_config.color_primaries = cp;
seq.color_config.transfer_characteristics = tc;
seq.color_config.matrix_coefficients = mc;
}
let mut out = params.clone();
out.codec_id = CodecId::new(CODEC_ID_STR);
out.pixel_format = Some(pf);
out.extradata = Av1CodecConfig::from_sequence_header(&seq).to_bytes();
let time_base = match params.frame_rate {
Some(r) if r.num > 0 && r.den > 0 => TimeBase::new(r.den, r.num),
_ => TimeBase::new(1, 25),
};
Ok(Box::new(Av1Encoder {
codec_id: CodecId::new(CODEC_ID_STR),
params: out,
opts,
width,
height,
bit_depth,
format,
time_base,
next_pts: 0,
queue: std::collections::VecDeque::new(),
eof: false,
thread_budget: 1,
}))
}
struct Av1Encoder {
codec_id: CodecId,
params: CodecParameters,
opts: Av1EncoderOptions,
width: u32,
height: u32,
bit_depth: u8,
format: ChromaFormat,
time_base: TimeBase,
next_pts: i64,
queue: std::collections::VecDeque<Packet>,
eof: bool,
thread_budget: usize,
}
impl std::fmt::Debug for Av1Encoder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Av1Encoder")
.field("opts", &self.opts)
.field("width", &self.width)
.field("height", &self.height)
.field("bit_depth", &self.bit_depth)
.field("format", &self.format)
.field("queued", &self.queue.len())
.finish()
}
}
impl Av1Encoder {
fn to_yuv(&self, frame: &VideoFrame) -> CoreResult<YuvFrame> {
let planes = frame.image_planes();
let need = usize::from(self.format.num_planes());
if planes.len() < need {
return Err(CoreError::invalid(format!(
"oxideav-av1: frame carries {} planes, format needs {need}",
planes.len()
)));
}
let bps: usize = if self.bit_depth > 8 { 2 } else { 1 };
let lift = |p: &VideoPlane, w: usize, h: usize| -> CoreResult<Vec<u16>> {
let row_bytes = w * bps;
if p.stride < row_bytes || p.data.len() < p.stride * (h - 1) + row_bytes {
return Err(CoreError::invalid(
"oxideav-av1: frame plane shorter than the declared geometry",
));
}
let mut out = Vec::with_capacity(w * h);
for y in 0..h {
let row = &p.data[y * p.stride..y * p.stride + row_bytes];
if bps == 1 {
out.extend(row.iter().map(|&b| u16::from(b)));
} else {
out.extend(
row.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]])),
);
}
}
Ok(out)
};
let (w, h) = (self.width as usize, self.height as usize);
let probe = YuvFrame::filled(self.width, self.height, self.bit_depth, self.format, 0);
let (cw, ch) = (
probe.chroma_width() as usize,
probe.chroma_height() as usize,
);
let y = lift(&planes[0], w, h)?;
let (u, v) = if need > 1 {
(lift(&planes[1], cw, ch)?, lift(&planes[2], cw, ch)?)
} else {
(Vec::new(), Vec::new())
};
Ok(YuvFrame {
width: self.width,
height: self.height,
bit_depth: self.bit_depth,
format: self.format,
y,
u,
v,
})
}
}
impl Encoder for Av1Encoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn output_params(&self) -> &CodecParameters {
&self.params
}
fn send_frame(&mut self, frame: &CoreFrame) -> CoreResult<()> {
let CoreFrame::Video(vf) = frame else {
return Err(CoreError::invalid(
"oxideav-av1: encoder takes video frames",
));
};
let input = self.to_yuv(vf)?;
let opts = self.opts.still_options(self.thread_budget);
let tu = if self.opts.still {
encode_still_yuv(&input, &opts)
.map_err(|e| CoreError::invalid(format!("oxideav-av1: {e}")))?
.temporal_unit_bytes
} else {
encode_key_frame_yuv_with_options(&input, &opts)
.map_err(|e| CoreError::invalid(format!("oxideav-av1: {e}")))?
.temporal_unit_bytes
};
let pts = vf.pts.unwrap_or(self.next_pts);
self.next_pts = pts + 1;
self.queue.push_back(
Packet::new(0, self.time_base, tu)
.with_pts(pts)
.with_dts(pts)
.with_duration(1)
.with_keyframe(true),
);
Ok(())
}
fn receive_packet(&mut self) -> CoreResult<Packet> {
match self.queue.pop_front() {
Some(p) => Ok(p),
None if self.eof => Err(CoreError::Eof),
None => Err(CoreError::NeedMore),
}
}
fn flush(&mut self) -> CoreResult<()> {
self.eof = true;
Ok(())
}
fn set_execution_context(&mut self, ctx: &ExecutionContext) {
self.thread_budget = ctx.threads.max(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::ProbeContext;
fn textured_frame(width: u32, height: u32, pf: PixelFormat) -> (VideoFrame, YuvFrame) {
let (bd, fmt) = pixel_format_layout(pf).expect("supported");
let mut yuv = YuvFrame::filled(width, height, bd, fmt, 0);
let max = (1u32 << bd) - 1;
for (i, s) in yuv.y.iter_mut().enumerate() {
*s = ((i as u32 * 37 + (i as u32 / width) * 91) % max) as u16;
}
for (i, s) in yuv.u.iter_mut().enumerate() {
*s = ((i as u32 * 13) % max) as u16;
}
for (i, s) in yuv.v.iter_mut().enumerate() {
*s = ((i as u32 * 29 + 5) % max) as u16;
}
let pack = |p: &[u16], w: usize| -> VideoPlane {
let mut data = Vec::new();
for &s in p {
if bd > 8 {
data.extend_from_slice(&s.to_le_bytes());
} else {
data.push(s as u8);
}
}
VideoPlane {
stride: w * if bd > 8 { 2 } else { 1 },
data,
}
};
let mut planes = vec![pack(&yuv.y, width as usize)];
if fmt != ChromaFormat::Monochrome {
planes.push(pack(&yuv.u, yuv.chroma_width() as usize));
planes.push(pack(&yuv.v, yuv.chroma_width() as usize));
}
(
VideoFrame {
pts: Some(7),
planes,
},
yuv,
)
}
fn video_params(width: u32, height: u32, pf: PixelFormat) -> CodecParameters {
let mut p = CodecParameters::video(CodecId::new(CODEC_ID_STR));
p.width = Some(width);
p.height = Some(height);
p.pixel_format = Some(pf);
p
}
#[test]
fn framework_still_encode_round_trips_through_the_framework_decoder() {
for (pf, opts) in [
(PixelFormat::Yuv420P, [("still", "true"), ("quality", "60")]),
(PixelFormat::Gray10Le, [("still", "true"), ("q", "80")]),
(
PixelFormat::YuvJ444P,
[("still", "true"), ("lossless", "true")],
),
] {
let mut params = video_params(32, 24, pf);
for (k, v) in opts {
params.options.insert(k, v);
}
let mut enc = make_encoder(¶ms).expect("encoder constructs");
let (frame, yuv) = textured_frame(32, 24, pf);
enc.send_frame(&CoreFrame::Video(frame))
.expect("frame accepted");
let pkt = enc.receive_packet().expect("one packet");
assert!(pkt.flags.keyframe && pkt.pts == Some(7));
assert!(matches!(enc.receive_packet(), Err(CoreError::NeedMore)));
enc.flush().unwrap();
assert!(matches!(enc.receive_packet(), Err(CoreError::Eof)));
let extradata = enc.output_params().extradata.clone();
let cfg = Av1CodecConfig::parse(&extradata).expect("av1C extradata");
let seq = crate::sequence_header::parse_sequence_header(
crate::obu::ObuIter::new(&pkt.data)
.filter_map(Result::ok)
.find(|d| d.obu_type == crate::obu::ObuType::SequenceHeader)
.expect("in-band sequence header")
.payload,
)
.expect("parses");
assert!(seq.still_picture && seq.reduced_still_picture_header);
assert!(cfg.matches_sequence_header(&seq), "{pf:?}: av1C mismatch");
assert_eq!(seq.color_config.color_range, pf == PixelFormat::YuvJ444P);
let opts = Av1EncoderOptions::from_params(¶ms).unwrap();
let direct = encode_still_yuv(&yuv, &opts.still_options(1)).expect("direct still");
assert_eq!(
direct.temporal_unit_bytes, pkt.data,
"{pf:?}: framework != direct"
);
let mut dparams = CodecParameters::video(CodecId::new(CODEC_ID_STR));
dparams.extradata = extradata;
let mut dec = make_decoder(&dparams).expect("decoder");
dec.send_packet(&pkt).expect("decodes");
let CoreFrame::Video(out) = dec.receive_frame().expect("frame") else {
panic!("video frame expected");
};
let widen = |p: &[u8]| -> Vec<u16> {
if cfg.bit_depth() > 8 {
p.chunks(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect()
} else {
p.iter().map(|&b| u16::from(b)).collect()
}
};
assert_eq!(widen(&out.planes[0].data), direct.recon_y);
if opts.base_q_idx == 0 {
assert_eq!(direct.recon_y, yuv.y, "{pf:?}: lossless luma");
}
}
}
#[test]
fn framework_all_intra_encode_emits_one_key_packet_per_frame() {
let mut params = video_params(16, 16, PixelFormat::Yuv420P);
params.options.insert("q", "200");
params.options.insert("speed", "fast");
let mut enc = make_encoder(¶ms).expect("encoder constructs");
assert_eq!(enc.output_params().extradata, vec![0x81, 0x00, 0x0c, 0x00]);
for i in 0..2 {
let (mut frame, _) = textured_frame(16, 16, PixelFormat::Yuv420P);
frame.pts = None;
enc.send_frame(&CoreFrame::Video(frame)).expect("frame");
let pkt = enc.receive_packet().expect("packet");
assert_eq!(pkt.pts, Some(i));
let frames = crate::decoder::decode_av1_spec(&{
let mut buf = Vec::new();
let cur = std::io::Cursor::new(&mut buf);
let mut w = crate::encoder::IvfWriter::new(
cur,
crate::encoder::ivf::FOURCC_AV01,
16,
16,
25,
1,
)
.unwrap();
w.write_frame(&pkt.data, 0).unwrap();
w.patch_frame_count().unwrap();
buf
})
.expect("decodes");
assert_eq!(frames.len(), 1);
}
}
#[test]
fn encoder_options_and_geometry_are_validated() {
let mut params = video_params(16, 16, PixelFormat::Yuv420P);
params.options.insert("speed", "warp");
assert!(make_encoder(¶ms).is_err());
let mut params = video_params(16, 16, PixelFormat::Yuv420P);
params.options.insert("bogus", "1");
assert!(make_encoder(¶ms).is_err());
let params = video_params(16, 16, PixelFormat::Rgb24);
assert!(make_encoder(¶ms).is_err());
let mut params = video_params(16, 16, PixelFormat::Yuv420P);
params.options.insert("quality", "100");
let o = Av1EncoderOptions::from_params(¶ms).unwrap();
assert_eq!(o.base_q_idx, 0);
params.options.insert("full_range", "true");
params.options.insert("tile_cols_log2", "1");
let o = Av1EncoderOptions::from_params(¶ms).unwrap();
assert!(o.full_range && o.tile_cols_log2 == 1);
params.options.insert("color_primaries", "1");
assert!(Av1EncoderOptions::from_params(¶ms).is_err());
params.options.insert("transfer_characteristics", "13");
params.options.insert("matrix_coefficients", "6");
let o = Av1EncoderOptions::from_params(¶ms).unwrap();
assert_eq!(o.color_description, Some((1, 13, 6)));
}
#[test]
fn defaults_are_fast_and_auto_threaded_under_the_execution_budget() {
let params = video_params(512, 256, PixelFormat::Yuv420P);
let o = Av1EncoderOptions::from_params(¶ms).unwrap();
assert_eq!(o.speed, StillSpeed::Fast);
assert_eq!(o.threads, None);
assert!(o.auto_tiles);
assert_eq!(o.still_options(1).threads, 1);
assert!(!o.still_options(1).auto_tiles);
let four = o.still_options(4);
assert_eq!(four.threads, 4);
assert!(four.auto_tiles);
let mut explicit = params.clone();
explicit.options.insert("threads", "2");
explicit.options.insert("tile_rows_log2", "1");
let e = Av1EncoderOptions::from_params(&explicit).unwrap();
assert_eq!(e.still_options(8).threads, 2);
assert!(!e.still_options(8).auto_tiles);
let mut auto = params.clone();
auto.options.insert("threads", "auto");
assert_eq!(Av1EncoderOptions::from_params(&auto).unwrap().threads, None);
let (frame, yuv) = textured_frame(512, 256, PixelFormat::Yuv420P);
for still in [true, false] {
let mut p = params.clone();
p.options
.insert("still", if still { "true" } else { "false" });
let mut enc = make_encoder(&p).expect("encoder");
enc.set_execution_context(&ExecutionContext::with_threads(4));
enc.send_frame(&CoreFrame::Video(frame.clone()))
.expect("frame");
enc.flush().expect("flush");
let pkt = enc.receive_packet().expect("packet");
let direct = if still {
encode_still_yuv(&yuv, &o.still_options(4)).expect("direct")
} else {
encode_key_frame_yuv_with_options(&yuv, &o.still_options(4)).expect("direct")
};
assert_eq!(pkt.data, direct.temporal_unit_bytes, "still={still}");
let ti = direct.fh.tile_info.as_ref().expect("tile info");
assert!(
ti.tile_cols_log2 + ti.tile_rows_log2 > 0,
"auto tiles engaged"
);
let seq = crate::sequence_header::parse_sequence_header(
crate::obu::ObuIter::new(&pkt.data)
.filter_map(Result::ok)
.find(|d| d.obu_type == crate::obu::ObuType::SequenceHeader)
.expect("in-band sequence header")
.payload,
)
.expect("parses");
assert_eq!(seq.still_picture, still);
assert_eq!(seq.reduced_still_picture_header, still);
let frames = crate::decoder::decode_av1_spec(&direct.ivf_bytes).expect("decodes");
assert_eq!(frames.len(), 1);
assert_eq!(
frames[0].planes[0]
.iter()
.map(|&b| u16::from(b))
.collect::<Vec<_>>(),
direct.recon_y
);
}
}
#[test]
fn register_via_runtime_context_installs_decoder() {
let mut ctx = RuntimeContext::new();
register(&mut ctx);
let codec_id = CodecId::new(CODEC_ID_STR);
assert!(
ctx.codecs.has_decoder(&codec_id),
"codec registration should install a decoder factory"
);
}
#[test]
fn av1c_extradata_config_obus_seed_the_sequence_header() {
use crate::encoder::{encode_still_yuv420, StillOptions, Yuv420Frame};
use crate::obu::{ObuIter, ObuType};
let mut frame = Yuv420Frame::filled(32, 16, 60);
for (i, s) in frame.y.iter_mut().enumerate() {
*s = (i * 7 % 200) as u8 + 20;
}
let still = encode_still_yuv420(&frame, &StillOptions::new(90)).expect("encodes");
let mut seq_obu = Vec::new();
let mut frame_obus = Vec::new();
for desc in ObuIter::new(&still.temporal_unit_bytes) {
let desc = desc.expect("well-formed OBU");
let obu = crate::encoder::obu::ObuFrame::new(desc.obu_type, desc.payload.to_vec());
if desc.obu_type == ObuType::SequenceHeader {
seq_obu.push(obu);
} else {
frame_obus.push(obu);
}
}
let config_obus = crate::encoder::obu::write_temporal_unit(&seq_obu);
let payload = crate::encoder::obu::write_temporal_unit(&frame_obus);
let mut av1c = still.codec_config.clone();
av1c.config_obus = config_obus.clone();
let decode_with = |extradata: Vec<u8>, packet: &[u8]| -> CoreResult<Vec<Vec<u8>>> {
let mut params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
params.extradata = extradata;
let mut dec = make_decoder(¶ms)?;
dec.send_packet(&Packet::new(
0,
oxideav_core::TimeBase::new(1, 1),
packet.to_vec(),
))?;
match dec.receive_frame()? {
CoreFrame::Video(v) => Ok(v.planes.into_iter().map(|p| p.data).collect()),
other => panic!("expected a video frame, got {other:?}"),
}
};
let expected = decode_with(Vec::new(), &still.temporal_unit_bytes).expect("in-band");
assert!(
decode_with(Vec::new(), &payload).is_err(),
"a header-less payload must be refused without extradata"
);
let via_av1c = decode_with(av1c.to_bytes(), &payload).expect("av1C configOBUs");
assert_eq!(via_av1c, expected);
let via_raw = decode_with(config_obus, &payload).expect("bare OBU extradata");
assert_eq!(via_raw, expected);
let repeated = decode_with(av1c.to_bytes(), &still.temporal_unit_bytes)
.expect("repeated sequence header");
assert_eq!(repeated, expected);
let bare = decode_with(still.codec_config.to_bytes(), &still.temporal_unit_bytes)
.expect("bare av1C");
assert_eq!(bare, expected);
}
#[test]
fn register_claims_container_tags() {
let mut reg = CodecRegistry::new();
register_codecs(&mut reg);
let fourcc = CodecTag::fourcc(b"AV01");
assert_eq!(
reg.resolve_tag_ref(&ProbeContext::new(&fourcc))
.map(oxideav_core::CodecId::as_str),
Some(CODEC_ID_STR),
"FourCC AV01 / sample-entry av01 must resolve to av1"
);
let mkv = CodecTag::matroska("V_AV1");
assert_eq!(
reg.resolve_tag_ref(&ProbeContext::new(&mkv))
.map(oxideav_core::CodecId::as_str),
Some(CODEC_ID_STR),
"Matroska V_AV1 must resolve to av1"
);
}
}