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 /// The caller must ensure `ptr` is a valid `CMSampleBufferRef` retained
36 /// at +1 (the wrapper will release on drop). For NULL-tolerant
37 /// construction prefer [`Self::from_raw`].
38 #[must_use]
39 pub const unsafe fn from_ptr(ptr: *mut std::ffi::c_void) -> Self {
40 Self(ptr)
41 }
42
43 /// Wrap a raw `CMSampleBufferRef` without bumping its refcount.
44 ///
45 /// Use this when the caller has just received a `+1` retained pointer
46 /// (e.g. a Swift `Unmanaged.passRetained(...).toOpaque()`). The
47 /// returned `CMSampleBuffer` will release the pointer when dropped.
48 ///
49 /// Returns `None` for a NULL pointer.
50 #[must_use]
51 pub fn from_raw(ptr: *mut std::ffi::c_void) -> Option<Self> {
52 if ptr.is_null() {
53 None
54 } else {
55 Some(Self(ptr))
56 }
57 }
58
59 /// Wrap a raw `CMSampleBufferRef`, calling `CFRetain` to bump its
60 /// refcount before taking ownership.
61 ///
62 /// Use this when the caller holds a borrowed (non-owning) reference
63 /// and wants to take ownership without affecting the source.
64 ///
65 /// # Safety
66 ///
67 /// `ptr` must be a valid `CMSampleBufferRef` (or NULL). Passing a
68 /// dangling or wrong-type pointer is undefined behaviour.
69 #[must_use]
70 pub unsafe fn from_raw_retained(ptr: *mut std::ffi::c_void) -> Option<Self> {
71 if ptr.is_null() {
72 None
73 } else {
74 let retained = unsafe { ffi::cm_sample_buffer_retain(ptr) };
75 Self::from_raw(retained)
76 }
77 }
78
79 /// Borrow the underlying `CMSampleBufferRef` for hand-off to other
80 /// Apple bindings without changing its refcount. The pointer remains
81 /// valid for the lifetime of `self`.
82 #[must_use]
83 pub const fn as_ptr(&self) -> *mut std::ffi::c_void {
84 self.0
85 }
86
87 /// Whether the sample buffer is in a valid state.
88 #[must_use]
89 pub fn is_valid(&self) -> bool {
90 unsafe { ffi::cm_sample_buffer_is_valid(self.0) }
91 }
92
93 /// Whether the sample buffer's data is ready for consumption.
94 #[must_use]
95 pub fn data_is_ready(&self) -> bool {
96 unsafe { ffi::cm_sample_buffer_data_is_ready(self.0) }
97 }
98
99 /// Number of samples carried by this buffer (1 for video, N for audio).
100 #[must_use]
101 pub fn num_samples(&self) -> i64 {
102 unsafe { ffi::cm_sample_buffer_get_num_samples(self.0) }
103 }
104
105 /// Presentation timestamp of the first sample.
106 ///
107 /// Returns [`CMTime::INVALID`] if the buffer has no PTS.
108 #[must_use]
109 pub fn presentation_timestamp(&self) -> CMTime {
110 let mut t = CMTime::INVALID;
111 unsafe {
112 ffi::cm_sample_buffer_get_presentation_timestamp(
113 self.0,
114 &mut t.value,
115 &mut t.timescale,
116 &mut t.flags,
117 &mut t.epoch,
118 );
119 }
120 t
121 }
122
123 /// Decode timestamp of the first sample (matters when there's B-frame
124 /// reordering between PTS and DTS).
125 #[must_use]
126 pub fn decode_timestamp(&self) -> CMTime {
127 let mut t = CMTime::INVALID;
128 unsafe {
129 ffi::cm_sample_buffer_get_decode_timestamp(
130 self.0,
131 &mut t.value,
132 &mut t.timescale,
133 &mut t.flags,
134 &mut t.epoch,
135 );
136 }
137 t
138 }
139
140 /// Total duration of all samples in this buffer.
141 #[must_use]
142 pub fn duration(&self) -> CMTime {
143 let mut t = CMTime::INVALID;
144 unsafe {
145 ffi::cm_sample_buffer_get_duration(
146 self.0,
147 &mut t.value,
148 &mut t.timescale,
149 &mut t.flags,
150 &mut t.epoch,
151 );
152 }
153 t
154 }
155
156 /// The attached [`CMBlockBuffer`] holding the encoded sample data, if
157 /// the sample buffer is data-bearing (as opposed to image-bearing).
158 ///
159 /// Video frames from `VTCompressionSession` always have a data buffer
160 /// (the encoded NAL units / `ProRes` frame data). Decoded video frames
161 /// from a capture pipeline typically use an image buffer instead — see
162 /// [`Self::image_buffer_ptr`].
163 #[must_use]
164 pub fn data_buffer(&self) -> Option<CMBlockBuffer> {
165 let ptr = unsafe { ffi::cm_sample_buffer_get_data_buffer(self.0) };
166 if ptr.is_null() {
167 None
168 } else {
169 // CMSampleBufferGetDataBuffer returns an unretained reference;
170 // bump the refcount so our wrapper can release on drop.
171 let retained = unsafe { ffi::cm_block_buffer_retain(ptr) };
172 CMBlockBuffer::from_raw(retained)
173 }
174 }
175
176 /// Format description (codec, dimensions, audio params, ...) attached
177 /// to this sample buffer.
178 #[must_use]
179 pub fn format_description(&self) -> Option<CMFormatDescription> {
180 let ptr = unsafe { ffi::cm_sample_buffer_get_format_description(self.0) };
181 if ptr.is_null() {
182 None
183 } else {
184 let retained = unsafe { ffi::cm_format_description_retain(ptr) };
185 CMFormatDescription::from_raw(retained)
186 }
187 }
188
189 /// Raw `CVImageBufferRef` if the sample is image-bearing (decoded
190 /// video frames from a capture pipeline). Use a CV crate's wrapper to
191 /// turn this into a safe `CVPixelBuffer`.
192 ///
193 /// Returns NULL for sample buffers that don't carry an image buffer
194 /// (e.g. compressed video from `VideoToolbox`, audio samples).
195 #[must_use]
196 pub fn image_buffer_ptr(&self) -> *mut std::ffi::c_void {
197 unsafe { ffi::cm_sample_buffer_get_image_buffer(self.0) }
198 }
199}
200
201impl Clone for CMSampleBuffer {
202 fn clone(&self) -> Self {
203 let retained = unsafe { ffi::cm_sample_buffer_retain(self.0) };
204 Self(retained)
205 }
206}
207
208impl Drop for CMSampleBuffer {
209 fn drop(&mut self) {
210 if !self.0.is_null() {
211 unsafe { ffi::cm_sample_buffer_release(self.0) };
212 }
213 }
214}
215
216impl PartialEq for CMSampleBuffer {
217 fn eq(&self, other: &Self) -> bool {
218 self.0 == other.0
219 }
220}
221
222impl Eq for CMSampleBuffer {}
223
224impl std::hash::Hash for CMSampleBuffer {
225 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
226 unsafe {
227 let h = ffi::cm_sample_buffer_hash(self.0);
228 h.hash(state);
229 }
230 }
231}
232
233impl fmt::Debug for CMSampleBuffer {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 f.debug_struct("CMSampleBuffer")
236 .field("ptr", &self.0)
237 .field("num_samples", &self.num_samples())
238 .field("pts", &self.presentation_timestamp())
239 .finish()
240 }
241}