Skip to main content

apple_cf/cm/
sample_buffer.rs

1//! [`CMSampleBuffer`] — framework-agnostic safe wrapper around a `CoreMedia`
2//! `CMSampleBufferRef`.
3//!
4//! This wrapper exposes the *generic* `CMSampleBuffer` surface that every
5//! consumer needs: presentation timestamp, format description, attached
6//! data buffer (`CMBlockBuffer`), sample count, validity. Framework-specific
7//! attachment readers (e.g. `SCStreamFrameInfo`'s frame status, content
8//! rect, dirty rects) live in the consuming crates so that, for example,
9//! `screencapturekit-rs`'s SC-attachment readers don't get pulled into
10//! `videotoolbox-rs`.
11
12use super::{AudioBufferList, CMBlockBuffer, CMFormatDescription, CMTime};
13use crate::cf::{CFArray, CFDictionary};
14use crate::ffi;
15use std::fmt;
16
17/// Owned reference to a `CoreMedia` `CMSampleBufferRef`.
18///
19/// Cloning increments the underlying refcount via `CFRetain`; dropping
20/// releases via `CFRelease`. The pointer is opaque to safe Rust — accessor
21/// methods on this type are the only sanctioned way to inspect it.
22pub struct CMSampleBuffer(*mut std::ffi::c_void);
23
24// SAFETY: CMSampleBufferRef is documented as thread-safe for read access;
25// we only share the opaque pointer between threads and never dereference
26// it from Rust.
27unsafe impl Send for CMSampleBuffer {}
28unsafe impl Sync for CMSampleBuffer {}
29
30impl CMSampleBuffer {
31    /// Wrap a raw `CMSampleBufferRef` without bumping its refcount or
32    /// checking for NULL.
33    ///
34    /// # Safety
35    ///
36    /// `ptr` must be a non-null, live `CMSampleBufferRef` of the exact type
37    /// carrying one retain transferred to this wrapper. The caller must not
38    /// release or separately adopt that retain. For NULL-tolerant construction
39    /// prefer [`Self::from_raw`].
40    #[must_use]
41    pub const unsafe fn from_ptr(ptr: *mut std::ffi::c_void) -> Self {
42        Self(ptr)
43    }
44
45    /// Adopt a raw `CMSampleBufferRef` without bumping its refcount.
46    ///
47    /// Use this when the caller has just received a `+1` retained pointer
48    /// (e.g. a Swift `Unmanaged.passRetained(...).toOpaque()`). The
49    /// returned `CMSampleBuffer` will release the pointer when dropped.
50    ///
51    /// Returns `None` for a NULL pointer.
52    ///
53    /// # Safety
54    ///
55    /// A non-null `ptr` must be a live `CMSampleBufferRef` of the exact type
56    /// carrying one retain transferred to this wrapper. The caller must not
57    /// release or separately adopt that transferred retain.
58    #[must_use]
59    pub unsafe fn from_raw(ptr: *mut std::ffi::c_void) -> Option<Self> {
60        if ptr.is_null() {
61            None
62        } else {
63            Some(Self(ptr))
64        }
65    }
66
67    /// Wrap a raw `CMSampleBufferRef`, calling `CFRetain` to bump its
68    /// refcount before taking ownership.
69    ///
70    /// Use this when the caller holds a borrowed (non-owning) reference
71    /// and wants to take ownership without affecting the source.
72    ///
73    /// # Safety
74    ///
75    /// A non-null `ptr` must be a live `CMSampleBufferRef` of the exact type for
76    /// the duration of the retain call.
77    #[must_use]
78    pub unsafe fn from_raw_borrowed(ptr: *mut std::ffi::c_void) -> Option<Self> {
79        if ptr.is_null() {
80            None
81        } else {
82            let retained = unsafe { ffi::cm_sample_buffer_retain(ptr) };
83            unsafe { Self::from_raw(retained) }
84        }
85    }
86
87    /// Borrow the underlying +0 `CMSampleBufferRef` without changing its
88    /// refcount. The pointer remains valid for the lifetime of `self`.
89    #[must_use]
90    pub const fn as_ptr(&self) -> *mut std::ffi::c_void {
91        self.0
92    }
93
94    /// Whether the sample buffer is in a valid state.
95    #[must_use]
96    pub fn is_valid(&self) -> bool {
97        unsafe { ffi::cm_sample_buffer_is_valid(self.0) }
98    }
99
100    /// Whether the sample buffer's data is ready for consumption.
101    #[must_use]
102    pub fn data_is_ready(&self) -> bool {
103        unsafe { ffi::cm_sample_buffer_data_is_ready(self.0) }
104    }
105
106    /// Number of samples carried by this buffer (1 for video, N for audio).
107    #[must_use]
108    pub fn num_samples(&self) -> i64 {
109        unsafe { ffi::cm_sample_buffer_get_num_samples(self.0) }
110    }
111
112    /// Presentation timestamp of the first sample.
113    ///
114    /// Returns [`CMTime::INVALID`] if the buffer has no PTS.
115    #[must_use]
116    pub fn presentation_timestamp(&self) -> CMTime {
117        let mut t = CMTime::INVALID;
118        unsafe {
119            ffi::cm_sample_buffer_get_presentation_timestamp(
120                self.0,
121                &raw mut t.value,
122                &raw mut t.timescale,
123                &raw mut t.flags,
124                &raw mut t.epoch,
125            );
126        }
127        t
128    }
129
130    /// Decode timestamp of the first sample (matters when there's B-frame
131    /// reordering between PTS and DTS).
132    #[must_use]
133    pub fn decode_timestamp(&self) -> CMTime {
134        let mut t = CMTime::INVALID;
135        unsafe {
136            ffi::cm_sample_buffer_get_decode_timestamp(
137                self.0,
138                &raw mut t.value,
139                &raw mut t.timescale,
140                &raw mut t.flags,
141                &raw mut t.epoch,
142            );
143        }
144        t
145    }
146
147    /// Total duration of all samples in this buffer.
148    #[must_use]
149    pub fn duration(&self) -> CMTime {
150        let mut t = CMTime::INVALID;
151        unsafe {
152            ffi::cm_sample_buffer_get_duration(
153                self.0,
154                &raw mut t.value,
155                &raw mut t.timescale,
156                &raw mut t.flags,
157                &raw mut t.epoch,
158            );
159        }
160        t
161    }
162
163    /// The attached [`CMBlockBuffer`] holding the encoded sample data, if
164    /// the sample buffer is data-bearing (as opposed to image-bearing).
165    ///
166    /// Video frames from `VTCompressionSession` always have a data buffer
167    /// (the encoded NAL units / `ProRes` frame data). Decoded video frames
168    /// from a capture pipeline typically use an image buffer instead — see
169    /// [`Self::image_buffer_ptr_borrowed`].
170    #[must_use]
171    pub fn data_buffer(&self) -> Option<CMBlockBuffer> {
172        let ptr = unsafe { ffi::cm_sample_buffer_get_data_buffer(self.0) };
173        if ptr.is_null() {
174            None
175        } else {
176            // CMSampleBufferGetDataBuffer returns an unretained reference;
177            // bump the refcount so our wrapper can release on drop.
178            let retained = unsafe { ffi::cm_block_buffer_retain(ptr) };
179            unsafe { CMBlockBuffer::from_raw(retained) }
180        }
181    }
182
183    /// Format description (codec, dimensions, audio params, ...) attached
184    /// to this sample buffer.
185    #[must_use]
186    pub fn format_description(&self) -> Option<CMFormatDescription> {
187        let ptr = unsafe { ffi::cm_sample_buffer_get_format_description(self.0) };
188        if ptr.is_null() {
189            None
190        } else {
191            let retained = unsafe { ffi::cm_format_description_retain(ptr) };
192            unsafe { CMFormatDescription::from_raw(retained) }
193        }
194    }
195
196    /// Borrowed +0 `CVImageBufferRef` if the sample is image-bearing.
197    ///
198    /// Returns NULL for sample buffers that don't carry an image buffer
199    /// (for example, compressed video or audio samples). The pointer remains
200    /// valid only while `self` and the sample buffer's image association remain
201    /// alive. Retain it before storing or adopting it.
202    #[must_use]
203    pub fn image_buffer_ptr_borrowed(&self) -> *mut std::ffi::c_void {
204        extern "C" {
205            fn CMSampleBufferGetImageBuffer(
206                sample_buffer: *mut std::ffi::c_void,
207            ) -> *mut std::ffi::c_void;
208        }
209        unsafe { CMSampleBufferGetImageBuffer(self.0) }
210    }
211
212    #[must_use]
213    pub fn sample_attachments(&self) -> Vec<CFDictionary> {
214        let ptr = unsafe { ffi::acf_cm_sample_buffer_copy_sample_attachments(self.0) };
215        let Some(attachments) = (unsafe { CFArray::from_raw(ptr) }) else {
216            return Vec::new();
217        };
218        attachments
219            .values()
220            .into_iter()
221            .filter(|value| value.type_id() == CFDictionary::type_id())
222            .filter_map(|value| unsafe { CFDictionary::from_raw_borrowed(value.as_ptr()) })
223            .collect()
224    }
225
226    #[must_use]
227    pub fn is_sync_sample(&self) -> bool {
228        unsafe { ffi::acf_cm_sample_buffer_is_sync_sample(self.0) }
229    }
230
231    #[allow(clippy::missing_errors_doc)]
232    pub fn audio_buffer_list(&self) -> Result<AudioBufferList, i32> {
233        let mut num_buffers = 0_u32;
234        let mut buffers_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
235        let mut buffers_len = 0_usize;
236        let mut block_buffer_ptr = std::ptr::null_mut();
237        let status = unsafe {
238            ffi::acf_cm_sample_buffer_copy_audio_buffer_list(
239                self.0,
240                &raw mut num_buffers,
241                &raw mut buffers_ptr,
242                &raw mut buffers_len,
243                &raw mut block_buffer_ptr,
244            )
245        };
246        let list = unsafe {
247            AudioBufferList::from_bridge(
248                num_buffers,
249                buffers_ptr.cast(),
250                buffers_len,
251                block_buffer_ptr,
252            )
253        };
254        if status != 0 {
255            return Err(status);
256        }
257        list.ok_or(-50)
258    }
259
260    /// Copy the sample buffer's image buffer into an independently owned wrapper.
261    #[cfg(feature = "cv")]
262    #[must_use]
263    pub fn image_buffer(&self) -> Option<crate::cv::CVImageBuffer> {
264        let ptr = unsafe { ffi::cm_sample_buffer_copy_image_buffer(self.0) };
265        unsafe { crate::cv::CVImageBuffer::from_raw(ptr) }
266    }
267}
268
269crate::utils::retained::cf_retained!(
270    CMSampleBuffer,
271    retain = ffi::cm_sample_buffer_retain,
272    release = ffi::cm_sample_buffer_release,
273);
274
275impl PartialEq for CMSampleBuffer {
276    fn eq(&self, other: &Self) -> bool {
277        self.0 == other.0
278    }
279}
280
281impl Eq for CMSampleBuffer {}
282
283impl std::hash::Hash for CMSampleBuffer {
284    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
285        unsafe {
286            let h = ffi::cm_sample_buffer_hash(self.0);
287            h.hash(state);
288        }
289    }
290}
291
292impl fmt::Debug for CMSampleBuffer {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        f.debug_struct("CMSampleBuffer")
295            .field("ptr", &self.0)
296            .field("num_samples", &self.num_samples())
297            .field("pts", &self.presentation_timestamp())
298            .finish()
299    }
300}