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