use std::{num::NonZeroUsize, str::FromStr, time::Duration};
use orfail::OrFail;
use shiguredo_mp4::boxes::{SampleEntry, VisualSampleEntryFields};
use crate::{
metadata::SourceId,
types::{CodecName, EvenUsize},
};
#[derive(Debug, Clone)]
pub struct VideoFrame {
pub source_id: Option<SourceId>,
pub data: Vec<u8>,
pub format: VideoFormat,
pub keyframe: bool,
pub width: usize,
pub height: usize,
pub timestamp: Duration,
pub duration: Duration,
pub sample_entry: Option<SampleEntry>,
}
impl VideoFrame {
fn i420_plane_sizes(width: usize, height: usize) -> (usize, usize, usize) {
let y_size = width * height;
let uv_width = width.div_ceil(2);
let uv_height = height.div_ceil(2);
let uv_size = uv_width * uv_height;
(y_size, uv_size, uv_size)
}
fn i420_total_size(width: usize, height: usize) -> usize {
let (y_size, u_size, v_size) = Self::i420_plane_sizes(width, height);
y_size + u_size + v_size
}
fn i420_uv_dimensions(width: usize, height: usize) -> (usize, usize) {
(width.div_ceil(2), height.div_ceil(2))
}
pub fn from_bgr_data(
bgr_data: &[u8],
width: EvenUsize,
height: EvenUsize,
timestamp: Duration,
duration: Duration,
) -> orfail::Result<Self> {
let width_val = width.get();
let height_val = height.get();
let expected_size = width_val * height_val * 3;
if bgr_data.len() != expected_size {
return Err(orfail::Failure::new(format!(
"BGR data size mismatch: expected {}, got {}",
expected_size,
bgr_data.len()
)));
}
let (y_size, u_size, _) = Self::i420_plane_sizes(width_val, height_val);
let mut yuv_data = Vec::with_capacity(Self::i420_total_size(width_val, height_val));
let mut y_plane = vec![0u8; y_size];
let mut u_plane = vec![0u8; u_size];
let mut v_plane = vec![0u8; u_size];
for y in 0..height_val {
for x in 0..width_val {
let bgr_idx = (y * width_val + x) * 3;
let b = bgr_data[bgr_idx] as f32;
let g = bgr_data[bgr_idx + 1] as f32;
let r = bgr_data[bgr_idx + 2] as f32;
let y_val = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
let u_val = ((-0.169 * r - 0.331 * g + 0.500 * b) + 128.0) as u8;
let v_val = ((0.500 * r - 0.419 * g - 0.081 * b) + 128.0) as u8;
y_plane[y * width_val + x] = y_val;
if y % 2 == 0 && x % 2 == 0 {
let uv_idx = (y / 2) * (width_val / 2) + (x / 2);
u_plane[uv_idx] = u_val;
v_plane[uv_idx] = v_val;
}
}
}
yuv_data.extend_from_slice(&y_plane);
yuv_data.extend_from_slice(&u_plane);
yuv_data.extend_from_slice(&v_plane);
Ok(Self {
source_id: None,
data: yuv_data,
format: VideoFormat::I420,
keyframe: true,
width: width.get(),
height: height.get(),
timestamp,
duration,
sample_entry: None,
})
}
pub fn to_stripped(&self) -> Self {
Self {
source_id: self.source_id.clone(),
data: Vec::new(),
format: self.format,
keyframe: self.keyframe,
width: self.width,
height: self.height,
timestamp: self.timestamp,
duration: self.duration,
sample_entry: None,
}
}
#[expect(clippy::too_many_arguments)]
pub fn new_i420(
input_frame: Self,
width: usize,
height: usize,
y_plane: &[u8],
u_plane: &[u8],
v_plane: &[u8],
y_stride: usize,
u_stride: usize,
v_stride: usize,
) -> Self {
let (y_size, _, _) = Self::i420_plane_sizes(width, height);
let (uv_width, uv_height) = Self::i420_uv_dimensions(width, height);
let uv_size = uv_width * uv_height;
let mut data = Vec::with_capacity(Self::i420_total_size(width, height));
if width == y_stride {
data.extend_from_slice(&y_plane[..y_size]);
} else {
for i in 0..height {
let offset = y_stride * i;
data.extend_from_slice(&y_plane[offset..][..width]);
}
}
if uv_width == u_stride {
data.extend_from_slice(&u_plane[..uv_size]);
} else {
for i in 0..uv_height {
let offset = u_stride * i;
data.extend_from_slice(&u_plane[offset..][..uv_width]);
}
}
if uv_width == v_stride {
data.extend_from_slice(&v_plane[..uv_size]);
} else {
for i in 0..uv_height {
let offset = v_stride * i;
data.extend_from_slice(&v_plane[offset..][..uv_width]);
}
}
Self {
source_id: input_frame.source_id,
sample_entry: None, data,
format: VideoFormat::I420,
keyframe: true, width,
height,
timestamp: input_frame.timestamp,
duration: input_frame.duration,
}
}
#[expect(clippy::too_many_arguments)]
pub fn new_i420_from_high_depth(
input_frame: Self,
width: usize,
height: usize,
y_plane_16: &[u8],
u_plane_16: &[u8],
v_plane_16: &[u8],
y_stride: usize,
u_stride: usize,
v_stride: usize,
) -> orfail::Result<Self> {
let (y_size, _, _) = Self::i420_plane_sizes(width, height);
let (uv_width, uv_height) = Self::i420_uv_dimensions(width, height);
let uv_size = uv_width * uv_height;
let mut data = Vec::with_capacity(Self::i420_total_size(width, height));
let convert_10bit_to_8bit =
|value_16: u16| -> u8 { ((value_16 as u32 * 255 + 511) / 1023) as u8 };
if width * 2 == y_stride {
(y_plane_16.len() >= y_size * 2).or_fail_with(|()| {
format!(
"Y plane data insufficient: expected {} bytes, got {}",
y_size * 2,
y_plane_16.len()
)
})?;
for chunk in y_plane_16[..y_size * 2].chunks_exact(2) {
let value_16 = u16::from_le_bytes([chunk[0], chunk[1]]);
let value_8 = convert_10bit_to_8bit(value_16);
data.push(value_8);
}
} else {
for row in 0..height {
let row_start = row * y_stride;
(row_start + width * 2 <= y_plane_16.len()).or_fail_with(|()| {
format!(
"Y plane data insufficient: row {} requires {} bytes but only {} available",
row,
row_start + width * 2,
y_plane_16.len()
)
})?;
let row_data = &y_plane_16[row_start..row_start + width * 2];
for chunk in row_data.chunks_exact(2) {
let value_16 = u16::from_le_bytes([chunk[0], chunk[1]]);
let value_8 = convert_10bit_to_8bit(value_16);
data.push(value_8);
}
}
}
if uv_width * 2 == u_stride {
(u_plane_16.len() >= uv_size * 2).or_fail_with(|()| {
format!(
"U plane data insufficient: expected {} bytes, got {}",
uv_size * 2,
u_plane_16.len()
)
})?;
for chunk in u_plane_16[..uv_size * 2].chunks_exact(2) {
let value_16 = u16::from_le_bytes([chunk[0], chunk[1]]);
let value_8 = convert_10bit_to_8bit(value_16);
data.push(value_8);
}
} else {
for row in 0..uv_height {
let row_start = row * u_stride;
(row_start + uv_width * 2 <= u_plane_16.len()).or_fail_with(|()| {
format!(
"U plane data insufficient: row {} requires {} bytes but only {} available",
row,
row_start + uv_width * 2,
u_plane_16.len()
)
})?;
let row_data = &u_plane_16[row_start..row_start + uv_width * 2];
for chunk in row_data.chunks_exact(2) {
let value_16 = u16::from_le_bytes([chunk[0], chunk[1]]);
let value_8 = convert_10bit_to_8bit(value_16);
data.push(value_8);
}
}
}
if uv_width * 2 == v_stride {
(v_plane_16.len() >= uv_size * 2).or_fail_with(|()| {
format!(
"V plane data insufficient: expected {} bytes, got {}",
uv_size * 2,
v_plane_16.len()
)
})?;
for chunk in v_plane_16[..uv_size * 2].chunks_exact(2) {
let value_16 = u16::from_le_bytes([chunk[0], chunk[1]]);
let value_8 = convert_10bit_to_8bit(value_16);
data.push(value_8);
}
} else {
for row in 0..uv_height {
let row_start = row * v_stride;
(row_start + uv_width * 2 <= v_plane_16.len()).or_fail_with(|()| {
format!(
"V plane data insufficient: row {} requires {} bytes but only {} available",
row,
row_start + uv_width * 2,
v_plane_16.len()
)
})?;
let row_data = &v_plane_16[row_start..row_start + uv_width * 2];
for chunk in row_data.chunks_exact(2) {
let value_16 = u16::from_le_bytes([chunk[0], chunk[1]]);
let value_8 = convert_10bit_to_8bit(value_16);
data.push(value_8);
}
}
}
Ok(Self {
source_id: input_frame.source_id,
sample_entry: None, data,
format: VideoFormat::I420,
keyframe: true, width,
height,
timestamp: input_frame.timestamp,
duration: input_frame.duration,
})
}
pub fn mono_color(rgb: [u8; 3], width: EvenUsize, height: EvenUsize) -> Self {
if rgb == [0, 0, 0] {
return Self::black(width, height);
}
let r = rgb[0] as f32;
let g = rgb[1] as f32;
let b = rgb[2] as f32;
let y = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
let u = ((-0.169 * r - 0.331 * g + 0.500 * b) + 128.0) as u8;
let v = ((0.500 * r - 0.419 * g - 0.081 * b) + 128.0) as u8;
let actual_width = width.get();
let actual_height = height.get();
let (y_plane_size, u_plane_size, _) = Self::i420_plane_sizes(actual_width, actual_height);
let total_size = Self::i420_total_size(actual_width, actual_height);
let mut data = Vec::with_capacity(total_size);
data.resize(y_plane_size, y);
data.resize(y_plane_size + u_plane_size, u);
data.resize(total_size, v);
Self {
source_id: None,
data,
format: VideoFormat::I420,
keyframe: true,
width: actual_width,
height: actual_height,
timestamp: Duration::ZERO,
duration: Duration::ZERO,
sample_entry: None,
}
}
pub fn black(width: EvenUsize, height: EvenUsize) -> Self {
let actual_width = width.get();
let actual_height = height.get();
let (y_plane_size, _, _) = Self::i420_plane_sizes(actual_width, actual_height);
let total_size = Self::i420_total_size(actual_width, actual_height);
let mut data = vec![0; total_size];
for b in data.iter_mut().take(total_size).skip(y_plane_size) {
*b = 128;
}
Self {
source_id: None,
data,
format: VideoFormat::I420,
keyframe: true,
width: actual_width,
height: actual_height,
timestamp: Duration::ZERO,
duration: Duration::ZERO,
sample_entry: None,
}
}
pub fn ceiling_width(&self) -> EvenUsize {
EvenUsize::ceiling_new(self.width)
}
pub fn ceiling_height(&self) -> EvenUsize {
EvenUsize::ceiling_new(self.height)
}
pub fn as_yuv_planes(&self) -> Option<(&[u8], &[u8], &[u8])> {
if self.format != VideoFormat::I420 {
return None;
}
let (y_size, uv_size, _) = Self::i420_plane_sizes(self.width, self.height);
let y_plane = &self.data[..y_size];
let u_plane = &self.data[y_size..][..uv_size];
let v_plane = &self.data[y_size + uv_size..][..uv_size];
Some((y_plane, u_plane, v_plane))
}
pub fn end_timestamp(&self) -> Duration {
self.timestamp + self.duration
}
pub fn resize(
&self,
new_width: EvenUsize,
new_height: EvenUsize,
filter_mode: shiguredo_libyuv::FilterMode,
) -> orfail::Result<Option<Self>> {
(self.format == VideoFormat::I420).or_fail()?;
let width = self.width;
let height = self.height;
if width == new_width.get() && height == new_height.get() {
return Ok(None);
}
let (new_y_size, new_uv_size, _) =
Self::i420_plane_sizes(new_width.get(), new_height.get());
let mut new_data = vec![0; Self::i420_total_size(new_width.get(), new_height.get())];
let (src_y, src_u, src_v) = self.as_yuv_planes().or_fail()?;
let src_width = self.width;
let (src_uv_width, _) = Self::i420_uv_dimensions(self.width, self.height);
let dst_width = new_width.get();
let dst_uv_width = dst_width / 2;
let (dst_y, rest) = new_data.split_at_mut(new_y_size);
let (dst_u, dst_v) = rest.split_at_mut(new_uv_size);
let src = shiguredo_libyuv::I420Planes {
y: src_y,
y_stride: src_width, u: src_u,
u_stride: src_uv_width, v: src_v,
v_stride: src_uv_width, };
let mut dst = shiguredo_libyuv::I420PlanesMut {
y: dst_y,
y_stride: dst_width, u: dst_u,
u_stride: dst_uv_width, v: dst_v,
v_stride: dst_uv_width, };
shiguredo_libyuv::i420_scale(
&src,
shiguredo_libyuv::ImageSize::new(width, height), &mut dst,
shiguredo_libyuv::ImageSize::new(dst_width, new_height.get()), filter_mode,
)
.or_fail()?;
let resized = Self {
source_id: self.source_id.clone(),
data: new_data,
format: self.format,
keyframe: self.keyframe,
width: new_width.get(),
height: new_height.get(),
timestamp: self.timestamp,
duration: self.duration,
sample_entry: self.sample_entry.clone(),
};
Ok(Some(resized))
}
pub fn to_bgr_data(&self) -> orfail::Result<Vec<u8>> {
(self.format == VideoFormat::I420).or_fail()?;
let actual_width = self.width;
let actual_height = self.height;
let (y_plane, u_plane, v_plane) = self.as_yuv_planes().or_fail()?;
let y_stride = actual_width;
let (uv_stride, _) = Self::i420_uv_dimensions(actual_width, actual_height);
let mut bgr_data = Vec::with_capacity(actual_width * actual_height * 3);
for y in 0..actual_height {
for x in 0..actual_width {
let y_idx = y * y_stride + x;
let uv_y = y / 2;
let uv_x = x / 2;
let uv_idx = uv_y * uv_stride + uv_x;
let y_val = y_plane[y_idx] as f32;
let u_val = u_plane[uv_idx] as f32 - 128.0;
let v_val = v_plane[uv_idx] as f32 - 128.0;
let r = y_val + 1.402 * v_val;
let g = y_val - 0.344 * u_val - 0.714 * v_val;
let b = y_val + 1.772 * u_val;
let r = r.clamp(0.0, 255.0) as u8;
let g = g.clamp(0.0, 255.0) as u8;
let b = b.clamp(0.0, 255.0) as u8;
bgr_data.push(b);
bgr_data.push(g);
bgr_data.push(r);
}
}
Ok(bgr_data)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoFormat {
I420,
H264,
H264AnnexB,
H265,
Vp8,
Vp9,
Av1,
}
impl VideoFormat {
pub fn codec_name(self) -> Option<CodecName> {
match self {
VideoFormat::I420 => None,
VideoFormat::H264 => Some(CodecName::H264),
VideoFormat::H264AnnexB => Some(CodecName::H264),
VideoFormat::H265 => Some(CodecName::H265),
VideoFormat::Vp8 => Some(CodecName::Vp8),
VideoFormat::Vp9 => Some(CodecName::Vp9),
VideoFormat::Av1 => Some(CodecName::Av1),
}
}
}
impl std::fmt::Display for VideoFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VideoFormat::I420 => write!(f, "I420"),
_ => {
let name = self.codec_name().expect("infallible");
write!(f, "{}", name.as_str())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameRate {
pub numerator: NonZeroUsize,
pub denumerator: NonZeroUsize,
}
impl FrameRate {
pub const FPS_1: Self = Self {
numerator: NonZeroUsize::MIN,
denumerator: NonZeroUsize::MIN,
};
pub const FPS_25: Self = Self {
numerator: NonZeroUsize::MIN.saturating_add(24),
denumerator: NonZeroUsize::MIN,
};
}
impl FromStr for FrameRate {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((integer, fraction)) = s.split_once('/') {
let integer = NonZeroUsize::from_str(integer).map_err(|_| {
format!("the integer part of {s:?} is not a valid positive integer")
})?;
let fraction = NonZeroUsize::from_str(fraction).map_err(|_| {
format!("the fraction part of {s:?} is not a valid positive integer")
})?;
Ok(Self {
numerator: integer,
denumerator: fraction,
})
} else {
let integer = NonZeroUsize::from_str(s)
.map_err(|_| format!("{s:?} is not a valid positive integer"))?;
Ok(Self {
numerator: integer,
denumerator: NonZeroUsize::MIN,
})
}
}
}
impl nojson::DisplayJson for FrameRate {
fn fmt(&self, f: &mut nojson::JsonFormatter<'_, '_>) -> std::fmt::Result {
if self.denumerator.get() == 1 {
f.value(self.numerator.get())
} else {
f.string(format!("{}/{}", self.numerator, self.denumerator))
}
}
}
pub fn sample_entry_visual_fields(width: usize, height: usize) -> VisualSampleEntryFields {
VisualSampleEntryFields {
data_reference_index: VisualSampleEntryFields::DEFAULT_DATA_REFERENCE_INDEX,
width: width as u16,
height: height as u16,
horizresolution: VisualSampleEntryFields::DEFAULT_HORIZRESOLUTION,
vertresolution: VisualSampleEntryFields::DEFAULT_VERTRESOLUTION,
frame_count: VisualSampleEntryFields::DEFAULT_FRAME_COUNT,
compressorname: VisualSampleEntryFields::NULL_COMPRESSORNAME,
depth: VisualSampleEntryFields::DEFAULT_DEPTH,
}
}