1use std::sync::Arc;
10
11use gpui::RenderImage;
12use image::{Frame, RgbaImage};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct VideoFrame {
17 width: u32,
18 height: u32,
19 bgra: Vec<u8>,
20}
21
22impl VideoFrame {
23 pub fn rgba(width: u32, height: u32, mut data: Vec<u8>) -> Option<Self> {
26 if !fits(width, height, data.len(), 4) {
27 return None;
28 }
29 for px in data.chunks_exact_mut(4) {
30 px.swap(0, 2);
31 }
32 Some(VideoFrame {
33 width,
34 height,
35 bgra: data,
36 })
37 }
38
39 pub fn bgra(width: u32, height: u32, data: Vec<u8>) -> Option<Self> {
42 if !fits(width, height, data.len(), 4) {
43 return None;
44 }
45 Some(VideoFrame {
46 width,
47 height,
48 bgra: data,
49 })
50 }
51
52 pub fn i420(width: u32, height: u32, y: &[u8], u: &[u8], v: &[u8]) -> Option<Self> {
56 let (w, h) = (width as usize, height as usize);
57 let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
58 if w == 0 || h == 0 || y.len() != w * h || u.len() != cw * ch || v.len() != cw * ch {
59 return None;
60 }
61 let mut bgra = Vec::with_capacity(w * h * 4);
62 for row in 0..h {
63 for col in 0..w {
64 let luma = i32::from(y[row * w + col]) - 16;
65 let ci = (row / 2) * cw + col / 2;
66 let d = i32::from(u[ci]) - 128;
67 let e = i32::from(v[ci]) - 128;
68 let c = 298 * luma;
69 let clamp = |x: i32| (x >> 8).clamp(0, 255) as u8;
70 bgra.extend_from_slice(&[
71 clamp(c + 516 * d + 128),
72 clamp(c - 100 * d - 208 * e + 128),
73 clamp(c + 409 * e + 128),
74 255,
75 ]);
76 }
77 }
78 Some(VideoFrame {
79 width,
80 height,
81 bgra,
82 })
83 }
84
85 pub fn width(&self) -> u32 {
86 self.width
87 }
88
89 pub fn height(&self) -> u32 {
90 self.height
91 }
92
93 pub fn as_bgra(&self) -> &[u8] {
95 &self.bgra
96 }
97
98 pub(crate) fn into_image(self) -> Option<Arc<RenderImage>> {
101 let buffer = RgbaImage::from_raw(self.width, self.height, self.bgra)?;
102 Some(Arc::new(RenderImage::new(vec![Frame::new(buffer)])))
103 }
104}
105
106fn fits(width: u32, height: u32, len: usize, bytes: usize) -> bool {
107 width > 0 && height > 0 && len == width as usize * height as usize * bytes
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn rgba_is_stored_as_bgra() {
116 let f = VideoFrame::rgba(2, 1, vec![1, 2, 3, 255, 10, 20, 30, 255]).unwrap();
117 assert_eq!(f.as_bgra(), &[3, 2, 1, 255, 30, 20, 10, 255]);
118 }
119
120 #[test]
121 fn bgra_is_kept_as_is() {
122 let f = VideoFrame::bgra(1, 1, vec![9, 8, 7, 6]).unwrap();
123 assert_eq!(f.as_bgra(), &[9, 8, 7, 6]);
124 }
125
126 #[test]
127 fn a_wrong_sized_buffer_is_refused() {
128 assert!(VideoFrame::rgba(2, 2, vec![0; 15]).is_none());
129 assert!(VideoFrame::bgra(0, 4, vec![]).is_none());
130 assert!(VideoFrame::i420(2, 2, &[0; 4], &[0; 1], &[0; 2]).is_none());
131 }
132
133 #[test]
134 fn i420_maps_the_limited_range_extremes() {
135 let black = VideoFrame::i420(2, 2, &[16; 4], &[128], &[128]).unwrap();
137 assert_eq!(&black.as_bgra()[..4], &[0, 0, 0, 255]);
138 let white = VideoFrame::i420(2, 2, &[235; 4], &[128], &[128]).unwrap();
139 assert_eq!(&white.as_bgra()[..4], &[255, 255, 255, 255]);
140 }
141
142 #[test]
143 fn i420_puts_red_chroma_in_the_red_channel() {
144 let f = VideoFrame::i420(2, 2, &[81; 4], &[90], &[240]).unwrap();
146 let [b, g, r, a] = f.as_bgra()[..4] else {
147 unreachable!()
148 };
149 assert!(r > 240 && g < 20 && b < 20 && a == 255, "{r} {g} {b}");
150 }
151
152 #[test]
153 fn i420_takes_odd_dimensions() {
154 let f = VideoFrame::i420(3, 3, &[16; 9], &[128; 4], &[128; 4]).unwrap();
155 assert_eq!(f.as_bgra().len(), 3 * 3 * 4);
156 }
157}