use std::sync::Arc;
use gpui::RenderImage;
use image::{Frame, RgbaImage};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VideoFrame {
width: u32,
height: u32,
bgra: Vec<u8>,
}
impl VideoFrame {
pub fn rgba(width: u32, height: u32, mut data: Vec<u8>) -> Option<Self> {
if !fits(width, height, data.len(), 4) {
return None;
}
for px in data.chunks_exact_mut(4) {
px.swap(0, 2);
}
Some(VideoFrame {
width,
height,
bgra: data,
})
}
pub fn bgra(width: u32, height: u32, data: Vec<u8>) -> Option<Self> {
if !fits(width, height, data.len(), 4) {
return None;
}
Some(VideoFrame {
width,
height,
bgra: data,
})
}
pub fn i420(width: u32, height: u32, y: &[u8], u: &[u8], v: &[u8]) -> Option<Self> {
let (w, h) = (width as usize, height as usize);
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
if w == 0 || h == 0 || y.len() != w * h || u.len() != cw * ch || v.len() != cw * ch {
return None;
}
let mut bgra = Vec::with_capacity(w * h * 4);
for row in 0..h {
for col in 0..w {
let luma = i32::from(y[row * w + col]) - 16;
let ci = (row / 2) * cw + col / 2;
let d = i32::from(u[ci]) - 128;
let e = i32::from(v[ci]) - 128;
let c = 298 * luma;
let clamp = |x: i32| (x >> 8).clamp(0, 255) as u8;
bgra.extend_from_slice(&[
clamp(c + 516 * d + 128),
clamp(c - 100 * d - 208 * e + 128),
clamp(c + 409 * e + 128),
255,
]);
}
}
Some(VideoFrame {
width,
height,
bgra,
})
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn as_bgra(&self) -> &[u8] {
&self.bgra
}
pub(crate) fn into_image(self) -> Option<Arc<RenderImage>> {
let buffer = RgbaImage::from_raw(self.width, self.height, self.bgra)?;
Some(Arc::new(RenderImage::new(vec![Frame::new(buffer)])))
}
}
fn fits(width: u32, height: u32, len: usize, bytes: usize) -> bool {
width > 0 && height > 0 && len == width as usize * height as usize * bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgba_is_stored_as_bgra() {
let f = VideoFrame::rgba(2, 1, vec![1, 2, 3, 255, 10, 20, 30, 255]).unwrap();
assert_eq!(f.as_bgra(), &[3, 2, 1, 255, 30, 20, 10, 255]);
}
#[test]
fn bgra_is_kept_as_is() {
let f = VideoFrame::bgra(1, 1, vec![9, 8, 7, 6]).unwrap();
assert_eq!(f.as_bgra(), &[9, 8, 7, 6]);
}
#[test]
fn a_wrong_sized_buffer_is_refused() {
assert!(VideoFrame::rgba(2, 2, vec![0; 15]).is_none());
assert!(VideoFrame::bgra(0, 4, vec![]).is_none());
assert!(VideoFrame::i420(2, 2, &[0; 4], &[0; 1], &[0; 2]).is_none());
}
#[test]
fn i420_maps_the_limited_range_extremes() {
let black = VideoFrame::i420(2, 2, &[16; 4], &[128], &[128]).unwrap();
assert_eq!(&black.as_bgra()[..4], &[0, 0, 0, 255]);
let white = VideoFrame::i420(2, 2, &[235; 4], &[128], &[128]).unwrap();
assert_eq!(&white.as_bgra()[..4], &[255, 255, 255, 255]);
}
#[test]
fn i420_puts_red_chroma_in_the_red_channel() {
let f = VideoFrame::i420(2, 2, &[81; 4], &[90], &[240]).unwrap();
let [b, g, r, a] = f.as_bgra()[..4] else {
unreachable!()
};
assert!(r > 240 && g < 20 && b < 20 && a == 255, "{r} {g} {b}");
}
#[test]
fn i420_takes_odd_dimensions() {
let f = VideoFrame::i420(3, 3, &[16; 9], &[128; 4], &[128; 4]).unwrap();
assert_eq!(f.as_bgra().len(), 3 * 3 * 4);
}
}