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}