use std::collections::VecDeque;
use oxideav_core::Encoder;
use oxideav_core::{
CodecId, CodecParameters, Error, Frame, MediaType, Packet, PixelFormat, Result, TimeBase,
VideoFrame,
};
use crate::alpha::{encode_scanned_alpha, AlphaChannelType};
use crate::dct::{fdct8x8, fdct8x8_constant, is_constant_block};
use crate::decoder::BitDepth;
use crate::frame::{
compute_slice_sizes, frame_rate_code_from_rational, write_frame_with_meta,
write_picture_header, write_slice_header, ChromaFormat, FrameMeta, Profile,
};
use crate::quant::{qscale, QuantMatrices};
use crate::slice::{blocks_per_mb, chroma_blocks_per_mb, encode_slice_components};
#[derive(Clone, Debug, Default)]
pub struct EncoderConfig {
pub quant_matrices: Option<QuantMatrices>,
pub quantization_index: Option<u8>,
pub meta: Option<FrameMeta>,
pub rate_control: bool,
pub profile: Option<Profile>,
pub interlace_mode: u8,
pub mbs_per_slice: Option<u8>,
pub min_frame_size: Option<u32>,
pub explicit_qmat_carriage: bool,
pub alpha_channel_type: Option<AlphaChannelType>,
}
pub const RATE_CTRL_MAX_PASSES: usize = 10;
pub const RATE_CTRL_TOLERANCE: f64 = 0.05;
impl EncoderConfig {
pub fn flat() -> Self {
Self::default()
}
pub fn perceptual() -> Self {
Self {
quant_matrices: Some(QuantMatrices::perceptual()),
..Self::default()
}
}
pub fn perceptual_for_profile(profile: Profile) -> Self {
Self {
quant_matrices: Some(QuantMatrices::perceptual_for_profile(profile)),
profile: Some(profile),
..Self::default()
}
}
pub fn signature_for_profile(profile: Profile) -> Self {
Self {
quant_matrices: Some(QuantMatrices::signature_for_profile(profile)),
profile: Some(profile),
..Self::default()
}
}
pub fn for_profile(profile: Profile) -> Self {
Self {
profile: Some(profile),
..Self::default()
}
}
pub fn with_quant_matrices(mut self, qm: QuantMatrices) -> Self {
self.quant_matrices = Some(qm);
self
}
pub fn with_quantization_index(mut self, qi: u8) -> Self {
self.quantization_index = Some(qi);
self
}
pub fn with_meta(mut self, meta: FrameMeta) -> Self {
self.meta = Some(meta);
self
}
pub fn with_rate_control(mut self) -> Self {
self.rate_control = true;
self
}
pub fn with_profile(mut self, profile: Profile) -> Self {
self.profile = Some(profile);
self
}
pub fn with_interlace_mode(mut self, interlace_mode: u8) -> Self {
self.interlace_mode = interlace_mode;
self
}
pub fn with_mbs_per_slice(mut self, mbs_per_slice: u8) -> Self {
self.mbs_per_slice = Some(mbs_per_slice);
self
}
pub fn with_min_frame_size(mut self, min_frame_size: u32) -> Self {
self.min_frame_size = Some(min_frame_size);
self
}
pub fn with_explicit_qmat_carriage(mut self) -> Self {
self.explicit_qmat_carriage = true;
self
}
pub fn with_alpha_channel_type(mut self, act: AlphaChannelType) -> Self {
self.alpha_channel_type = Some(act);
self
}
}
#[derive(Copy, Clone, Debug)]
struct AlphaCoding {
act: AlphaChannelType,
input_depth: BitDepth,
}
impl AlphaCoding {
fn matched(act: AlphaChannelType) -> Self {
let input_depth = match act {
AlphaChannelType::Eight => BitDepth::Eight,
AlphaChannelType::Sixteen => BitDepth::Sixteen,
};
Self { act, input_depth }
}
fn for_typed_depth(input_depth: BitDepth) -> Self {
let act = match input_depth {
BitDepth::Eight => AlphaChannelType::Eight,
BitDepth::Ten | BitDepth::Twelve | BitDepth::Sixteen => AlphaChannelType::Sixteen,
};
Self { act, input_depth }
}
}
fn read_alpha_input(plane: &[u8], stride: usize, x: usize, y: usize, depth: BitDepth) -> u16 {
match depth {
BitDepth::Eight => plane[y * stride + x] as u16,
BitDepth::Ten | BitDepth::Twelve | BitDepth::Sixteen => {
let off = y * stride + x * 2;
let w = u16::from_le_bytes([plane[off], plane[off + 1]]);
(w as u32 & depth.max_value()) as u16
}
}
}
fn alpha_input_to_coded(raw: u16, input_depth: BitDepth, act: AlphaChannelType) -> u16 {
let in_max = input_depth.max_value() as u64;
let coded_max = act.mask() as u64;
if in_max == coded_max {
return raw;
}
((coded_max * raw as u64 + in_max / 2) / in_max) as u16
}
fn detect_alpha_channel_type(
plane: &oxideav_core::frame::VideoPlane,
width: usize,
) -> Result<AlphaChannelType> {
if width == 0 {
return Err(Error::invalid("prores encoder: zero-width alpha plane"));
}
match plane.stride / width {
1 => Ok(AlphaChannelType::Eight),
2 => Ok(AlphaChannelType::Sixteen),
other => Err(Error::invalid(format!(
"prores encoder: cannot infer alpha depth from a 4-plane frame whose alpha \
stride is {} bytes for {width} samples/row ({other} bytes/sample — expected \
1 for 8-bit or 2 for 16-bit LE); set \
EncoderConfig::alpha_channel_type explicitly",
plane.stride
))),
}
}
pub const DEFAULT_MBS_PER_SLICE: u8 = 8;
pub fn mbs_per_slice_to_log2(mbs_per_slice: u8) -> Result<u8> {
match mbs_per_slice {
1 => Ok(0),
2 => Ok(1),
4 => Ok(2),
8 => Ok(3),
_ => Err(Error::invalid(
"prores encoder: mbs_per_slice must be 1, 2, 4, or 8 (RDD 36 §5.3 — \
log2_desired_slice_size_in_mb is a 2-bit field, so 8 MBs is the maximum)",
)),
}
}
pub const DEFAULT_QUANT_INDEX: u8 = 4;
fn output_capacity_cap(width: u16, height: u16, chroma: ChromaFormat) -> usize {
let pixels = width as usize * height as usize;
let bpp = match chroma {
ChromaFormat::Y422 => 16,
ChromaFormat::Y444 => 24,
};
pixels.saturating_mul(bpp).saturating_add(1 << 16)
}
const MB_SIDE_PX: usize = 16;
pub fn pick_profile(chroma: ChromaFormat, bit_rate: Option<u64>) -> Profile {
match (chroma, bit_rate) {
(ChromaFormat::Y422, Some(br)) if br <= 70_000_000 => Profile::Proxy,
(ChromaFormat::Y422, Some(br)) if br <= 125_000_000 => Profile::Lt,
(ChromaFormat::Y422, Some(br)) if br <= 180_000_000 => Profile::Standard,
(ChromaFormat::Y422, Some(_)) => Profile::Hq,
(ChromaFormat::Y422, None) => Profile::Standard,
(ChromaFormat::Y444, Some(br)) if br >= 400_000_000 => Profile::Prores4444Xq,
(ChromaFormat::Y444, _) => Profile::Prores4444,
}
}
pub fn make_encoder(params: &CodecParameters) -> Result<Box<dyn Encoder>> {
make_encoder_with_config(params, EncoderConfig::default())
}
pub fn make_encoder_with_config(
params: &CodecParameters,
config: EncoderConfig,
) -> Result<Box<dyn Encoder>> {
if let Some(qm) = &config.quant_matrices {
if !qm.weights_valid() {
return Err(Error::invalid(
"prores encoder: quant matrix weight outside RDD 36 range 2..=63",
));
}
}
if let Some(qi) = config.quantization_index {
if !(1..=224).contains(&qi) {
return Err(Error::invalid(
"prores encoder: EncoderConfig::quantization_index out of range \
(must be 1..=224 per RDD 36 §7.3 / Table 15)",
));
}
}
if config.interlace_mode > 2 {
return Err(Error::invalid(
"prores encoder: EncoderConfig::interlace_mode must be 0 (progressive), \
1 (top-field-first) or 2 (bottom-field-first) — value 3 is reserved \
(RDD 36 §6.1.1 Table 2)",
));
}
if let Some(m) = config.mbs_per_slice {
mbs_per_slice_to_log2(m)?;
}
let width = params
.width
.ok_or_else(|| Error::invalid("prores encoder: missing width"))?;
let height = params
.height
.ok_or_else(|| Error::invalid("prores encoder: missing height"))?;
let pix = params.pixel_format.unwrap_or(PixelFormat::Yuv422P);
let (chroma, bit_depth, typed_alpha) = match pix {
PixelFormat::Yuv422P => (ChromaFormat::Y422, BitDepth::Eight, None),
PixelFormat::Yuv444P => (ChromaFormat::Y444, BitDepth::Eight, None),
PixelFormat::Yuv422P10Le => (ChromaFormat::Y422, BitDepth::Ten, None),
PixelFormat::Yuv444P10Le => (ChromaFormat::Y444, BitDepth::Ten, None),
PixelFormat::Yuv422P12Le => (ChromaFormat::Y422, BitDepth::Twelve, None),
PixelFormat::Yuv444P12Le => (ChromaFormat::Y444, BitDepth::Twelve, None),
PixelFormat::Yuv422P16Le => (ChromaFormat::Y422, BitDepth::Sixteen, None),
PixelFormat::Yuv444P16Le => (ChromaFormat::Y444, BitDepth::Sixteen, None),
PixelFormat::Yuva422P => (ChromaFormat::Y422, BitDepth::Eight, Some(BitDepth::Eight)),
PixelFormat::Yuva444P => (ChromaFormat::Y444, BitDepth::Eight, Some(BitDepth::Eight)),
PixelFormat::Yuva422P10Le => (ChromaFormat::Y422, BitDepth::Ten, Some(BitDepth::Ten)),
PixelFormat::Yuva444P10Le => (ChromaFormat::Y444, BitDepth::Ten, Some(BitDepth::Ten)),
PixelFormat::Yuva422P12Le => (ChromaFormat::Y422, BitDepth::Twelve, Some(BitDepth::Twelve)),
PixelFormat::Yuva444P12Le => (ChromaFormat::Y444, BitDepth::Twelve, Some(BitDepth::Twelve)),
PixelFormat::Yuva422P16Le => (
ChromaFormat::Y422,
BitDepth::Sixteen,
Some(BitDepth::Sixteen),
),
PixelFormat::Yuva444P16Le => (
ChromaFormat::Y444,
BitDepth::Sixteen,
Some(BitDepth::Sixteen),
),
other => {
return Err(Error::unsupported(format!(
"prores encoder: pixel format {other:?} not supported \
(expected Yuv4(2|4)4P / Yuva4(2|4)4P, plain or with a 10Le/12Le/16Le \
depth suffix)"
)));
}
};
if let Some(d) = typed_alpha {
match (d, config.alpha_channel_type) {
(BitDepth::Eight, Some(AlphaChannelType::Sixteen)) => {
return Err(Error::invalid(
"prores encoder: pixel_format Yuva4(2|4)4P carries 8-bit alpha samples; \
AlphaChannelType::Sixteen would mis-read them — declare a deep \
Yuva4(2|4)4P1?Le pixel_format (or a Yuv4(2|4)4P* format with a 16-bit \
LE alpha plane) instead",
));
}
(
BitDepth::Ten | BitDepth::Twelve | BitDepth::Sixteen,
Some(AlphaChannelType::Eight),
) => {
return Err(Error::invalid(format!(
"prores encoder: pixel_format {pix:?} codes 16-bit alpha \
(alpha_channel_type = 2) so the deep input samples lose no wire \
precision; AlphaChannelType::Eight contradicts the declared format — \
for 8-bit coded alpha declare Yuva4(2|4)4P or a Yuv4(2|4)4P* format \
with an 8-bit alpha plane"
)));
}
_ => {}
}
}
let profile = if let Some(p) = config.profile {
if p.chroma_format() != chroma {
return Err(Error::invalid(format!(
"prores encoder: EncoderConfig::profile {p:?} (chroma_format = \
{:?}) does not match requested pixel_format {pix:?} (chroma_format \
= {chroma:?})",
p.chroma_format(),
)));
}
p
} else {
pick_profile(chroma, params.bit_rate)
};
let mut output_params = params.clone();
output_params.media_type = MediaType::Video;
output_params.codec_id = CodecId::new(super::CODEC_ID_STR);
output_params.width = Some(width);
output_params.height = Some(height);
output_params.pixel_format = Some(pix);
let quant_index = config
.quantization_index
.unwrap_or_else(|| profile.default_quant_index());
let meta = config.meta.unwrap_or_else(|| FrameMeta {
frame_rate_code: params.frame_rate.map_or(0, frame_rate_code_from_rational),
..FrameMeta::default()
});
let target_bytes = if config.rate_control {
if let (Some(br), Some(fr)) = (params.bit_rate, params.frame_rate) {
if fr.num > 0 && fr.den > 0 {
let bits_per_frame = (br * fr.den as u64).saturating_div(fr.num as u64);
(bits_per_frame / 8) as usize
} else {
0
}
} else {
0
}
} else {
0
};
let interlace_mode = config.interlace_mode;
let log2_slice_mb_width =
mbs_per_slice_to_log2(config.mbs_per_slice.unwrap_or(DEFAULT_MBS_PER_SLICE))?;
let alpha_channel_type = config.alpha_channel_type;
Ok(Box::new(ProResEncoder {
alpha_channel_type,
typed_alpha,
output_params,
width,
height,
chroma,
bit_depth,
profile,
quant_index,
meta,
interlace_mode,
log2_slice_mb_width,
config,
time_base: params
.frame_rate
.map_or(TimeBase::new(1, 90_000), |r| TimeBase::new(r.den, r.num)),
target_bytes,
pending: VecDeque::new(),
eof: false,
}))
}
struct ProResEncoder {
output_params: CodecParameters,
width: u32,
height: u32,
chroma: ChromaFormat,
bit_depth: BitDepth,
profile: Profile,
quant_index: u8,
meta: FrameMeta,
interlace_mode: u8,
log2_slice_mb_width: u8,
alpha_channel_type: Option<AlphaChannelType>,
typed_alpha: Option<BitDepth>,
config: EncoderConfig,
time_base: TimeBase,
target_bytes: usize,
pending: VecDeque<Packet>,
eof: bool,
}
impl Encoder for ProResEncoder {
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<()> {
match frame {
Frame::Video(v) => {
if self.typed_alpha.is_some() && v.planes.len() != 4 {
return Err(Error::invalid(format!(
"prores encoder: pixel_format {:?} declares a 4-plane frame \
(Y, Cb, Cr, alpha) but the input has {} plane(s)",
self.output_params.pixel_format.unwrap(),
v.planes.len()
)));
}
let alpha: Option<AlphaCoding> = if let Some(depth) = self.typed_alpha {
let detected = detect_alpha_channel_type(&v.planes[3], self.width as usize)?;
let expected_bps = depth.bytes_per_sample();
let actual_bps = match detected {
AlphaChannelType::Eight => 1usize,
AlphaChannelType::Sixteen => 2usize,
};
if actual_bps != expected_bps {
return if expected_bps == 1 {
Err(Error::invalid(format!(
"prores encoder: pixel_format {:?} declares 8-bit alpha \
samples but the input alpha plane is 2 bytes per sample; \
declare a deep Yuva4(2|4)4P1?Le (or Yuv4(2|4)4P*) \
pixel_format for 16-bit-word alpha input",
self.output_params.pixel_format.unwrap()
)))
} else {
Err(Error::invalid(format!(
"prores encoder: pixel_format {:?} declares 16-bit-word \
alpha samples (low {} bits significant) but the input \
alpha plane is 1 byte per sample; declare Yuva4(2|4)4P \
for 8-bit alpha input",
self.output_params.pixel_format.unwrap(),
depth.bits()
)))
};
}
Some(AlphaCoding::for_typed_depth(depth))
} else {
match self.alpha_channel_type {
Some(act) => Some(AlphaCoding::matched(act)),
None if v.planes.len() == 4 => Some(AlphaCoding::matched(
detect_alpha_channel_type(&v.planes[3], self.width as usize)?,
)),
None => None,
}
};
let data = if self.target_bytes > 0 {
encode_frame_with_rate_control(
v,
self.width,
self.height,
self.chroma,
self.bit_depth,
self.profile,
self.quant_index,
self.config.quant_matrices,
self.config.explicit_qmat_carriage,
self.meta,
self.target_bytes,
self.interlace_mode,
self.log2_slice_mb_width,
alpha,
)?
} else {
encode_frame_full(
v,
self.width,
self.height,
self.chroma,
self.bit_depth,
self.profile,
self.quant_index,
alpha,
self.interlace_mode,
self.config.quant_matrices,
self.config.explicit_qmat_carriage,
self.meta,
self.log2_slice_mb_width,
)?
};
let data = match self.config.min_frame_size {
Some(min) => pad_frame_to_size(&data, min)?,
None => data,
};
let mut pkt = Packet::new(0, self.time_base, data);
pkt.pts = v.pts;
pkt.dts = v.pts;
pkt.flags.keyframe = true;
self.pending.push_back(pkt);
Ok(())
}
_ => Err(Error::invalid("prores encoder: video frames only")),
}
}
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 encode_frame_422(
frame: &VideoFrame,
width: u32,
height: u32,
profile: Profile,
quant_index: u8,
) -> Result<Vec<u8>> {
encode_frame(
frame,
width,
height,
ChromaFormat::Y422,
profile,
quant_index,
)
}
pub fn encode_frame(
frame: &VideoFrame,
img_w: u32,
img_h: u32,
chroma: ChromaFormat,
profile: Profile,
quantization_index: u8,
) -> Result<Vec<u8>> {
encode_frame_with_depth(
frame,
img_w,
img_h,
chroma,
BitDepth::Eight,
profile,
quantization_index,
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_frame_with_depth(
frame: &VideoFrame,
img_w: u32,
img_h: u32,
chroma: ChromaFormat,
bit_depth: BitDepth,
profile: Profile,
quantization_index: u8,
) -> Result<Vec<u8>> {
encode_frame_with_alpha(
frame,
img_w,
img_h,
chroma,
bit_depth,
profile,
quantization_index,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_frame_with_qmats(
frame: &VideoFrame,
img_w: u32,
img_h: u32,
chroma: ChromaFormat,
bit_depth: BitDepth,
profile: Profile,
quantization_index: u8,
qmats: QuantMatrices,
) -> Result<Vec<u8>> {
encode_frame_full(
frame,
img_w,
img_h,
chroma,
bit_depth,
profile,
quantization_index,
None,
0,
Some(qmats),
false,
FrameMeta::default(),
3, )
}
#[allow(clippy::too_many_arguments)]
pub fn encode_frame_with_alpha(
frame: &VideoFrame,
img_w: u32,
img_h: u32,
chroma: ChromaFormat,
bit_depth: BitDepth,
profile: Profile,
quantization_index: u8,
alpha_channel_type: Option<AlphaChannelType>,
) -> Result<Vec<u8>> {
encode_frame_full(
frame,
img_w,
img_h,
chroma,
bit_depth,
profile,
quantization_index,
alpha_channel_type.map(AlphaCoding::matched),
0,
None,
false,
FrameMeta::default(),
3, )
}
#[allow(clippy::too_many_arguments)]
pub fn encode_frame_interlaced(
frame: &VideoFrame,
img_w: u32,
img_h: u32,
chroma: ChromaFormat,
bit_depth: BitDepth,
profile: Profile,
quantization_index: u8,
alpha_channel_type: Option<AlphaChannelType>,
interlace_mode: u8,
) -> Result<Vec<u8>> {
if interlace_mode != 1 && interlace_mode != 2 {
return Err(Error::invalid(
"prores encoder: encode_frame_interlaced requires interlace_mode in {1, 2}",
));
}
encode_frame_full(
frame,
img_w,
img_h,
chroma,
bit_depth,
profile,
quantization_index,
alpha_channel_type.map(AlphaCoding::matched),
interlace_mode,
None,
false,
FrameMeta::default(),
3, )
}
pub fn pad_frame_to_size(frame_bytes: &[u8], min_frame_size: u32) -> Result<Vec<u8>> {
if frame_bytes.len() < 8 {
return Err(Error::invalid(
"prores encoder: frame too short to pad (missing frame_size + 'icpf')",
));
}
let coded_len = frame_bytes.len();
if (min_frame_size as usize) <= coded_len {
return Ok(frame_bytes.to_vec());
}
let target = min_frame_size as usize;
let stuffing_size = target - coded_len;
let mut out = Vec::with_capacity(target);
out.extend_from_slice(frame_bytes);
out.resize(coded_len + stuffing_size, 0u8);
out[0..4].copy_from_slice(&min_frame_size.to_be_bytes());
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn encode_frame_with_rate_control(
frame: &VideoFrame,
img_w: u32,
img_h: u32,
chroma: ChromaFormat,
bit_depth: BitDepth,
profile: Profile,
seed_qi: u8,
qmats: Option<QuantMatrices>,
explicit_qmat_carriage: bool,
meta: FrameMeta,
target_bytes: usize,
interlace_mode: u8,
log2_slice_mb_width: u8,
alpha: Option<AlphaCoding>,
) -> Result<Vec<u8>> {
let tol_lo = (target_bytes as f64 * (1.0 - RATE_CTRL_TOLERANCE)) as usize;
let tol_hi = (target_bytes as f64 * (1.0 + RATE_CTRL_TOLERANCE)) as usize;
let seed = encode_frame_full(
frame,
img_w,
img_h,
chroma,
bit_depth,
profile,
seed_qi,
alpha,
interlace_mode,
qmats,
explicit_qmat_carriage,
meta,
log2_slice_mb_width,
)?;
if seed.len() >= tol_lo && seed.len() <= tol_hi {
return Ok(seed);
}
let (mut lo, mut hi): (u8, u8) = if seed.len() > tol_hi {
(seed_qi + 1, 224)
} else {
(1, seed_qi - 1)
};
let mut best = seed;
for _ in 0..RATE_CTRL_MAX_PASSES {
if lo > hi {
break;
}
let mid = lo + (hi - lo) / 2;
let candidate = encode_frame_full(
frame,
img_w,
img_h,
chroma,
bit_depth,
profile,
mid,
alpha,
interlace_mode,
qmats,
explicit_qmat_carriage,
meta,
log2_slice_mb_width,
)?;
let sz = candidate.len();
if sz >= tol_lo && sz <= tol_hi {
return Ok(candidate);
}
let best_dist = (best.len() as i64 - target_bytes as i64).unsigned_abs();
let cand_dist = (sz as i64 - target_bytes as i64).unsigned_abs();
if cand_dist < best_dist {
best = candidate;
}
if sz > tol_hi {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
Ok(best)
}
#[allow(clippy::too_many_arguments)]
fn encode_frame_full(
frame: &VideoFrame,
img_w: u32,
img_h: u32,
chroma: ChromaFormat,
bit_depth: BitDepth,
profile: Profile,
quantization_index: u8,
alpha: Option<AlphaCoding>,
interlace_mode: u8,
qmats: Option<QuantMatrices>,
explicit_qmat_carriage: bool,
meta: FrameMeta,
log2_slice_mb_width: u8,
) -> Result<Vec<u8>> {
if log2_slice_mb_width > 3 {
return Err(Error::invalid(
"prores encoder: log2_desired_slice_size_in_mb must be 0..=3 \
(RDD 36 §5.2.2 — two-bit picture-header field)",
));
}
let expected_planes = if alpha.is_some() { 4 } else { 3 };
if frame.planes.len() != expected_planes {
return Err(Error::invalid(format!(
"prores encoder: expected {expected_planes} planes (got {})",
frame.planes.len()
)));
}
if !(1..=224).contains(&quantization_index) {
return Err(Error::invalid(
"prores encoder: quantization_index out of range",
));
}
if profile.chroma_format() != chroma {
return Err(Error::invalid(
"prores encoder: profile chroma_format does not match requested chroma",
));
}
if let Some(qm) = &qmats {
if !qm.weights_valid() {
return Err(Error::invalid(
"prores encoder: quant matrix weight outside RDD 36 range 2..=63",
));
}
}
if img_w == 0 || img_h == 0 {
return Err(Error::invalid(
"prores encoder: frame dimensions must be non-zero (RDD 36 §6.1.1)",
));
}
if img_w > u16::MAX as u32 || img_h > u16::MAX as u32 {
return Err(Error::invalid(format!(
"prores encoder: frame dimensions {img_w}×{img_h} exceed the RDD 36 §6.1.1 \
u16 horizontal_size / vertical_size limit of 65535"
)));
}
let width = img_w as usize;
let height = img_h as usize;
let cap = output_capacity_cap(img_w as u16, img_h as u16, chroma);
let qmat_pair = qmats.unwrap_or_default();
let (load_luma, load_chroma) = if explicit_qmat_carriage {
(true, true)
} else {
qmat_pair.wire_flags()
};
let luma_qmat = &qmat_pair.luma;
let chroma_qmat = &qmat_pair.chroma;
let pictures: Vec<(usize, FieldStride)> = if interlace_mode == 0 {
vec![(height, FieldStride::progressive())]
} else {
let top_h = height.div_ceil(2);
let bot_h = height / 2;
if interlace_mode == 1 {
vec![
(top_h, FieldStride::new(2, 0)),
(bot_h, FieldStride::new(2, 1)),
]
} else {
vec![
(bot_h, FieldStride::new(2, 1)),
(top_h, FieldStride::new(2, 0)),
]
}
};
let interlaced = interlace_mode != 0;
let mut picture_blobs: Vec<Vec<u8>> = Vec::with_capacity(pictures.len());
for (picture_height, field) in &pictures {
let blob = encode_one_picture(
frame,
width,
height,
*picture_height,
chroma,
bit_depth,
quantization_index,
luma_qmat,
chroma_qmat,
alpha,
log2_slice_mb_width,
interlaced,
*field,
)?;
picture_blobs.push(blob);
}
let frame_header_size =
20usize + if load_luma { 64 } else { 0 } + if load_chroma { 64 } else { 0 };
let pictures_total: usize = picture_blobs.iter().map(|p| p.len()).sum();
let total_frame_size_no_padding = 4 + 4 + frame_header_size + pictures_total;
if total_frame_size_no_padding > cap {
return Err(Error::invalid(
"prores encoder: encoded size exceeds internal cap",
));
}
let mut out = Vec::with_capacity(total_frame_size_no_padding);
write_frame_with_meta(
&mut out,
total_frame_size_no_padding as u32,
img_w as u16,
img_h as u16,
chroma,
interlace_mode,
luma_qmat,
chroma_qmat,
load_luma,
load_chroma,
alpha.map_or(0, |a| a.act.code()),
meta,
);
for blob in &picture_blobs {
out.extend_from_slice(blob);
}
debug_assert_eq!(out.len(), total_frame_size_no_padding);
Ok(out)
}
#[derive(Copy, Clone, Debug)]
struct FieldStride {
step: usize,
offset: usize,
}
impl FieldStride {
fn new(step: usize, offset: usize) -> Self {
Self { step, offset }
}
fn progressive() -> Self {
Self { step: 1, offset: 0 }
}
fn map(self, picture_row: usize) -> usize {
self.step * picture_row + self.offset
}
}
#[allow(clippy::too_many_arguments)]
fn encode_one_picture(
frame: &VideoFrame,
frame_w: usize,
frame_h: usize,
picture_height: usize,
chroma: ChromaFormat,
bit_depth: BitDepth,
quantization_index: u8,
luma_qmat: &[u8; 64],
chroma_qmat: &[u8; 64],
alpha: Option<AlphaCoding>,
log2_slice_mb_width: u8,
interlaced: bool,
field: FieldStride,
) -> Result<Vec<u8>> {
let c_w = match chroma {
ChromaFormat::Y422 => frame_w.div_ceil(2),
ChromaFormat::Y444 => frame_w,
};
let mbs_x = frame_w.div_ceil(MB_SIDE_PX);
let mbs_y = picture_height.div_ceil(MB_SIDE_PX);
let slice_sizes_template = compute_slice_sizes(mbs_x, log2_slice_mb_width);
let slices_per_row = slice_sizes_template.len();
let slice_count = slices_per_row * mbs_y;
let _cb_per_mb = chroma_blocks_per_mb(chroma);
let per_mb = blocks_per_mb(chroma);
const LUMA_OFFSETS: [(usize, usize); 4] = [(0, 0), (1, 0), (0, 1), (1, 1)];
let chroma_offsets: &[(usize, usize)] = match chroma {
ChromaFormat::Y422 => &[(0, 0), (0, 1)],
ChromaFormat::Y444 => &LUMA_OFFSETS,
};
let mut slice_payloads: Vec<Vec<u8>> = Vec::with_capacity(slice_count);
for my in 0..mbs_y {
let mut mx = 0usize;
for &mbs_this_slice in &slice_sizes_template {
let mbs_this_slice = mbs_this_slice.min(mbs_x - mx);
if mbs_this_slice == 0 {
break;
}
let mut blocks: Vec<[i32; 64]> = Vec::with_capacity(mbs_this_slice * per_mb);
for mb_within in 0..mbs_this_slice {
let mb_x = mx + mb_within;
for (bx, by) in LUMA_OFFSETS {
let x0 = mb_x * MB_SIDE_PX + bx * 8;
let y0 = my * MB_SIDE_PX + by * 8;
blocks.push(encode_block(
&frame.planes[0].data,
frame.planes[0].stride,
frame_w,
frame_h,
x0,
y0,
luma_qmat,
quantization_index,
bit_depth,
field,
));
}
for plane_idx in [1usize, 2] {
for (bx, by) in chroma_offsets.iter().copied() {
let (x0, y0) = match chroma {
ChromaFormat::Y422 => (mb_x * 8, my * MB_SIDE_PX + by * 8),
ChromaFormat::Y444 => {
(mb_x * MB_SIDE_PX + bx * 8, my * MB_SIDE_PX + by * 8)
}
};
blocks.push(encode_block(
&frame.planes[plane_idx].data,
frame.planes[plane_idx].stride,
c_w,
frame_h,
x0,
y0,
chroma_qmat,
quantization_index,
bit_depth,
field,
));
}
}
}
let (y_data, cb_data, cr_data) =
encode_slice_components(mbs_this_slice, chroma, interlaced, &blocks)?;
if y_data.len() > u16::MAX as usize
|| cb_data.len() > u16::MAX as usize
|| cr_data.len() > u16::MAX as usize
{
return Err(Error::invalid(
"prores encoder: slice component exceeded u16 size limit",
));
}
let alpha_blob: Vec<u8> = if let Some(ac) = alpha {
let slice_vertical_size = MB_SIDE_PX;
let cols = MB_SIDE_PX * mbs_this_slice;
let mut samples: Vec<u16> = Vec::with_capacity(cols * slice_vertical_size);
let a_plane = &frame.planes[3];
let a_stride = a_plane.stride;
for r in 0..slice_vertical_size {
let frame_row = field
.map(my * MB_SIDE_PX + r)
.min(frame_h.saturating_sub(1));
for c in 0..cols {
let x = (mx * MB_SIDE_PX + c).min(frame_w.saturating_sub(1));
let raw =
read_alpha_input(&a_plane.data, a_stride, x, frame_row, ac.input_depth);
samples.push(alpha_input_to_coded(raw, ac.input_depth, ac.act));
}
}
encode_scanned_alpha(&samples, ac.act)?
} else {
Vec::new()
};
let cr_field = if alpha.is_some() {
Some(cr_data.len() as u16)
} else {
None
};
let mut slice_buf = Vec::with_capacity(
8 + y_data.len() + cb_data.len() + cr_data.len() + alpha_blob.len(),
);
write_slice_header(
&mut slice_buf,
quantization_index,
y_data.len() as u16,
cb_data.len() as u16,
cr_field,
);
slice_buf.extend_from_slice(&y_data);
slice_buf.extend_from_slice(&cb_data);
slice_buf.extend_from_slice(&cr_data);
slice_buf.extend_from_slice(&alpha_blob);
slice_payloads.push(slice_buf);
mx += mbs_this_slice;
}
}
debug_assert_eq!(slice_payloads.len(), slice_count);
if slice_payloads.iter().any(|p| p.len() > u16::MAX as usize) {
return Err(Error::invalid(
"prores encoder: slice exceeded u16 size table limit",
));
}
let slice_table_size = slice_count * 2;
let slice_bytes: usize = slice_payloads.iter().map(|p| p.len()).sum();
let picture_header_size = 8usize;
let picture_size = (picture_header_size + slice_table_size + slice_bytes) as u32;
let mut blob = Vec::with_capacity(picture_size as usize);
write_picture_header(
&mut blob,
picture_size,
if slice_count <= u16::MAX as usize {
slice_count as u16
} else {
0
},
log2_slice_mb_width,
);
for p in &slice_payloads {
blob.extend_from_slice(&(p.len() as u16).to_be_bytes());
}
for p in &slice_payloads {
blob.extend_from_slice(p);
}
debug_assert_eq!(blob.len(), picture_size as usize);
Ok(blob)
}
fn read_sample(plane: &[u8], stride: usize, x: usize, y: usize, bit_depth: BitDepth) -> f32 {
match bit_depth {
BitDepth::Eight => (plane[y * stride + x] as f32) * 2.0 - 256.0,
BitDepth::Ten => {
let off = y * stride + x * 2;
let lo = plane[off] as u16;
let hi = plane[off + 1] as u16;
let s = (lo | (hi << 8)) & 0x03FF;
(s as f32) / 2.0 - 256.0
}
BitDepth::Twelve => {
let off = y * stride + x * 2;
let lo = plane[off] as u16;
let hi = plane[off + 1] as u16;
let s = (lo | (hi << 8)) & 0x0FFF;
(s as f32) / 8.0 - 256.0
}
BitDepth::Sixteen => {
let off = y * stride + x * 2;
let lo = plane[off] as u16;
let hi = plane[off + 1] as u16;
let s = lo | (hi << 8);
(s as f32) / 128.0 - 256.0
}
}
}
#[allow(clippy::too_many_arguments)]
fn encode_block(
plane: &[u8],
stride: usize,
plane_w: usize,
plane_h: usize,
x0: usize,
y0: usize,
qmat: &[u8; 64],
quantization_index: u8,
bit_depth: BitDepth,
field: FieldStride,
) -> [i32; 64] {
let mut blk = [0.0f32; 64];
for j in 0..8 {
let frame_row = field.map(y0 + j).min(plane_h.saturating_sub(1));
for i in 0..8 {
let x = (x0 + i).min(plane_w.saturating_sub(1));
blk[j * 8 + i] = read_sample(plane, stride, x, frame_row, bit_depth);
}
}
if is_constant_block(&blk) {
fdct8x8_constant(&mut blk);
} else {
fdct8x8(&mut blk);
}
let qs = qscale(quantization_index) as f32;
let mut out = [0i32; 64];
for k in 0..64 {
let denom = qmat[k] as f32 * qs;
let v = blk[k] * 8.0 / denom;
out[k] = if v >= 0.0 {
(v + 0.5) as i32
} else {
-((-v + 0.5) as i32)
};
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decoder::decode_packet;
use crate::frame::parse_frame;
use oxideav_core::frame::VideoPlane;
use oxideav_core::{CodecId, CodecParameters, Frame, MediaType, PixelFormat};
fn field_distinct_422(width: u32, height: u32) -> VideoFrame {
let w = width as usize;
let h = height as usize;
let cw = w / 2;
let mut y = vec![0u8; w * h];
let cb = vec![128u8; cw * h];
let cr = vec![128u8; cw * h];
for j in 0..h {
for i in 0..w {
let base: i32 = if j % 2 == 0 { 170 } else { 90 };
let grad = ((i + j) % 32) as i32;
y[j * w + i] = (base + grad - 8).clamp(16, 235) as u8;
}
}
VideoFrame {
pts: Some(0),
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: cw,
data: cb,
},
VideoPlane {
stride: cw,
data: cr,
},
],
}
}
fn enc_params(width: u32, height: u32) -> CodecParameters {
let mut p = CodecParameters::video(CodecId::new(crate::CODEC_ID_STR));
p.media_type = MediaType::Video;
p.width = Some(width);
p.height = Some(height);
p.pixel_format = Some(PixelFormat::Yuv422P);
p
}
#[test]
fn config_interlace_mode_default_is_progressive() {
assert_eq!(EncoderConfig::default().interlace_mode, 0);
assert_eq!(
EncoderConfig::default()
.with_interlace_mode(1)
.interlace_mode,
1
);
assert_eq!(
EncoderConfig::default()
.with_interlace_mode(2)
.interlace_mode,
2
);
}
#[test]
fn config_interlace_mode_3_rejected_at_construction() {
let params = enc_params(64, 48);
let cfg = EncoderConfig::default().with_interlace_mode(3);
let msg = match make_encoder_with_config(¶ms, cfg) {
Ok(_) => panic!("interlace_mode 3 must be rejected (RDD 36 Table 2 reserved)"),
Err(e) => format!("{e}"),
};
assert!(
msg.contains("interlace_mode"),
"error must name interlace_mode, got: {msg}"
);
}
#[test]
fn send_frame_progressive_default_emits_one_picture() {
let params = enc_params(64, 48);
let mut enc = make_encoder(¶ms).expect("make_encoder");
enc.send_frame(&Frame::Video(field_distinct_422(64, 48)))
.expect("send_frame");
let pkt = enc.receive_packet().expect("receive_packet");
let (fh, _) = parse_frame(&pkt.data).expect("parse frame");
assert_eq!(fh.interlace_mode, 0);
assert_eq!(fh.picture_count(), 1);
}
fn send_frame_interlaced_roundtrips(interlace_mode: u8) {
let (w, h) = (64u32, 48u32);
let src = field_distinct_422(w, h);
let params = enc_params(w, h);
let cfg = EncoderConfig::default().with_interlace_mode(interlace_mode);
let mut enc = make_encoder_with_config(¶ms, cfg).expect("make_encoder_with_config");
enc.send_frame(&Frame::Video(src.clone()))
.expect("send_frame");
let pkt = enc.receive_packet().expect("receive_packet");
let (fh, _) = parse_frame(&pkt.data).expect("parse frame");
assert_eq!(fh.interlace_mode, interlace_mode, "header interlace_mode");
assert_eq!(fh.picture_count(), 2, "interlaced frame carries 2 pictures");
let decoded = decode_packet(&pkt.data, Some(0)).expect("decode_packet");
let dy = &decoded.planes[0].data;
let stride = decoded.planes[0].stride;
let mut even_sum = 0u64;
let mut odd_sum = 0u64;
for j in 0..(h as usize) {
let mut row = 0u64;
for i in 0..(w as usize) {
row += dy[j * stride + i] as u64;
}
if j % 2 == 0 {
even_sum += row;
} else {
odd_sum += row;
}
}
assert!(
even_sum > odd_sum,
"interlace_mode {interlace_mode}: even-row sum {even_sum} not > odd-row sum \
{odd_sum} (field assignment swapped?)"
);
}
#[test]
fn send_frame_interlaced_tff_roundtrips() {
send_frame_interlaced_roundtrips(1);
}
#[test]
fn send_frame_interlaced_bff_roundtrips() {
send_frame_interlaced_roundtrips(2);
}
#[test]
fn send_frame_interlaced_with_rate_control_keeps_field_order() {
let (w, h) = (64u32, 48u32);
let src = field_distinct_422(w, h);
let mut params = enc_params(w, h);
params.bit_rate = Some(50_000_000);
params.frame_rate = Some(oxideav_core::Rational::new(25, 1));
let cfg = EncoderConfig::default()
.with_interlace_mode(1)
.with_rate_control();
let mut enc = make_encoder_with_config(¶ms, cfg).expect("make_encoder_with_config");
enc.send_frame(&Frame::Video(src)).expect("send_frame");
let pkt = enc.receive_packet().expect("receive_packet");
let (fh, _) = parse_frame(&pkt.data).expect("parse frame");
assert_eq!(fh.interlace_mode, 1);
assert_eq!(fh.picture_count(), 2);
}
#[test]
fn encode_block_constant_input_matches_general_path() {
let plane = vec![200u8; 8 * 8];
let qmat = [4u8; 64];
let qi = 4u8;
let out = super::encode_block(
&plane,
8,
8,
8,
0,
0,
&qmat,
qi,
super::BitDepth::Eight,
super::FieldStride::progressive(),
);
assert_eq!(out[0], 576, "constant-block DC matches the closed form");
for k in 1..64 {
assert_eq!(
out[k], 0,
"AC[{k}] must be exactly 0 after the constant-block fast path"
);
}
}
#[test]
fn constant_flat_frame_decodes_pixel_exact_at_hq() {
use crate::decoder::decode_packet;
for &v in &[16u8, 64, 128, 200, 235] {
let (w, h) = (64u32, 48u32);
let wu = w as usize;
let hu = h as usize;
let cwu = wu / 2;
let src = VideoFrame {
pts: Some(0),
planes: vec![
VideoPlane {
stride: wu,
data: vec![v; wu * hu],
},
VideoPlane {
stride: cwu,
data: vec![128u8; cwu * hu],
},
VideoPlane {
stride: cwu,
data: vec![128u8; cwu * hu],
},
],
};
let mut params = enc_params(w, h);
params.bit_rate = Some(220_000_000); let mut enc = make_encoder(¶ms).expect("make_encoder");
enc.send_frame(&Frame::Video(src)).expect("send_frame");
let pkt = enc.receive_packet().expect("receive_packet");
let out = decode_packet(&pkt.data, None).expect("decode_packet");
for j in 0..hu {
for i in 0..wu {
let got = out.planes[0].data[j * out.planes[0].stride + i];
assert_eq!(got, v, "v={v}, pos=({i},{j}): expected {v}, got {got}");
}
}
}
}
}
#[cfg(test)]
mod alpha_input_tests {
use super::{alpha_input_to_coded, read_alpha_input, AlphaChannelType, BitDepth};
#[test]
fn matched_width_conversion_is_identity() {
for a in (0u32..=255).step_by(7).chain([255]) {
assert_eq!(
alpha_input_to_coded(a as u16, BitDepth::Eight, AlphaChannelType::Eight),
a as u16
);
}
for a in (0u32..=65535).step_by(251).chain([65535]) {
assert_eq!(
alpha_input_to_coded(a as u16, BitDepth::Sixteen, AlphaChannelType::Sixteen),
a as u16
);
}
}
#[test]
fn deep_promotion_roundtrips_losslessly() {
for (depth, max_in) in [(BitDepth::Ten, 1023u32), (BitDepth::Twelve, 4095u32)] {
for a in 0..=max_in {
let coded = alpha_input_to_coded(a as u16, depth, AlphaChannelType::Sixteen);
let want = ((65535u64 * a as u64 + max_in as u64 / 2) / max_in as u64) as u16;
assert_eq!(coded, want, "promotion mismatch at {depth:?} alpha {a}");
let back = ((max_in as u64 * coded as u64 * 2 + 65535) / (65535 * 2)) as u32;
assert_eq!(back, a, "round-trip mismatch at {depth:?} alpha {a}");
}
}
}
#[test]
fn promotion_endpoints_hit_full_scale() {
for depth in [BitDepth::Ten, BitDepth::Twelve, BitDepth::Sixteen] {
assert_eq!(alpha_input_to_coded(0, depth, AlphaChannelType::Sixteen), 0);
assert_eq!(
alpha_input_to_coded(depth.max_value() as u16, depth, AlphaChannelType::Sixteen),
65535
);
}
}
#[test]
fn plane_reads_honour_depth_and_stride() {
let plane8 = [10u8, 20, 30, 40, 50, 60];
assert_eq!(read_alpha_input(&plane8, 3, 2, 1, BitDepth::Eight), 60);
let w = |v: u16| v.to_le_bytes();
let mut plane16 = Vec::new();
for v in [0x1234u16, 0xFFFF, 0x8001, 0x03FF] {
plane16.extend_from_slice(&w(v));
}
assert_eq!(
read_alpha_input(&plane16, 4, 1, 0, BitDepth::Sixteen),
0xFFFF
);
assert_eq!(
read_alpha_input(&plane16, 4, 0, 1, BitDepth::Ten),
0x8001 & 0x03FF
);
assert_eq!(
read_alpha_input(&plane16, 4, 1, 1, BitDepth::Twelve),
0x03FF
);
}
}