use std::collections::VecDeque;
use oxideav_codec::Encoder;
use oxideav_core::{
CodecId, CodecParameters, Error, Frame, MediaType, Packet, PixelFormat, Rational, Result,
TimeBase, VideoFrame, VideoPlane,
};
use oxideav_vp8::encoder::{encode_keyframe, DEFAULT_QINDEX};
use crate::riff::{build_webp_file, AlphChunkBytes, ImageKind, WebpMetadata};
use crate::vp8l::encode_vp8l_argb;
use crate::CODEC_ID_VP8;
pub fn make_encoder(params: &CodecParameters) -> Result<Box<dyn Encoder>> {
make_encoder_with_qindex(params, DEFAULT_QINDEX)
}
pub fn make_encoder_with_qindex(params: &CodecParameters, qindex: u8) -> Result<Box<dyn Encoder>> {
let width = params
.width
.ok_or_else(|| Error::invalid("VP8 WebP encoder: missing width"))?;
let height = params
.height
.ok_or_else(|| Error::invalid("VP8 WebP encoder: missing height"))?;
if width == 0 || height == 0 || width > 16383 || height > 16383 {
return Err(Error::invalid(format!(
"VP8 WebP encoder: dimensions {width}x{height} out of range (1..=16383)"
)));
}
let pix = params.pixel_format.unwrap_or(PixelFormat::Yuv420P);
if pix != PixelFormat::Yuv420P && pix != PixelFormat::Rgba {
return Err(Error::unsupported(format!(
"VP8 WebP encoder: pixel format {pix:?} not supported — feed Yuv420P or Rgba"
)));
}
let frame_rate = params.frame_rate.unwrap_or(Rational::new(1, 1));
let mut output_params = params.clone();
output_params.media_type = MediaType::Video;
output_params.codec_id = CodecId::new(CODEC_ID_VP8);
output_params.pixel_format = Some(pix);
output_params.width = Some(width);
output_params.height = Some(height);
output_params.frame_rate = Some(frame_rate);
let time_base = TimeBase::new(1, 1000);
Ok(Box::new(Vp8WebpEncoder {
output_params,
width,
height,
qindex: qindex.min(127),
input_format: pix,
time_base,
pending: VecDeque::new(),
eof: false,
}))
}
struct Vp8WebpEncoder {
output_params: CodecParameters,
width: u32,
height: u32,
qindex: u8,
input_format: PixelFormat,
time_base: TimeBase,
pending: VecDeque<Packet>,
eof: bool,
}
impl Encoder for Vp8WebpEncoder {
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<()> {
let v = match frame {
Frame::Video(v) => v,
_ => return Err(Error::invalid("VP8 WebP encoder: video frames only")),
};
if v.width != self.width || v.height != self.height {
return Err(Error::invalid(format!(
"VP8 WebP encoder: frame dims {}x{} do not match encoder {}x{}",
v.width, v.height, self.width, self.height
)));
}
if v.format != self.input_format {
return Err(Error::unsupported(format!(
"VP8 WebP encoder: frame format {:?} must match encoder input \
{:?} — rebuild the encoder with the correct pixel format",
v.format, self.input_format
)));
}
let bytes = match v.format {
PixelFormat::Yuv420P => {
let vp8 = encode_keyframe(self.width, self.height, self.qindex, v)?;
build_webp_file(
ImageKind::Vp8Lossy,
&vp8,
self.width,
self.height,
None,
&WebpMetadata::default(),
)
}
PixelFormat::Rgba => encode_rgba_lossy(self.width, self.height, self.qindex, v)?,
other => {
return Err(Error::unsupported(format!(
"VP8 WebP encoder: frame format {other:?} unsupported"
)))
}
};
let mut pkt = Packet::new(0, self.time_base, bytes);
pkt.pts = v.pts;
pkt.dts = pkt.pts;
pkt.flags.keyframe = true;
self.pending.push_back(pkt);
Ok(())
}
fn receive_packet(&mut self) -> Result<Packet> {
if let Some(p) = self.pending.pop_front() {
return Ok(p);
}
if self.eof {
Err(Error::Eof)
} else {
Err(Error::NeedMore)
}
}
fn flush(&mut self) -> Result<()> {
self.eof = true;
Ok(())
}
}
fn encode_rgba_lossy(width: u32, height: u32, qindex: u8, v: &VideoFrame) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if v.planes.is_empty() {
return Err(Error::invalid("VP8 WebP encoder: RGBA frame has no planes"));
}
let plane = &v.planes[0];
if plane.stride < w * 4 {
return Err(Error::invalid(
"VP8 WebP encoder: RGBA stride too small for frame width",
));
}
let mut alpha = Vec::with_capacity(w * h);
let (y, u, v_chroma) = rgba_rows_to_yuv420(w, h, plane.stride, &plane.data, &mut alpha);
let yuv_frame = VideoFrame {
format: PixelFormat::Yuv420P,
width,
height,
pts: v.pts,
time_base: v.time_base,
planes: vec![
VideoPlane { stride: w, data: y },
VideoPlane {
stride: w / 2 + (w & 1),
data: u,
},
VideoPlane {
stride: w / 2 + (w & 1),
data: v_chroma,
},
],
};
let vp8_bytes = encode_keyframe(width, height, qindex, &yuv_frame)?;
let alph_payload = encode_alpha_plane_as_vp8l(width, height, &alpha)?;
let alph = AlphChunkBytes {
header_byte: 1,
payload: alph_payload,
};
Ok(build_webp_file(
ImageKind::Vp8Lossy,
&vp8_bytes,
width,
height,
Some(&alph),
&WebpMetadata::default(),
))
}
fn rgba_rows_to_yuv420(
w: usize,
h: usize,
stride: usize,
rgba: &[u8],
alpha: &mut Vec<u8>,
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let cw = w / 2 + (w & 1);
let ch = h / 2 + (h & 1);
let mut y_plane = vec![0u8; w * h];
let mut u_plane = vec![0u8; cw * ch];
let mut v_plane = vec![0u8; cw * ch];
for j in 0..h {
let row_start = j * stride;
for i in 0..w {
let px = &rgba[row_start + i * 4..row_start + i * 4 + 4];
let r = px[0] as i32;
let g = px[1] as i32;
let b = px[2] as i32;
alpha.push(px[3]);
let y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
y_plane[j * w + i] = y.clamp(0, 255) as u8;
}
}
for cy in 0..ch {
for cx in 0..cw {
let mut u_sum = 0i32;
let mut v_sum = 0i32;
let mut n = 0i32;
for dy in 0..2 {
let jj = cy * 2 + dy;
if jj >= h {
break;
}
for dx in 0..2 {
let ii = cx * 2 + dx;
if ii >= w {
break;
}
let px = &rgba[jj * stride + ii * 4..jj * stride + ii * 4 + 4];
let r = px[0] as i32;
let g = px[1] as i32;
let b = px[2] as i32;
u_sum += (-38 * r - 74 * g + 112 * b + 128) >> 8;
v_sum += (112 * r - 94 * g - 18 * b + 128) >> 8;
n += 1;
}
}
let u = (u_sum / n) + 128;
let v = (v_sum / n) + 128;
u_plane[cy * cw + cx] = u.clamp(0, 255) as u8;
v_plane[cy * cw + cx] = v.clamp(0, 255) as u8;
}
}
(y_plane, u_plane, v_plane)
}
fn encode_alpha_plane_as_vp8l(width: u32, height: u32, alpha: &[u8]) -> Result<Vec<u8>> {
debug_assert_eq!(alpha.len(), (width as usize) * (height as usize));
let mut pixels = Vec::with_capacity(alpha.len());
for &a in alpha {
let g = a as u32;
pixels.push(0xff00_0000 | (g << 8));
}
let full_bitstream = encode_vp8l_argb(width, height, &pixels, false)?;
if full_bitstream.len() <= 5 {
return Err(Error::invalid(
"VP8 WebP encoder: VP8L alpha bitstream too short to strip header",
));
}
Ok(full_bitstream[5..].to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn riff_wrapper_layout_even_payload() {
let payload = vec![0xAAu8; 10];
let out = build_webp_file(
ImageKind::Vp8Lossy,
&payload,
16,
16,
None,
&WebpMetadata::default(),
);
assert_eq!(&out[0..4], b"RIFF");
assert_eq!(&out[8..12], b"WEBP");
assert_eq!(&out[12..16], b"VP8 ");
let riff_size = u32::from_le_bytes([out[4], out[5], out[6], out[7]]);
assert_eq!(riff_size, 22);
let chunk_len = u32::from_le_bytes([out[16], out[17], out[18], out[19]]);
assert_eq!(chunk_len, 10);
assert_eq!(&out[20..30], &payload[..]);
assert_eq!(out.len(), 30);
}
#[test]
fn riff_wrapper_layout_odd_payload_pads() {
let payload = vec![0x55u8; 11];
let out = build_webp_file(
ImageKind::Vp8Lossy,
&payload,
16,
16,
None,
&WebpMetadata::default(),
);
let riff_size = u32::from_le_bytes([out[4], out[5], out[6], out[7]]);
assert_eq!(riff_size, 24);
assert_eq!(out.len(), 32);
assert_eq!(out[31], 0x00);
}
}