use anyhow::{Context, Result, anyhow};
use bytes::Bytes;
use codec::decode;
use codec::frame::{PixelFormat, VideoFrame};
use container::streaming;
pub const DEFAULT_THUMBNAIL_FRACTION: f64 = 0.10;
pub const DEFAULT_THUMBNAIL_QUALITY: f32 = 65.0;
pub const DEFAULT_THUMBNAIL_SPEED: u8 = 8;
#[derive(Debug, Clone)]
pub struct ThumbnailOutput {
pub bytes: Vec<u8>,
pub width: u32,
pub height: u32,
}
pub fn generate_thumbnail(
input_data: &Bytes,
fraction: f64,
quality: f32,
speed: u8,
) -> Result<ThumbnailOutput> {
let frame = capture_frame_at_fraction(input_data, fraction)
.context("capturing thumbnail source frame")?;
let (rgb, width, height) = yuv420p_to_rgb8(&frame).context("converting YUV → RGB")?;
let avif =
encode_avif_rgb(&rgb, width, height, quality, speed).context("encoding AVIF still")?;
Ok(ThumbnailOutput {
bytes: avif,
width,
height,
})
}
fn capture_frame_at_fraction(input_data: &Bytes, fraction: f64) -> Result<VideoFrame> {
let mut demuxer =
streaming::demux_streaming(input_data).context("demuxing for thumbnail capture")?;
let header = demuxer.header().clone();
let total_frames = header.info.total_frames.max(1);
let target_idx = ((total_frames as f64) * fraction.clamp(0.0, 0.999)) as u64;
let mut decoder =
decode::create_decoder(&header.codec, header.info).context("creating thumbnail decoder")?;
let mut current_idx: u64 = 0;
let mut last_frame: Option<VideoFrame> = None;
loop {
match demuxer
.next_video_sample()
.context("demuxing next video sample for thumbnail")?
{
Some(sample) => {
decoder
.push_sample(&sample.data)
.context("pushing sample to thumbnail decoder")?;
while let Some(frame) = decoder
.decode_next()
.context("decoding frame for thumbnail")?
{
last_frame = Some(frame);
if current_idx >= target_idx {
return last_frame.ok_or_else(|| anyhow!("frame slot vanished"));
}
current_idx += 1;
}
}
None => {
decoder.finish().context("decoder finish for thumbnail")?;
while let Some(frame) = decoder
.decode_next()
.context("decoding frame after finish for thumbnail")?
{
last_frame = Some(frame);
if current_idx >= target_idx {
return last_frame.ok_or_else(|| anyhow!("frame slot vanished"));
}
current_idx += 1;
}
break;
}
}
}
last_frame.ok_or_else(|| anyhow!("source produced no decoded frames"))
}
fn yuv420p_to_rgb8(frame: &VideoFrame) -> Result<(Vec<u8>, u32, u32)> {
if frame.format != PixelFormat::Yuv420p {
return Err(anyhow!("thumbnail expects Yuv420p, got {:?}", frame.format));
}
let w = frame.width as usize;
let h = frame.height as usize;
if w == 0 || h == 0 {
return Err(anyhow!("thumbnail frame has zero dimension"));
}
let y_size = w * h;
let cw = w / 2;
let ch = h / 2;
let c_size = cw * ch;
let data = frame.data.as_ref();
if data.len() < y_size + 2 * c_size {
return Err(anyhow!(
"thumbnail frame plane buffer truncated: data={} expected≥{}",
data.len(),
y_size + 2 * c_size
));
}
let y_plane = &data[0..y_size];
let u_plane = &data[y_size..y_size + c_size];
let v_plane = &data[y_size + c_size..y_size + 2 * c_size];
let mut rgb = Vec::with_capacity(w * h * 3);
for row in 0..h {
let cy = row / 2;
for col in 0..w {
let cx = col / 2;
let y = y_plane[row * w + col] as f32;
let u = u_plane[cy * cw + cx] as f32;
let v = v_plane[cy * cw + cx] as f32;
let y1 = (y - 16.0) * 1.164_383_5;
let cb = u - 128.0;
let cr = v - 128.0;
let r = y1 + 1.792_741_1 * cr;
let g = y1 - 0.213_248_5 * cb - 0.532_909_3 * cr;
let b = y1 + 2.112_401_8 * cb;
rgb.push(clamp_u8(r));
rgb.push(clamp_u8(g));
rgb.push(clamp_u8(b));
}
}
Ok((rgb, frame.width, frame.height))
}
fn clamp_u8(v: f32) -> u8 {
if v <= 0.0 {
0
} else if v >= 255.0 {
255
} else {
v.round() as u8
}
}
fn encode_avif_rgb(
rgb: &[u8],
width: u32,
height: u32,
quality: f32,
speed: u8,
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if rgb.len() != w * h * 3 {
return Err(anyhow!(
"avif rgb buffer size mismatch: {} vs {}",
rgb.len(),
w * h * 3
));
}
let pixels: &[rgb::Rgb<u8>] =
unsafe { std::slice::from_raw_parts(rgb.as_ptr() as *const rgb::Rgb<u8>, w * h) };
let img = ravif::Img::new(pixels, w, h);
let encoded = ravif::Encoder::new()
.with_quality(quality)
.with_speed(speed)
.encode_rgb(img)
.map_err(|e| anyhow!("ravif encode failed: {e}"))?;
Ok(encoded.avif_file)
}