use anyhow::{Result, bail};
use bytes::BytesMut;
use crate::frame::{ColorMetadata, ColorSpace, PixelFormat, TransferFn, VideoFrame};
use crate::tonemap::tonemap_yuv420p10le_bt2020_to_yuv420p_bt709;
mod bt601_to_709;
mod bt601_to_709_10bit;
mod chroma_convert;
mod downsample_444;
mod scale;
#[cfg(test)]
mod tests;
pub use bt601_to_709::{bt601_to_bt709_planes, bt601_to_bt709_planes_scalar};
pub use bt601_to_709_10bit::{
bt601_to_bt709_planes_10bit, bt601_to_bt709_planes_10bit_scalar,
};
pub use downsample_444::{
downsample_444_to_420_frame, downsample_chroma_444_to_420,
downsample_chroma_444_to_420_10bit,
};
pub use scale::{
bilinear_scale_plane, bilinear_scale_plane_scalar, bilinear_scale_plane_u16,
bilinear_scale_plane_u16_scalar, scale_frame,
};
const Q15: i32 = 15;
const Q15_ROUND: i32 = 1 << (Q15 - 1);
#[allow(dead_code)] const M_Y_Y: i32 = 32768;
const M_Y_CB: i32 = (-0.11554975_f64 * 32768.0) as i32; const M_Y_CR: i32 = (-0.20793764_f64 * 32768.0) as i32; const M_CB_CB: i32 = (1.01863972_f64 * 32768.0).round() as i32; const M_CB_CR: i32 = (0.11461795_f64 * 32768.0).round() as i32; const M_CR_CB: i32 = (0.07504945_f64 * 32768.0).round() as i32; const M_CR_CR: i32 = (1.02532707_f64 * 32768.0).round() as i32;
fn read_u16le(bytes: &[u8]) -> Vec<u16> {
bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect()
}
fn write_u16le(out: &mut BytesMut, samples: &[u16]) {
for s in samples {
out.extend_from_slice(&s.to_le_bytes());
}
}
pub fn convert_to_sdr_bt709(
frame: &VideoFrame,
color_metadata: &ColorMetadata,
) -> Result<VideoFrame> {
let is_hdr_transfer = matches!(
color_metadata.transfer,
TransferFn::St2084 | TransferFn::AribStdB67
);
if is_hdr_transfer && matches!(frame.format, PixelFormat::Yuv420p10le) {
let max_white_nits = color_metadata
.mastering_display
.as_ref()
.map(|m| (m.max_luminance as f32) / 10_000.0)
.filter(|n| *n > 0.0);
return tonemap_yuv420p10le_bt2020_to_yuv420p_bt709(
frame,
color_metadata.transfer,
max_white_nits,
);
}
convert_to_yuv420p_bt709(frame)
}
pub fn convert_to_yuv420p_bt709(frame: &VideoFrame) -> Result<VideoFrame> {
use PixelFormat::*;
match frame.format {
Yuv420p10le => return Ok(frame.clone()),
Yuv422p10le => return chroma_convert::yuv422p10le_to_yuv420p10le(frame),
Yuv444p10le | Yuva444p10le => {
return downsample_444::downsample_444_to_420_frame(frame)
}
Yuv420p12le => bail!(
"Yuv420p12le not yet supported in convert_to_yuv420p_bt709 \
(no decoder in tree emits 12-bit; add a 12→10-bit dither \
when a decoder lands that does)"
),
_ => {}
}
match frame.format {
Rgb24 => return chroma_convert::rgb_to_yuv420p_bt709(frame, false),
Rgba32 => return chroma_convert::rgb_to_yuv420p_bt709(frame, true),
_ => {}
}
let yuv420p = match frame.format {
Yuv420p => frame.clone(),
Nv12 => chroma_convert::nv12_to_yuv420p(frame)?,
Nv21 => chroma_convert::nv21_to_yuv420p(frame)?,
Yuv422p => chroma_convert::yuv422p_to_yuv420p(frame)?,
Yuv444p => downsample_444::downsample_444_to_420_frame(frame)?,
other => bail!(
"unsupported conversion: {:?}/{:?} → Yuv420p/Bt709",
other,
frame.color_space
),
};
if yuv420p.color_space == ColorSpace::Bt709 {
Ok(yuv420p)
} else {
bt601_to_709::recolor_yuv420p_bt601_to_bt709(&yuv420p)
}
}