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;
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, DEFAULT_QMAT};
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 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 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 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;
const SLICE_MB_WIDTH_LOG2: u8 = 3;
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)",
));
}
}
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) = match pix {
PixelFormat::Yuv422P => (ChromaFormat::Y422, BitDepth::Eight),
PixelFormat::Yuv444P => (ChromaFormat::Y444, BitDepth::Eight),
PixelFormat::Yuv422P10Le => (ChromaFormat::Y422, BitDepth::Ten),
PixelFormat::Yuv444P10Le => (ChromaFormat::Y444, BitDepth::Ten),
PixelFormat::Yuv422P12Le => (ChromaFormat::Y422, BitDepth::Twelve),
PixelFormat::Yuv444P12Le => (ChromaFormat::Y444, BitDepth::Twelve),
other => {
return Err(Error::unsupported(format!(
"prores encoder: pixel format {other:?} not supported \
(expected Yuv4(2|4)4P / Yuv4(2|4)4P10Le / Yuv4(2|4)4P12Le)"
)));
}
};
let profile = 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
};
Ok(Box::new(ProResEncoder {
output_params,
width,
height,
chroma,
bit_depth,
profile,
quant_index,
meta,
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,
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) => {
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.meta,
self.target_bytes,
)?
} else {
encode_frame_full(
v,
self.width,
self.height,
self.chroma,
self.bit_depth,
self.profile,
self.quant_index,
None,
0,
self.config.quant_matrices,
self.meta,
)?
};
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),
FrameMeta::default(),
)
}
#[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,
0,
None,
FrameMeta::default(),
)
}
#[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,
interlace_mode,
None,
FrameMeta::default(),
)
}
#[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>,
meta: FrameMeta,
target_bytes: usize,
) -> 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, None, 0, qmats, meta,
)?;
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, 224)
} else {
(1, seed_qi)
};
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, None, 0, qmats, meta,
)?;
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 {
if mid == 0 {
break;
}
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_channel_type: Option<AlphaChannelType>,
interlace_mode: u8,
qmats: Option<QuantMatrices>,
meta: FrameMeta,
) -> Result<Vec<u8>> {
let expected_planes = if alpha_channel_type.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",
));
}
}
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 = !qmat_pair.is_default();
let load_chroma = load_luma && qmat_pair.chroma != qmat_pair.luma;
let luma_qmat = if load_luma {
&qmat_pair.luma
} else {
&DEFAULT_QMAT
};
let chroma_qmat = if load_luma {
&qmat_pair.chroma
} else {
&DEFAULT_QMAT
};
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_channel_type,
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_channel_type.map_or(0, |a| a.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_channel_type: Option<AlphaChannelType>,
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, SLICE_MB_WIDTH_LOG2);
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(act) = alpha_channel_type {
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 v: u16 = match act {
AlphaChannelType::Eight => {
a_plane.data[frame_row * a_stride + x] as u16
}
AlphaChannelType::Sixteen => {
let off = frame_row * a_stride + x * 2;
u16::from_le_bytes([a_plane.data[off], a_plane.data[off + 1]])
}
};
samples.push(v);
}
}
encode_scanned_alpha(&samples, act)?
} else {
Vec::new()
};
let cr_field = if alpha_channel_type.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
},
SLICE_MB_WIDTH_LOG2,
);
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
}
}
}
#[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);
}
}
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
}