use std::borrow::Cow;
use bytes::Bytes;
use moq_net::Timestamp;
use yuv::{YuvChromaSubsampling, YuvConversionMode, YuvPlanarImageMut, YuvRange, YuvStandardMatrix, rgba_to_yuv420};
use crate::{Error, Size};
pub struct Frame {
pub timestamp: Timestamp,
pub surface: Surface,
}
impl Frame {
pub fn new(surface: Surface, timestamp: Timestamp) -> Self {
Self { timestamp, surface }
}
pub fn size(&self) -> Size {
Size::new(self.surface.width(), self.surface.height())
}
pub fn resize(&self, size: Size) -> Result<Frame, Error> {
Ok(Frame {
timestamp: self.timestamp,
surface: self.surface.resize(size)?,
})
}
}
#[non_exhaustive]
pub enum Surface {
#[cfg(target_os = "macos")]
PixelBuffer(macos::PixelBuffer),
#[cfg(target_os = "windows")]
Texture(d3d11::Texture),
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Cuda(cuda::Frame),
I420(I420),
}
impl Surface {
pub fn width(&self) -> u32 {
match self {
#[cfg(target_os = "macos")]
Surface::PixelBuffer(s) => s.width,
#[cfg(target_os = "windows")]
Surface::Texture(t) => t.width,
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Surface::Cuda(c) => c.width,
Surface::I420(i) => i.width,
}
}
pub fn height(&self) -> u32 {
match self {
#[cfg(target_os = "macos")]
Surface::PixelBuffer(s) => s.height,
#[cfg(target_os = "windows")]
Surface::Texture(t) => t.height,
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Surface::Cuda(c) => c.height,
Surface::I420(i) => i.height,
}
}
pub fn rgba(rgba: &[u8], size: Size) -> Result<Self, Error> {
size.validate("RGBA frame")?;
let expected = size.pixels() as usize * 4;
if rgba.len() != expected {
return Err(Error::Codec(anyhow::anyhow!(
"RGBA buffer is {} bytes, expected {expected} for {size}",
rgba.len()
)));
}
Ok(Surface::I420(I420::from_rgba(
rgba,
size.width * 4,
size.width,
size.height,
)?))
}
pub fn resize(&self, size: Size) -> Result<Surface, Error> {
size.validate("resize to")?;
let Size { width, height } = size;
Ok(match self {
Surface::I420(i420) => Surface::I420(i420.resize(width, height)?),
#[cfg(target_os = "macos")]
Surface::PixelBuffer(pixels) => match pixels.resize(width, height) {
Ok(scaled) => Surface::PixelBuffer(scaled),
Err(err) => {
static WARN_ONCE: std::sync::Once = std::sync::Once::new();
WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
Surface::I420(pixels.download_i420()?.resize(width, height)?)
}
},
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Surface::Cuda(cuda) => match cuda.resize(width, height) {
Ok(scaled) => Surface::Cuda(scaled),
Err(err) => {
static WARN_ONCE: std::sync::Once = std::sync::Once::new();
WARN_ONCE.call_once(|| tracing::warn!(%err, "GPU resize failed; falling back to the CPU"));
Surface::I420(cuda.download_i420()?.resize(width, height)?)
}
},
#[allow(unreachable_patterns)]
other => Surface::I420(other.to_i420()?.into_owned().resize(width, height)?),
})
}
pub fn into_i420(self) -> Result<Bytes, Error> {
match self {
Surface::I420(i420) => Ok(Bytes::from(i420.data)),
#[allow(unreachable_patterns)]
other => Ok(Bytes::from(other.to_i420()?.into_owned().data)),
}
}
#[cfg(target_os = "macos")]
pub fn into_pixel_buffer(
self,
) -> Result<objc2_core_foundation::CFRetained<objc2_core_video::CVPixelBuffer>, Error> {
match self {
Surface::PixelBuffer(pixels) => Ok(pixels.buffer),
Surface::I420(i420) => macos::upload_i420(&i420),
}
}
pub(crate) fn to_i420(&self) -> Result<Cow<'_, I420>, Error> {
match self {
#[cfg(target_os = "macos")]
Surface::PixelBuffer(s) => Ok(Cow::Owned(s.download_i420()?)),
#[cfg(target_os = "windows")]
Surface::Texture(t) => Ok(Cow::Owned(t.download_i420()?)),
#[cfg(all(target_os = "linux", feature = "nvdec"))]
Surface::Cuda(c) => Ok(Cow::Owned(c.download_i420()?)),
Surface::I420(i) => Ok(Cow::Borrowed(i)),
}
}
}
#[derive(Clone)]
pub struct I420 {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) data: Vec<u8>,
}
impl I420 {
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self, Error> {
crate::Size::new(width, height).validate("I420")?;
let expected = Self::len(width, height);
if data.len() != expected {
return Err(Error::Codec(anyhow::anyhow!(
"I420 {width}x{height} needs {expected} bytes, got {}",
data.len()
)));
}
Ok(Self { width, height, data })
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn len(width: u32, height: u32) -> usize {
let luma = width as usize * height as usize;
luma + luma / 2
}
pub(crate) fn from_rgba(rgba: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
rgba_to_yuv420(
&mut planar,
rgba,
stride,
YuvRange::Limited,
YuvStandardMatrix::Bt601,
YuvConversionMode::Balanced,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("rgba_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "pipewire")))]
pub(crate) fn from_bgra(bgra: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
use yuv::bgra_to_yuv420;
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
bgra_to_yuv420(
&mut planar,
bgra,
stride,
YuvRange::Limited,
YuvStandardMatrix::Bt601,
YuvConversionMode::Balanced,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("bgra_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
pub(crate) fn from_planes(
y: &[u8],
u: &[u8],
v: &[u8],
y_stride: usize,
uv_stride: usize,
width: u32,
height: u32,
) -> Self {
let (w, h) = (width as usize, height as usize);
let (cw, ch) = (w / 2, h / 2);
let mut data = vec![0u8; Self::len(width, height)];
let (luma, chroma) = data.split_at_mut(w * h);
let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
for row in 0..h {
luma[row * w..row * w + w].copy_from_slice(&y[row * y_stride..row * y_stride + w]);
}
for row in 0..ch {
u_dst[row * cw..row * cw + cw].copy_from_slice(&u[row * uv_stride..row * uv_stride + cw]);
v_dst[row * cw..row * cw + cw].copy_from_slice(&v[row * uv_stride..row * uv_stride + cw]);
}
Self { width, height, data }
}
#[cfg(target_os = "linux")]
pub(crate) fn from_rgb(rgb: &[u8], width: u32, height: u32) -> Result<Self, Error> {
use yuv::rgb_to_yuv420;
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
rgb_to_yuv420(
&mut planar,
rgb,
width * 3,
YuvRange::Limited,
YuvStandardMatrix::Bt601,
YuvConversionMode::Balanced,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("rgb_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
#[cfg(target_os = "linux")]
pub(crate) fn from_yuyv(yuyv: &[u8], stride: u32, width: u32, height: u32) -> Result<Self, Error> {
use yuv::{YuvPackedImage, yuyv422_to_yuv420};
let mut planar = YuvPlanarImageMut::alloc(width, height, YuvChromaSubsampling::Yuv420);
let packed = YuvPackedImage {
yuy: yuyv,
yuy_stride: stride,
width,
height,
};
yuyv422_to_yuv420(&mut planar, &packed)
.map_err(|e| Error::Codec(anyhow::anyhow!("yuyv422_to_yuv420 failed for {width}x{height}: {e}")))?;
Ok(Self::pack(&planar, width, height))
}
#[cfg(target_os = "windows")]
pub(crate) fn from_nv12(nv12: &[u8], width: u32, height: u32) -> Result<Self, Error> {
let (w, h) = (width as usize, height as usize);
let luma = w * h;
let chroma = luma / 4;
let need = luma + 2 * chroma;
if nv12.len() < need {
return Err(Error::Codec(anyhow::anyhow!(
"NV12 buffer too small: {} < {need} for {width}x{height}",
nv12.len()
)));
}
let mut data = vec![0u8; Self::len(width, height)];
data[..luma].copy_from_slice(&nv12[..luma]);
let (u_dst, v_dst) = data[luma..].split_at_mut(chroma);
deinterleave_uv(&nv12[luma..need], u_dst, v_dst);
Ok(Self { width, height, data })
}
pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
use std::cell::RefCell;
use fast_image_resize::images::{Image, ImageRef};
use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer};
thread_local! {
static RESIZER: RefCell<Resizer> = RefCell::new(Resizer::new());
}
let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Bilinear));
let plane = |resizer: &mut Resizer,
src: &[u8],
sw: u32,
sh: u32,
dst: &mut [u8],
dw: u32,
dh: u32|
-> Result<(), Error> {
let src = ImageRef::new(sw, sh, src, PixelType::U8)
.map_err(|e| Error::Codec(anyhow::anyhow!("resize source: {e}")))?;
let mut dst = Image::from_slice_u8(dw, dh, dst, PixelType::U8)
.map_err(|e| Error::Codec(anyhow::anyhow!("resize destination: {e}")))?;
resizer
.resize(&src, &mut dst, &options)
.map_err(|e| Error::Codec(anyhow::anyhow!("resize: {e}")))
};
let luma = width as usize * height as usize;
let mut data = vec![0u8; Self::len(width, height)];
let (y_dst, chroma) = data.split_at_mut(luma);
let (u_dst, v_dst) = chroma.split_at_mut(luma / 4);
RESIZER.with_borrow_mut(|resizer| {
plane(resizer, self.y(), self.width, self.height, y_dst, width, height)?;
let (sw2, sh2) = (self.width / 2, self.height / 2);
let (dw2, dh2) = (width / 2, height / 2);
plane(resizer, self.u(), sw2, sh2, u_dst, dw2, dh2)?;
plane(resizer, self.v(), sw2, sh2, v_dst, dw2, dh2)
})?;
Ok(Self { width, height, data })
}
fn pack(planar: &YuvPlanarImageMut<u8>, width: u32, height: u32) -> Self {
let mut data = Vec::with_capacity(Self::len(width, height));
data.extend_from_slice(planar.y_plane.borrow());
data.extend_from_slice(planar.u_plane.borrow());
data.extend_from_slice(planar.v_plane.borrow());
Self { width, height, data }
}
fn luma_len(&self) -> usize {
self.width as usize * self.height as usize
}
fn chroma_len(&self) -> usize {
self.luma_len() / 4
}
pub fn y(&self) -> &[u8] {
&self.data[..self.luma_len()]
}
pub fn u(&self) -> &[u8] {
let start = self.luma_len();
&self.data[start..start + self.chroma_len()]
}
pub fn v(&self) -> &[u8] {
let start = self.luma_len() + self.chroma_len();
&self.data[start..start + self.chroma_len()]
}
}
#[cfg(any(target_os = "windows", all(target_os = "linux", feature = "nvenc")))]
pub(crate) fn interleave_uv(u: &[u8], v: &[u8], uv: &mut [u8]) {
for (pair, (u, v)) in uv.chunks_exact_mut(2).zip(u.iter().zip(v)) {
pair[0] = *u;
pair[1] = *v;
}
}
#[cfg(target_os = "windows")]
pub(crate) fn deinterleave_uv(uv: &[u8], u: &mut [u8], v: &mut [u8]) {
for (pair, (u, v)) in uv.chunks_exact(2).zip(u.iter_mut().zip(v)) {
*u = pair[0];
*v = pair[1];
}
}
#[cfg(target_os = "macos")]
pub mod macos {
use std::collections::{HashMap, VecDeque};
use std::ffi::c_void;
use std::ptr;
use std::ptr::NonNull;
use std::sync::{Arc, LazyLock, Mutex};
use objc2_core_foundation::{CFDictionary, CFNumber, CFNumberType, CFRetained, CFString};
use objc2_core_video::{
CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
CVPixelBufferGetPixelFormatType, CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferPool,
CVPixelBufferUnlockBaseAddress, kCVPixelBufferHeightKey, kCVPixelBufferIOSurfacePropertiesKey,
kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferWidthKey, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
};
use objc2_video_toolbox::VTPixelTransferSession;
use super::I420;
use crate::Error;
const LOCK_READ_ONLY: CVPixelBufferLockFlags = CVPixelBufferLockFlags(1);
const SCALER_CACHE_CAPACITY: usize = 16;
type ScalerCache = Mutex<Cache<Scaler>>;
static SCALERS: LazyLock<ScalerCache> = LazyLock::new(|| Mutex::new(Cache::new(SCALER_CACHE_CAPACITY)));
struct Cache<T> {
values: HashMap<(u32, u32), Arc<Mutex<T>>>,
order: VecDeque<(u32, u32)>,
capacity: usize,
}
impl<T> Cache<T> {
fn new(capacity: usize) -> Self {
Self {
values: HashMap::new(),
order: VecDeque::new(),
capacity,
}
}
fn get_or_insert_with<E>(
&mut self,
key: (u32, u32),
create: impl FnOnce() -> Result<T, E>,
) -> Result<Arc<Mutex<T>>, E> {
if let Some(value) = self.values.get(&key).cloned() {
self.touch(key);
return Ok(value);
}
let value = Arc::new(Mutex::new(create()?));
self.values.insert(key, Arc::clone(&value));
self.touch(key);
self.prune();
Ok(value)
}
fn touch(&mut self, key: (u32, u32)) {
self.order.retain(|entry| *entry != key);
self.order.push_back(key);
}
fn prune(&mut self) {
let mut remaining = self.order.len();
while self.values.len() > self.capacity && remaining > 0 {
let key = self.order.pop_front().expect("remaining entries");
let idle = self.values.get(&key).is_some_and(|value| Arc::strong_count(value) == 1);
if idle {
self.values.remove(&key);
} else {
self.order.push_back(key);
}
remaining -= 1;
}
}
}
pub struct PixelBuffer {
pub(crate) buffer: CFRetained<CVPixelBuffer>,
pub(crate) width: u32,
pub(crate) height: u32,
}
unsafe impl Send for PixelBuffer {}
unsafe impl Sync for PixelBuffer {}
impl PixelBuffer {
pub fn buffer(&self) -> &CVPixelBuffer {
&self.buffer
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub(crate) fn new(buffer: CFRetained<CVPixelBuffer>, width: u32, height: u32) -> Self {
Self { buffer, width, height }
}
pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
let scaler = {
let mut scalers = SCALERS
.lock()
.map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler cache lock poisoned")))?;
scalers.get_or_insert_with((width, height), || Scaler::new(width, height))?
};
let result = scaler
.lock()
.map_err(|_| Error::Codec(anyhow::anyhow!("pixel-transfer scaler lock poisoned")))?
.resize(self);
drop(scaler);
if let Ok(mut scalers) = SCALERS.lock() {
scalers.prune();
}
result
}
pub(crate) fn download_i420(&self) -> Result<I420, Error> {
let format = CVPixelBufferGetPixelFormatType(&self.buffer);
if format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
&& format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
{
return Err(Error::Codec(anyhow::anyhow!(
"cannot download pixel format {format:#x}; expected NV12"
)));
}
let (w, h) = (self.width as usize, self.height as usize);
let (cw, ch) = (w / 2, h / 2);
let status = unsafe { CVPixelBufferLockBaseAddress(&self.buffer, LOCK_READ_ONLY) };
if status != 0 {
return Err(Error::Codec(anyhow::anyhow!(
"CVPixelBufferLockBaseAddress failed: {status}"
)));
}
let _guard = UnlockGuard(&self.buffer);
let mut data = vec![0u8; I420::len(self.width, self.height)];
let (luma, chroma) = data.split_at_mut(w * h);
let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
let y_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 0) as *const u8;
let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 0);
for row in 0..h {
unsafe {
ptr::copy_nonoverlapping(y_base.add(row * y_stride), luma[row * w..].as_mut_ptr(), w);
}
}
let uv_base = CVPixelBufferGetBaseAddressOfPlane(&self.buffer, 1) as *const u8;
let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&self.buffer, 1);
for row in 0..ch {
let src = unsafe { uv_base.add(row * uv_stride) };
for col in 0..cw {
unsafe {
u_plane[row * cw + col] = *src.add(col * 2);
v_plane[row * cw + col] = *src.add(col * 2 + 1);
}
}
}
Ok(I420 {
width: self.width,
height: self.height,
data,
})
}
}
struct Scaler {
session: CFRetained<VTPixelTransferSession>,
pool: CFRetained<CVPixelBufferPool>,
width: u32,
height: u32,
}
unsafe impl Send for Scaler {}
impl Scaler {
fn new(width: u32, height: u32) -> Result<Self, Error> {
let mut session_ptr: *mut VTPixelTransferSession = std::ptr::null_mut();
let status = unsafe {
VTPixelTransferSession::create(None, NonNull::new(&mut session_ptr).expect("stack pointer is non-null"))
};
let session = NonNull::new(session_ptr)
.filter(|_| status == 0)
.map(|ptr| unsafe { CFRetained::from_raw(ptr) })
.ok_or_else(|| Error::Codec(anyhow::anyhow!("VTPixelTransferSessionCreate failed: {status}")))?;
let attributes = pool_attributes(width, height)?;
let mut pool_ptr: *mut CVPixelBufferPool = std::ptr::null_mut();
let status = unsafe {
CVPixelBufferPool::create(
None,
None,
Some(&attributes),
NonNull::new(&mut pool_ptr).expect("stack pointer is non-null"),
)
};
let pool = NonNull::new(pool_ptr)
.filter(|_| status == 0)
.map(|ptr| unsafe { CFRetained::from_raw(ptr) })
.ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreate failed: {status}")))?;
Ok(Self {
session,
pool,
width,
height,
})
}
fn resize(&mut self, source: &PixelBuffer) -> Result<PixelBuffer, Error> {
let mut output_ptr: *mut CVPixelBuffer = std::ptr::null_mut();
let status = unsafe {
CVPixelBufferPool::create_pixel_buffer(
None,
&self.pool,
NonNull::new(&mut output_ptr).expect("stack pointer is non-null"),
)
};
let output = NonNull::new(output_ptr)
.filter(|_| status == 0)
.map(|ptr| unsafe { CFRetained::from_raw(ptr) })
.ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferPoolCreatePixelBuffer failed: {status}")))?;
let status = unsafe { self.session.transfer_image(&source.buffer, &output) };
if status != 0 {
return Err(Error::Codec(anyhow::anyhow!(
"VTPixelTransferSessionTransferImage failed: {status}"
)));
}
Ok(PixelBuffer::new(output, self.width, self.height))
}
}
fn pool_attributes(width: u32, height: u32) -> Result<CFRetained<CFDictionary>, Error> {
let width =
i32::try_from(width).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer width is too large")))?;
let height =
i32::try_from(height).map_err(|_| Error::Codec(anyhow::anyhow!("pixel-buffer height is too large")))?;
let format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange as i32;
let width = cf_number(width)?;
let height = cf_number(height)?;
let format = cf_number(format)?;
let iosurface = unsafe {
CFDictionary::new(
None,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
&objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
&objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
)
}
.ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build IOSurface attributes dictionary")))?;
let mut keys = [
(unsafe { kCVPixelBufferPixelFormatTypeKey } as *const CFString).cast::<c_void>(),
(unsafe { kCVPixelBufferWidthKey } as *const CFString).cast::<c_void>(),
(unsafe { kCVPixelBufferHeightKey } as *const CFString).cast::<c_void>(),
(unsafe { kCVPixelBufferIOSurfacePropertiesKey } as *const CFString).cast::<c_void>(),
];
let mut values = [
(format.as_ref() as *const CFNumber).cast::<c_void>(),
(width.as_ref() as *const CFNumber).cast::<c_void>(),
(height.as_ref() as *const CFNumber).cast::<c_void>(),
(iosurface.as_ref() as *const CFDictionary).cast::<c_void>(),
];
unsafe {
CFDictionary::new(
None,
keys.as_mut_ptr(),
values.as_mut_ptr(),
4,
&objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
&objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
)
}
.ok_or_else(|| {
Error::Codec(anyhow::anyhow!(
"failed to build pixel-buffer pool attributes dictionary"
))
})
}
fn cf_number(value: i32) -> Result<CFRetained<CFNumber>, Error> {
unsafe { CFNumber::new(None, CFNumberType::SInt32Type, (&value as *const i32).cast::<c_void>()) }
.ok_or_else(|| Error::Codec(anyhow::anyhow!("failed to build CFNumber")))
}
struct UnlockGuard<'a>(&'a CVPixelBuffer);
impl Drop for UnlockGuard<'_> {
fn drop(&mut self) {
unsafe { CVPixelBufferUnlockBaseAddress(self.0, LOCK_READ_ONLY) };
}
}
pub(crate) fn upload_i420(frame: &I420) -> Result<CFRetained<CVPixelBuffer>, Error> {
let (w, h) = (frame.width as usize, frame.height as usize);
let (cw, ch) = (w / 2, h / 2);
let mut ptr: *mut CVPixelBuffer = std::ptr::null_mut();
let status = unsafe {
CVPixelBufferCreate(
None,
w,
h,
kCVPixelFormatType_420YpCbCr8Planar,
None,
NonNull::new(&mut ptr).unwrap(),
)
};
let buffer = NonNull::new(ptr)
.filter(|_| status == 0)
.map(|p| unsafe { CFRetained::from_raw(p) })
.ok_or_else(|| Error::Codec(anyhow::anyhow!("CVPixelBufferCreate failed: {status}")))?;
let flags = CVPixelBufferLockFlags(0);
let status = unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) };
if status != 0 {
return Err(Error::Codec(anyhow::anyhow!(
"CVPixelBufferLockBaseAddress failed: {status}"
)));
}
copy_plane(&buffer, 0, frame.y(), w, h);
copy_plane(&buffer, 1, frame.u(), cw, ch);
copy_plane(&buffer, 2, frame.v(), cw, ch);
unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
Ok(buffer)
}
fn copy_plane(buffer: &CVPixelBuffer, plane: usize, src: &[u8], row_bytes: usize, rows: usize) {
let base = CVPixelBufferGetBaseAddressOfPlane(buffer, plane) as *mut u8;
let stride = CVPixelBufferGetBytesPerRowOfPlane(buffer, plane);
for y in 0..rows {
unsafe {
let dst = base.add(y * stride);
std::ptr::copy_nonoverlapping(src[y * row_bytes..].as_ptr(), dst, row_bytes);
}
}
}
#[cfg(test)]
mod cache_tests {
use super::Cache;
#[test]
fn evicts_the_least_recently_used_idle_value() {
let mut cache = Cache::new(2);
let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
drop(first);
let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
drop(second);
let first = cache
.get_or_insert_with((1, 1), || Err::<(), _>("cached value was recreated"))
.unwrap();
drop(first);
let third = cache.get_or_insert_with((3, 3), || Ok::<_, ()>(())).unwrap();
drop(third);
assert!(cache.values.contains_key(&(1, 1)));
assert!(!cache.values.contains_key(&(2, 2)));
assert!(cache.values.contains_key(&(3, 3)));
assert_eq!(cache.values.len(), 2);
}
#[test]
fn defers_eviction_until_an_active_value_is_released() {
let mut cache = Cache::new(1);
let first = cache.get_or_insert_with((1, 1), || Ok::<_, ()>(())).unwrap();
let second = cache.get_or_insert_with((2, 2), || Ok::<_, ()>(())).unwrap();
assert_eq!(cache.values.len(), 2);
drop(first);
cache.prune();
assert!(!cache.values.contains_key(&(1, 1)));
assert!(cache.values.contains_key(&(2, 2)));
assert_eq!(cache.values.len(), 1);
drop(second);
}
}
}
#[cfg(all(target_os = "linux", feature = "nvdec"))]
pub mod cuda {
use std::sync::{Arc, OnceLock};
use cudarc::driver::{CudaContext, CudaFunction, LaunchConfig, PushKernelArg, result};
use super::I420;
use crate::Error;
const RESIZE_PTX: &str = include_str!("frame/nv12_resize.ptx");
struct Kernels {
luma: CudaFunction,
chroma: CudaFunction,
}
fn kernels(ctx: &Arc<CudaContext>) -> Result<&'static Kernels, Error> {
static KERNELS: OnceLock<Result<Kernels, String>> = OnceLock::new();
KERNELS
.get_or_init(|| {
let module = ctx
.load_module(cudarc::nvrtc::Ptx::from_src(RESIZE_PTX))
.map_err(|e| format!("load nv12_resize PTX: {e:?}"))?;
Ok(Kernels {
luma: module
.load_function("resize_luma")
.map_err(|e| format!("load resize_luma: {e:?}"))?,
chroma: module
.load_function("resize_chroma")
.map_err(|e| format!("load resize_chroma: {e:?}"))?,
})
})
.as_ref()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize unavailable: {e}")))
}
struct Buffer {
ctx: Arc<CudaContext>,
ptr: cudarc::driver::sys::CUdeviceptr,
len: usize,
}
impl Drop for Buffer {
fn drop(&mut self) {
if self.ctx.bind_to_thread().is_ok() {
let _ = unsafe { result::free_sync(self.ptr) };
}
}
}
#[derive(Clone)]
pub struct Frame {
buf: Arc<Buffer>,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) pitch: u32,
}
impl Frame {
pub(crate) fn alloc(ctx: &Arc<CudaContext>, width: u32, height: u32, pitch: u32) -> Result<Self, Error> {
debug_assert!(pitch >= width && width.is_multiple_of(2) && height.is_multiple_of(2));
let len = pitch as usize * height as usize * 3 / 2;
ctx.bind_to_thread()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
let ptr = unsafe { result::malloc_sync(len) }
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA alloc of {len} bytes: {e:?}")))?;
Ok(Self {
buf: Arc::new(Buffer {
ctx: ctx.clone(),
ptr,
len,
}),
width,
height,
pitch,
})
}
pub(crate) fn device_ptr(&self) -> u64 {
self.buf.ptr
}
pub(crate) fn download_i420(&self) -> Result<I420, Error> {
self.buf
.ctx
.bind_to_thread()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA bind: {e:?}")))?;
let mut host = vec![0u8; self.buf.len];
unsafe { result::memcpy_dtoh_sync(&mut host, self.buf.ptr) }
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA download: {e:?}")))?;
let (w, h) = (self.width as usize, self.height as usize);
let (cw, ch) = (w / 2, h / 2);
let pitch = self.pitch as usize;
let mut data = vec![0u8; I420::len(self.width, self.height)];
let (luma, chroma) = data.split_at_mut(w * h);
let (u_dst, v_dst) = chroma.split_at_mut(cw * ch);
for row in 0..h {
luma[row * w..row * w + w].copy_from_slice(&host[row * pitch..row * pitch + w]);
}
let uv_base = pitch * h;
for row in 0..ch {
let src = &host[uv_base + row * pitch..uv_base + row * pitch + w];
for col in 0..cw {
u_dst[row * cw + col] = src[col * 2];
v_dst[row * cw + col] = src[col * 2 + 1];
}
}
Ok(I420 {
width: self.width,
height: self.height,
data,
})
}
pub(crate) fn resize(&self, width: u32, height: u32) -> Result<Self, Error> {
let ctx = &self.buf.ctx;
let kernels = kernels(ctx)?;
let pitch = width.next_multiple_of(256);
let dst = Self::alloc(ctx, width, height, pitch)?;
let stream = ctx.default_stream();
let block = (16u32, 16, 1);
let grid = |w: u32, h: u32| (w.div_ceil(16), h.div_ceil(16), 1);
let launch_err = |plane: &str, e| Error::Codec(anyhow::anyhow!("CUDA resize {plane}: {e:?}"));
unsafe {
stream
.launch_builder(&kernels.luma)
.arg(&self.buf.ptr)
.arg(&self.pitch)
.arg(&self.width)
.arg(&self.height)
.arg(&dst.buf.ptr)
.arg(&pitch)
.arg(&width)
.arg(&height)
.launch(LaunchConfig {
grid_dim: grid(width, height),
block_dim: block,
shared_mem_bytes: 0,
})
}
.map_err(|e| launch_err("luma", e))?;
let src_uv = self.buf.ptr + u64::from(self.pitch) * u64::from(self.height);
let dst_uv = dst.buf.ptr + u64::from(pitch) * u64::from(height);
let (src_pw, src_ph) = (self.width / 2, self.height / 2);
let (dst_pw, dst_ph) = (width / 2, height / 2);
unsafe {
stream
.launch_builder(&kernels.chroma)
.arg(&src_uv)
.arg(&self.pitch)
.arg(&src_pw)
.arg(&src_ph)
.arg(&dst_uv)
.arg(&pitch)
.arg(&dst_pw)
.arg(&dst_ph)
.launch(LaunchConfig {
grid_dim: grid(dst_pw, dst_ph),
block_dim: block,
shared_mem_bytes: 0,
})
}
.map_err(|e| launch_err("chroma", e))?;
stream
.synchronize()
.map_err(|e| Error::Codec(anyhow::anyhow!("CUDA resize sync: {e:?}")))?;
Ok(dst)
}
}
}
#[cfg(target_os = "windows")]
pub mod d3d11 {
use std::ptr;
use windows::Win32::Foundation::HMODULE;
use windows::Win32::Graphics::Direct3D::D3D_DRIVER_TYPE_HARDWARE;
use windows::Win32::Graphics::Direct3D10::ID3D10Multithread;
use windows::Win32::Graphics::Direct3D11::{
D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_MAP_READ,
D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, D3D11CreateDevice,
ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
};
use windows::core::Interface;
use super::I420;
use crate::Error;
fn err(ctx: &str, e: windows::core::Error) -> Error {
Error::Codec(anyhow::anyhow!("{ctx}: {e}"))
}
pub(crate) fn create_device() -> Result<ID3D11Device, Error> {
let mut device: Option<ID3D11Device> = None;
unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
None,
)
.map_err(|e| err("D3D11CreateDevice", e))?;
}
let device = device.ok_or_else(|| Error::Codec(anyhow::anyhow!("D3D11CreateDevice returned null")))?;
let multithread = device
.cast::<ID3D10Multithread>()
.map_err(|e| err("query ID3D10Multithread", e))?;
unsafe {
let _ = multithread.SetMultithreadProtected(true);
}
Ok(device)
}
pub struct Texture {
pub(crate) device: ID3D11Device,
pub(crate) texture: ID3D11Texture2D,
pub(crate) subresource: u32,
pub(crate) width: u32,
pub(crate) height: u32,
}
impl Texture {
pub(crate) fn new(
device: ID3D11Device,
texture: ID3D11Texture2D,
subresource: u32,
width: u32,
height: u32,
) -> Self {
Self {
device,
texture,
subresource,
width,
height,
}
}
pub(crate) fn download_i420(&self) -> Result<I420, Error> {
let context = unsafe { self.device.GetImmediateContext() }.map_err(|e| err("GetImmediateContext", e))?;
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { self.texture.GetDesc(&mut desc) };
desc.ArraySize = 1;
desc.MipLevels = 1;
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32;
desc.MiscFlags = 0;
let mut staging: Option<ID3D11Texture2D> = None;
unsafe {
self.device
.CreateTexture2D(&desc, None, Some(&mut staging))
.map_err(|e| err("CreateTexture2D (staging)", e))?;
}
let staging = staging.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))?;
unsafe {
context.CopySubresourceRegion(&staging, 0, 0, 0, 0, &self.texture, self.subresource, None);
}
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
unsafe {
context
.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
.map_err(|e| err("Map (staging)", e))?;
}
let _guard = UnmapGuard {
context: &context,
resource: &staging,
};
let (w, h) = (self.width as usize, self.height as usize);
let (cw, ch) = (w / 2, h / 2);
let pitch = mapped.RowPitch as usize;
let base = mapped.pData as *const u8;
let tex_height = desc.Height as usize;
let mut data = vec![0u8; I420::len(self.width, self.height)];
let (luma, chroma) = data.split_at_mut(w * h);
let (u_plane, v_plane) = chroma.split_at_mut(cw * ch);
for row in 0..h {
unsafe {
ptr::copy_nonoverlapping(base.add(row * pitch), luma[row * w..].as_mut_ptr(), w);
}
}
let uv_base = unsafe { base.add(pitch * tex_height) };
for row in 0..ch {
let src = unsafe { uv_base.add(row * pitch) };
for col in 0..cw {
unsafe {
u_plane[row * cw + col] = *src.add(col * 2);
v_plane[row * cw + col] = *src.add(col * 2 + 1);
}
}
}
Ok(I420 {
width: self.width,
height: self.height,
data,
})
}
}
struct UnmapGuard<'a> {
context: &'a ID3D11DeviceContext,
resource: &'a ID3D11Texture2D,
}
impl Drop for UnmapGuard<'_> {
fn drop(&mut self) {
unsafe { self.context.Unmap(self.resource, 0) };
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn i420_new_rejects_a_short_buffer() {
use super::I420;
assert!(I420::new(64, 32, vec![0; I420::len(64, 32)]).is_ok());
assert!(I420::new(64, 32, vec![0; I420::len(64, 32) - 1]).is_err());
assert!(I420::new(64, 32, Vec::new()).is_err());
assert!(I420::new(63, 32, vec![0; I420::len(63, 32)]).is_err());
assert!(I420::new(0, 32, Vec::new()).is_err());
}
use super::{Frame, I420, Surface};
use crate::Size;
#[test]
fn surface_rgba_rejects_a_mismatched_buffer() {
let ok = vec![0x80u8; 64 * 32 * 4];
assert!(Surface::rgba(&ok, Size::new(64, 32)).is_ok());
assert!(Surface::rgba(&ok[..ok.len() - 4], Size::new(64, 32)).is_err());
assert!(Surface::rgba(&ok, Size::new(32, 32)).is_err());
assert!(Surface::rgba(&ok, Size::new(0, 32)).is_err());
}
#[test]
fn frame_size_follows_the_surface() {
let rgba = vec![0x80u8; 64 * 32 * 4];
let surface = Surface::rgba(&rgba, Size::new(64, 32)).unwrap();
let frame = Frame::new(surface, moq_net::Timestamp::from_micros(1234).unwrap());
assert_eq!(frame.size(), Size::new(64, 32));
let scaled = frame.resize(Size::new(32, 16)).unwrap();
assert_eq!(scaled.size(), Size::new(32, 16));
assert_eq!(scaled.timestamp, frame.timestamp);
}
#[cfg(target_os = "macos")]
#[test]
fn into_pixel_buffer_uploads_a_cpu_frame() {
use objc2_core_video::{CVPixelBufferGetHeight, CVPixelBufferGetWidth};
let i420 = I420::new(64, 32, vec![0x80; I420::len(64, 32)]).unwrap();
let frame = Frame::new(Surface::I420(i420), moq_net::Timestamp::from_micros(0).unwrap());
let buffer = frame.surface.into_pixel_buffer().expect("upload a CPU frame");
assert_eq!(CVPixelBufferGetWidth(&buffer), 64);
assert_eq!(CVPixelBufferGetHeight(&buffer), 32);
}
fn gradient_i420(width: u32, height: u32) -> I420 {
let (w, h) = (width as usize, height as usize);
let (cw, ch) = (w / 2, h / 2);
let mut data = vec![0u8; I420::len(width, height)];
let (y, chroma) = data.split_at_mut(w * h);
let (u, v) = chroma.split_at_mut(cw * ch);
for row in 0..h {
for col in 0..w {
y[row * w + col] = ((col * 255) / w) as u8;
}
}
for row in 0..ch {
for col in 0..cw {
u[row * cw + col] = ((row * 255) / ch) as u8;
v[row * cw + col] = (((row + col) * 255) / (ch + cw)) as u8;
}
}
I420 { width, height, data }
}
fn mae(a: &[u8], b: &[u8]) -> u64 {
assert_eq!(a.len(), b.len());
a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() / a.len() as u64
}
#[test]
fn i420_resize_follows_gradients() {
let src = gradient_i420(320, 240);
let dst = src.resize(128, 96).unwrap();
assert_eq!((dst.width, dst.height), (128, 96));
let expected = gradient_i420(128, 96);
assert!(mae(dst.y(), expected.y()) < 4, "luma ramp drifted");
assert!(mae(dst.u(), expected.u()) < 4, "u ramp drifted");
assert!(mae(dst.v(), expected.v()) < 4, "v ramp drifted");
}
#[cfg(target_os = "macos")]
#[test]
fn pixel_buffer_resize_matches_cpu() {
let src_i420 = gradient_i420(320, 240);
let src = Surface::PixelBuffer(nv12_surface(&src_i420));
let scaled = src.resize(Size::new(160, 120)).unwrap();
let Surface::PixelBuffer(scaled) = scaled else {
panic!("VideoToolbox resize downloaded to the CPU");
};
let gpu = scaled.download_i420().unwrap();
let cpu = src_i420.resize(160, 120).unwrap();
assert_eq!((gpu.width, gpu.height), (160, 120));
assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
}
#[cfg(target_os = "macos")]
fn nv12_surface(frame: &I420) -> super::macos::PixelBuffer {
use std::ptr::{self, NonNull};
use objc2_core_foundation::CFRetained;
use objc2_core_video::{
CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
};
let mut raw: *mut CVPixelBuffer = ptr::null_mut();
let status = unsafe {
CVPixelBufferCreate(
None,
frame.width as usize,
frame.height as usize,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
None,
NonNull::new(&mut raw).expect("stack pointer is non-null"),
)
};
assert_eq!(status, 0, "CVPixelBufferCreate failed");
let buffer = unsafe { CFRetained::from_raw(NonNull::new(raw).expect("CoreVideo returned a buffer")) };
let flags = CVPixelBufferLockFlags(0);
assert_eq!(unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) }, 0);
let width = frame.width as usize;
let height = frame.height as usize;
let y_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 0) as *mut u8;
let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 0);
for row in 0..height {
unsafe {
ptr::copy_nonoverlapping(frame.y()[row * width..].as_ptr(), y_base.add(row * y_stride), width);
}
}
let (chroma_width, chroma_height) = (width / 2, height / 2);
let uv_base = CVPixelBufferGetBaseAddressOfPlane(&buffer, 1) as *mut u8;
let uv_stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, 1);
for row in 0..chroma_height {
let output = unsafe { uv_base.add(row * uv_stride) };
for col in 0..chroma_width {
unsafe {
*output.add(col * 2) = frame.u()[row * chroma_width + col];
*output.add(col * 2 + 1) = frame.v()[row * chroma_width + col];
}
}
}
unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
super::macos::PixelBuffer::new(buffer, frame.width, frame.height)
}
#[cfg(all(target_os = "linux", feature = "nvdec"))]
#[test]
fn cuda_resize_matches_cpu() {
use std::sync::Arc;
use cudarc::driver::{CudaContext, result};
use super::cuda;
if unsafe { libloading::Library::new("libcuda.so.1") }.is_err() {
return;
}
let Ok(ctx): Result<Arc<CudaContext>, _> = CudaContext::new(0) else {
return;
};
let (w, h) = (322u32, 242u32); let src_i420 = gradient_i420(w, h);
let pitch = 512u32;
let frame = cuda::Frame::alloc(&ctx, w, h, pitch).unwrap();
let mut host = vec![0u8; pitch as usize * h as usize * 3 / 2];
for row in 0..h as usize {
let dst = row * pitch as usize;
host[dst..dst + w as usize].copy_from_slice(&src_i420.y()[row * w as usize..(row + 1) * w as usize]);
}
let (cw, ch) = (w as usize / 2, h as usize / 2);
for row in 0..ch {
let dst = (h as usize + row) * pitch as usize;
for col in 0..cw {
host[dst + 2 * col] = src_i420.u()[row * cw + col];
host[dst + 2 * col + 1] = src_i420.v()[row * cw + col];
}
}
unsafe { result::memcpy_htod_sync(frame.device_ptr(), &host) }.unwrap();
let scaled = frame.resize(160, 120).unwrap();
let gpu = scaled.download_i420().unwrap();
let cpu = src_i420.resize(160, 120).unwrap();
assert_eq!((gpu.width, gpu.height), (160, 120));
assert!(mae(gpu.y(), cpu.y()) < 4, "GPU and CPU luma disagree");
assert!(mae(gpu.u(), cpu.u()) < 4, "GPU and CPU u disagree");
assert!(mae(gpu.v(), cpu.v()) < 4, "GPU and CPU v disagree");
}
}