Skip to main content

openipc_video/backends/macos/
surface.rs

1use std::ffi::c_void;
2
3use objc2_core_foundation::CFRetained;
4use objc2_core_video::{
5    kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
6    kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVReturnSuccess, CVPixelBuffer,
7    CVPixelBufferGetBaseAddress, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRow,
8    CVPixelBufferGetBytesPerRowOfPlane, CVPixelBufferGetHeight, CVPixelBufferGetHeightOfPlane,
9    CVPixelBufferGetPixelFormatType, CVPixelBufferGetPlaneCount, CVPixelBufferGetWidth,
10    CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
11};
12
13use crate::{DecodedSurface, FrameDimensions, PixelFormat, VideoError};
14
15/// Read-only view of one mapped CoreVideo pixel-buffer plane.
16#[derive(Debug, Clone, Copy)]
17pub struct MacOsMappedPlane<'a> {
18    data: &'a [u8],
19    stride: usize,
20}
21
22impl<'a> MacOsMappedPlane<'a> {
23    /// Bytes in this plane, including any row padding.
24    pub const fn data(&self) -> &'a [u8] {
25        self.data
26    }
27
28    /// Number of bytes between adjacent rows.
29    pub const fn stride(&self) -> usize {
30        self.stride
31    }
32}
33
34struct PixelBufferLock<'a>(&'a CVPixelBuffer);
35
36impl Drop for PixelBufferLock<'_> {
37    fn drop(&mut self) {
38        // SAFETY: This guard is created only after a successful read-only lock,
39        // and uses the same pixel buffer and flags when releasing it.
40        let _ = unsafe { CVPixelBufferUnlockBaseAddress(self.0, CVPixelBufferLockFlags::ReadOnly) };
41    }
42}
43
44/// Retained VideoToolbox output backed by a CoreVideo pixel buffer.
45pub struct MacOsVideoFrame {
46    pixel_buffer: CFRetained<CVPixelBuffer>,
47    decode_info_flags: u32,
48}
49
50impl std::fmt::Debug for MacOsVideoFrame {
51    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        formatter
53            .debug_struct("MacOsVideoFrame")
54            .field("pixel_buffer", &CFRetained::as_ptr(&self.pixel_buffer))
55            .field("decode_info_flags", &self.decode_info_flags)
56            .finish()
57    }
58}
59
60// CoreVideo pixel buffers are reference-counted, may be retained across the
61// VideoToolbox callback boundary, and are designed for cross-thread GPU and
62// display handoff. This wrapper exposes only immutable metadata operations.
63unsafe impl Send for MacOsVideoFrame {}
64unsafe impl Sync for MacOsVideoFrame {}
65
66impl MacOsVideoFrame {
67    pub(crate) fn new(pixel_buffer: CFRetained<CVPixelBuffer>, decode_info_flags: u32) -> Self {
68        Self {
69            pixel_buffer,
70            decode_info_flags,
71        }
72    }
73
74    /// Borrow the retained CoreVideo pixel buffer.
75    pub fn pixel_buffer(&self) -> &CVPixelBuffer {
76        &self.pixel_buffer
77    }
78
79    /// VideoToolbox information flags reported for this output frame.
80    pub const fn decode_info_flags(&self) -> u32 {
81        self.decode_info_flags
82    }
83
84    /// Whether the output is IOSurface-backed for native GPU interop.
85    pub fn is_io_surface_backed(&self) -> bool {
86        unsafe extern "C-unwind" {
87            fn CVPixelBufferGetIOSurface(pixel_buffer: &CVPixelBuffer) -> *mut c_void;
88        }
89        // SAFETY: The retained pixel buffer is a valid CoreVideo object. The
90        // Get function returns a non-owning pointer used only for a null check.
91        !unsafe { CVPixelBufferGetIOSurface(&self.pixel_buffer) }.is_null()
92    }
93
94    /// Map the decoded image for a scoped, read-only CPU access.
95    ///
96    /// The mapped slices cannot outlive the callback. Native GPU consumers
97    /// should use [`Self::pixel_buffer`] directly to avoid this readback.
98    pub fn with_mapped_planes<R>(
99        &self,
100        callback: impl FnOnce(&[MacOsMappedPlane<'_>]) -> R,
101    ) -> Result<R, VideoError> {
102        // SAFETY: The retained pixel buffer is valid and is unlocked by the
103        // guard below before this method returns, including during unwinding.
104        let status = unsafe {
105            CVPixelBufferLockBaseAddress(&self.pixel_buffer, CVPixelBufferLockFlags::ReadOnly)
106        };
107        if status != kCVReturnSuccess {
108            return Err(VideoError::Platform {
109                api: "CVPixelBufferLockBaseAddress",
110                status,
111            });
112        }
113        let _lock = PixelBufferLock(&self.pixel_buffer);
114        let plane_count = CVPixelBufferGetPlaneCount(&self.pixel_buffer);
115        let mut planes = Vec::with_capacity(plane_count.max(1));
116
117        if plane_count == 0 {
118            let pointer = CVPixelBufferGetBaseAddress(&self.pixel_buffer).cast::<u8>();
119            let stride = CVPixelBufferGetBytesPerRow(&self.pixel_buffer);
120            let length = stride.saturating_mul(CVPixelBufferGetHeight(&self.pixel_buffer));
121            if pointer.is_null() && length != 0 {
122                return Err(VideoError::Backend {
123                    backend: "videotoolbox",
124                    operation: "map pixel buffer",
125                    message: "CoreVideo returned a null base address".to_owned(),
126                });
127            }
128            // SAFETY: CoreVideo keeps the locked base address valid for
129            // `stride * height` bytes until `_lock` is dropped.
130            let data = unsafe { std::slice::from_raw_parts(pointer, length) };
131            planes.push(MacOsMappedPlane { data, stride });
132        } else {
133            for index in 0..plane_count {
134                let pointer =
135                    CVPixelBufferGetBaseAddressOfPlane(&self.pixel_buffer, index).cast::<u8>();
136                let stride = CVPixelBufferGetBytesPerRowOfPlane(&self.pixel_buffer, index);
137                let length =
138                    stride.saturating_mul(CVPixelBufferGetHeightOfPlane(&self.pixel_buffer, index));
139                if pointer.is_null() && length != 0 {
140                    return Err(VideoError::Backend {
141                        backend: "videotoolbox",
142                        operation: "map pixel-buffer plane",
143                        message: format!("CoreVideo returned a null address for plane {index}"),
144                    });
145                }
146                // SAFETY: CoreVideo keeps each locked plane valid for
147                // `stride * plane_height` bytes until `_lock` is dropped.
148                let data = unsafe { std::slice::from_raw_parts(pointer, length) };
149                planes.push(MacOsMappedPlane { data, stride });
150            }
151        }
152
153        Ok(callback(&planes))
154    }
155}
156
157impl DecodedSurface for MacOsVideoFrame {
158    fn dimensions(&self) -> FrameDimensions {
159        FrameDimensions {
160            width: u32::try_from(CVPixelBufferGetWidth(&self.pixel_buffer)).unwrap_or(u32::MAX),
161            height: u32::try_from(CVPixelBufferGetHeight(&self.pixel_buffer)).unwrap_or(u32::MAX),
162        }
163    }
164
165    fn pixel_format(&self) -> PixelFormat {
166        let value = CVPixelBufferGetPixelFormatType(&self.pixel_buffer);
167        if value == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange {
168            PixelFormat::Nv12VideoRange
169        } else if value == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange {
170            PixelFormat::Nv12FullRange
171        } else if value == kCVPixelFormatType_32BGRA {
172            PixelFormat::Bgra8
173        } else {
174            PixelFormat::Native(value)
175        }
176    }
177}