Skip to main content

screencapturekit/cm/
sample_buffer.rs

1//! `CMSampleBuffer` — re-exported from [`apple_cf::cm::CMSampleBuffer`] plus
2//! `ScreenCaptureKit`-specific extension traits for the `SCStreamFrameInfo`
3//! attachment readers and the few sample-buffer accessors that aren't
4//! framework-agnostic enough to live in `apple-cf` yet.
5//!
6//! Bring [`CMSampleBufferSCExt`] into scope to call `frame_status()`,
7//! `display_time()`, `frame_info()`, etc. on any `CMSampleBuffer` carrying
8//! `ScreenCaptureKit` attachments.
9//!
10//! Bring [`CMSampleBufferExt`] into scope for the
11//! `pixel_buffer()`/`make_data_ready()` convenience accessors.
12
13use super::ffi;
14use super::{CMBlockBuffer, CMSampleTimingInfo, CMTime, SCFrameStatus};
15use crate::cv::CVPixelBuffer;
16
17/// Re-exported `CMSampleBuffer` — same opaque-pointer wrapper used across
18/// the doom-fish suite.
19pub use apple_cf::cm::CMSampleBuffer;
20
21// ------------------------------------------------------------------
22// FrameInfoFields — bit flags for the batched frame_info reader.
23// ------------------------------------------------------------------
24
25/// Bit flags marking which fields the batched [`CMSampleBufferSCExt::frame_info`]
26/// fetch managed to populate. Mirrors `FrameInfoFieldBits` in the Swift
27/// bridge — keep them in sync.
28struct FrameInfoFields;
29
30impl FrameInfoFields {
31    const STATUS: u32 = 1 << 0;
32    const DISPLAY_TIME: u32 = 1 << 1;
33    const SCALE_FACTOR: u32 = 1 << 2;
34    const CONTENT_SCALE: u32 = 1 << 3;
35    const CONTENT_RECT: u32 = 1 << 4;
36    const BOUNDING_RECT: u32 = 1 << 5;
37    const SCREEN_RECT: u32 = 1 << 6;
38    const PRESENTER_OVERLAY_RECT: u32 = 1 << 7;
39    const DIRTY_RECTS: u32 = 1 << 8;
40}
41
42/// Snapshot of every `SCStreamFrameInfo` attachment on a sample buffer.
43///
44/// Returned by [`CMSampleBufferSCExt::frame_info`]. Each field is `Some` when
45/// the underlying attachment was present (depends on macOS version, output
46/// type, and stream configuration); `None` indicates the attachment was
47/// missing. Every key `ScreenCaptureKit` documents on `SCStreamFrameInfo` has
48/// a field here, so a `FrameInfo` is a faithful, complete representation of
49/// the attachment dictionary — there is no attachment you still have to reach
50/// for a single-key accessor to read.
51#[allow(clippy::derive_partial_eq_without_eq)]
52#[derive(Debug, Default, Clone, PartialEq)]
53pub struct FrameInfo {
54    /// `SCStreamFrameInfo.status` — frame completeness / idle state.
55    pub frame_status: Option<SCFrameStatus>,
56    /// `SCStreamFrameInfo.displayTime` — mach absolute time the frame was
57    /// composited.
58    pub display_time: Option<u64>,
59    /// `SCStreamFrameInfo.scaleFactor` — display scale (e.g. 2.0 for Retina).
60    pub scale_factor: Option<f64>,
61    /// `SCStreamFrameInfo.contentScale` — capture scale relative to the
62    /// source content.
63    pub content_scale: Option<f64>,
64    /// `SCStreamFrameInfo.contentRect` — captured content within the frame.
65    pub content_rect: Option<crate::cg::CGRect>,
66    /// `SCStreamFrameInfo.boundingRect` — bounding rect of all captured
67    /// windows (macOS 14.0+).
68    pub bounding_rect: Option<crate::cg::CGRect>,
69    /// `SCStreamFrameInfo.screenRect` — full screen rect (macOS 13.1+).
70    pub screen_rect: Option<crate::cg::CGRect>,
71    /// `SCStreamFrameInfo.presenterOverlayContentRect` — Presenter Overlay
72    /// bounding rect (macOS 14.2+).
73    pub presenter_overlay_content_rect: Option<crate::cg::CGRect>,
74    /// `SCStreamFrameInfo.dirtyRects` — regions that changed since the
75    /// previous frame. `Some(vec)` is always non-empty; an attachment holding
76    /// zero usable rects reads back as `None`.
77    pub dirty_rects: Option<Vec<crate::cg::CGRect>>,
78}
79
80// ------------------------------------------------------------------
81// CMSampleBufferSCExt — ScreenCaptureKit-specific attachment readers.
82// ------------------------------------------------------------------
83
84/// Extension trait that exposes `SCStreamFrameInfo` attachment accessors on
85/// any [`CMSampleBuffer`] produced by `ScreenCaptureKit`.
86///
87/// These are SC-specific by design: they read attachment keys defined on
88/// `SCStreamFrameInfo` and are meaningless on sample buffers from other
89/// sources (videotoolbox, `AVFoundation` capture, etc.).
90pub trait CMSampleBufferSCExt {
91    /// `SCStreamFrameInfo.status` attachment.
92    fn frame_status(&self) -> Option<SCFrameStatus>;
93    /// `SCStreamFrameInfo.displayTime` attachment.
94    fn display_time(&self) -> Option<u64>;
95    /// `SCStreamFrameInfo.scaleFactor` attachment.
96    fn scale_factor(&self) -> Option<f64>;
97    /// `SCStreamFrameInfo.contentScale` attachment.
98    fn content_scale(&self) -> Option<f64>;
99    /// `SCStreamFrameInfo.contentRect` attachment.
100    fn content_rect(&self) -> Option<crate::cg::CGRect>;
101    /// `SCStreamFrameInfo.boundingRect` attachment.
102    fn bounding_rect(&self) -> Option<crate::cg::CGRect>;
103    /// `SCStreamFrameInfo.screenRect` attachment.
104    fn screen_rect(&self) -> Option<crate::cg::CGRect>;
105    /// `SCStreamFrameInfo.presenterOverlayContentRect` attachment.
106    fn presenter_overlay_content_rect(&self) -> Option<crate::cg::CGRect>;
107    /// `SCStreamFrameInfo.dirtyRects` attachment.
108    fn dirty_rects(&self) -> Option<Vec<crate::cg::CGRect>>;
109    /// Read every populated `SCStreamFrameInfo` attachment in a single
110    /// FFI round-trip.
111    fn frame_info(&self) -> Option<FrameInfo>;
112}
113
114impl CMSampleBufferSCExt for CMSampleBuffer {
115    fn frame_status(&self) -> Option<SCFrameStatus> {
116        let mut status = 0_i32;
117        unsafe { ffi::cm_sample_buffer_get_frame_status(self.as_ptr(), &raw mut status) }
118            .then(|| SCFrameStatus::from_raw(status))
119    }
120
121    fn display_time(&self) -> Option<u64> {
122        unsafe {
123            let mut value: u64 = 0;
124            if ffi::cm_sample_buffer_get_display_time(self.as_ptr(), &raw mut value) {
125                Some(value)
126            } else {
127                None
128            }
129        }
130    }
131
132    fn scale_factor(&self) -> Option<f64> {
133        unsafe {
134            let mut value: f64 = 0.0;
135            if ffi::cm_sample_buffer_get_scale_factor(self.as_ptr(), &raw mut value) {
136                Some(value)
137            } else {
138                None
139            }
140        }
141    }
142
143    fn content_scale(&self) -> Option<f64> {
144        unsafe {
145            let mut value: f64 = 0.0;
146            if ffi::cm_sample_buffer_get_content_scale(self.as_ptr(), &raw mut value) {
147                Some(value)
148            } else {
149                None
150            }
151        }
152    }
153
154    fn content_rect(&self) -> Option<crate::cg::CGRect> {
155        unsafe {
156            let mut x = 0.0;
157            let mut y = 0.0;
158            let mut w = 0.0;
159            let mut h = 0.0;
160            if ffi::cm_sample_buffer_get_content_rect(
161                self.as_ptr(),
162                &raw mut x,
163                &raw mut y,
164                &raw mut w,
165                &raw mut h,
166            ) {
167                Some(crate::cg::CGRect::new(x, y, w, h))
168            } else {
169                None
170            }
171        }
172    }
173
174    fn bounding_rect(&self) -> Option<crate::cg::CGRect> {
175        unsafe {
176            let mut x = 0.0;
177            let mut y = 0.0;
178            let mut w = 0.0;
179            let mut h = 0.0;
180            if ffi::cm_sample_buffer_get_bounding_rect(
181                self.as_ptr(),
182                &raw mut x,
183                &raw mut y,
184                &raw mut w,
185                &raw mut h,
186            ) {
187                Some(crate::cg::CGRect::new(x, y, w, h))
188            } else {
189                None
190            }
191        }
192    }
193
194    fn screen_rect(&self) -> Option<crate::cg::CGRect> {
195        unsafe {
196            let mut x = 0.0;
197            let mut y = 0.0;
198            let mut w = 0.0;
199            let mut h = 0.0;
200            if ffi::cm_sample_buffer_get_screen_rect(
201                self.as_ptr(),
202                &raw mut x,
203                &raw mut y,
204                &raw mut w,
205                &raw mut h,
206            ) {
207                Some(crate::cg::CGRect::new(x, y, w, h))
208            } else {
209                None
210            }
211        }
212    }
213
214    fn presenter_overlay_content_rect(&self) -> Option<crate::cg::CGRect> {
215        #[cfg(feature = "macos_14_2")]
216        unsafe {
217            let mut x = 0.0;
218            let mut y = 0.0;
219            let mut w = 0.0;
220            let mut h = 0.0;
221            if ffi::cm_sample_buffer_get_presenter_overlay_content_rect(
222                self.as_ptr(),
223                &raw mut x,
224                &raw mut y,
225                &raw mut w,
226                &raw mut h,
227            ) {
228                Some(crate::cg::CGRect::new(x, y, w, h))
229            } else {
230                None
231            }
232        }
233        #[cfg(not(feature = "macos_14_2"))]
234        None
235    }
236
237    fn dirty_rects(&self) -> Option<Vec<crate::cg::CGRect>> {
238        unsafe {
239            let mut rects_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
240            let mut count: usize = 0;
241            if !ffi::cm_sample_buffer_get_dirty_rects(
242                self.as_ptr(),
243                &raw mut rects_ptr,
244                &raw mut count,
245            ) {
246                return None;
247            }
248            take_dirty_rects(rects_ptr, count)
249        }
250    }
251
252    fn frame_info(&self) -> Option<FrameInfo> {
253        unsafe {
254            let mut fields: u32 = 0;
255            let mut status: i32 = 0;
256            let mut display_time: u64 = 0;
257            let mut scale_factor: f64 = 0.0;
258            let mut content_scale: f64 = 0.0;
259            let mut content_rect = [0.0_f64; 4];
260            let mut bounding_rect = [0.0_f64; 4];
261            let mut screen_rect = [0.0_f64; 4];
262            let mut presenter_overlay_rect = [0.0_f64; 4];
263            let mut dirty_rects_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
264            let mut dirty_rects_count: usize = 0;
265            if !ffi::cm_sample_buffer_get_frame_info(
266                self.as_ptr(),
267                &raw mut fields,
268                &raw mut status,
269                &raw mut display_time,
270                &raw mut scale_factor,
271                &raw mut content_scale,
272                content_rect.as_mut_ptr(),
273                bounding_rect.as_mut_ptr(),
274                screen_rect.as_mut_ptr(),
275                presenter_overlay_rect.as_mut_ptr(),
276                &raw mut dirty_rects_ptr,
277                &raw mut dirty_rects_count,
278            ) {
279                // The bridge only allocates the dirty-rect array when it sets
280                // the DIRTY_RECTS bit, but free defensively so an unexpected
281                // false return can never leak it.
282                drop(take_dirty_rects(dirty_rects_ptr, dirty_rects_count));
283                return None;
284            }
285            let to_rect = |a: [f64; 4]| crate::cg::CGRect::new(a[0], a[1], a[2], a[3]);
286            let dirty_rects = if (fields & FrameInfoFields::DIRTY_RECTS) != 0 {
287                take_dirty_rects(dirty_rects_ptr, dirty_rects_count)
288            } else {
289                drop(take_dirty_rects(dirty_rects_ptr, dirty_rects_count));
290                None
291            };
292            Some(FrameInfo {
293                frame_status: ((fields & FrameInfoFields::STATUS) != 0)
294                    .then(|| SCFrameStatus::from_raw(status)),
295                display_time: ((fields & FrameInfoFields::DISPLAY_TIME) != 0)
296                    .then_some(display_time),
297                scale_factor: ((fields & FrameInfoFields::SCALE_FACTOR) != 0)
298                    .then_some(scale_factor),
299                content_scale: ((fields & FrameInfoFields::CONTENT_SCALE) != 0)
300                    .then_some(content_scale),
301                content_rect: ((fields & FrameInfoFields::CONTENT_RECT) != 0)
302                    .then(|| to_rect(content_rect)),
303                bounding_rect: ((fields & FrameInfoFields::BOUNDING_RECT) != 0)
304                    .then(|| to_rect(bounding_rect)),
305                screen_rect: ((fields & FrameInfoFields::SCREEN_RECT) != 0)
306                    .then(|| to_rect(screen_rect)),
307                presenter_overlay_content_rect: ((fields
308                    & FrameInfoFields::PRESENTER_OVERLAY_RECT)
309                    != 0)
310                    .then(|| to_rect(presenter_overlay_rect)),
311                dirty_rects,
312            })
313        }
314    }
315}
316
317/// Copy a bridge-allocated `[x, y, w, h] * count` array into owned
318/// [`CGRect`](crate::cg::CGRect)s and hand the allocation back to the bridge.
319///
320/// # Safety
321///
322/// `rects_ptr` must be null or a bridge-allocated array of `count * 4` `f64`s
323/// that has not been freed yet.
324unsafe fn take_dirty_rects(
325    rects_ptr: *mut std::ffi::c_void,
326    count: usize,
327) -> Option<Vec<crate::cg::CGRect>> {
328    if rects_ptr.is_null() {
329        return None;
330    }
331    let rects_typed = rects_ptr.cast::<f64>();
332    let mut rects = Vec::with_capacity(count);
333    for i in 0..count {
334        unsafe {
335            let base = rects_typed.add(i * 4);
336            rects.push(crate::cg::CGRect::new(
337                *base,
338                *base.add(1),
339                *base.add(2),
340                *base.add(3),
341            ));
342        }
343    }
344    unsafe { ffi::cm_sample_buffer_free_dirty_rects(rects_ptr) };
345    (!rects.is_empty()).then_some(rects)
346}
347
348// ------------------------------------------------------------------
349// CMSampleBufferExt — crate-specific convenience accessors.
350// ------------------------------------------------------------------
351
352/// Extension trait carrying `CMSampleBuffer` convenience accessors used by
353/// this crate.
354pub trait CMSampleBufferExt {
355    /// Construct a sample buffer wrapping a `CVPixelBuffer`.
356    ///
357    /// # Errors
358    ///
359    /// Returns the underlying `OSStatus` if `CoreMedia` fails to create the
360    /// sample buffer.
361    fn create_for_image_buffer(
362        image_buffer: &CVPixelBuffer,
363        presentation_time: CMTime,
364        duration: CMTime,
365    ) -> Result<Self, i32>
366    where
367        Self: Sized;
368
369    /// Return an owned `CVPixelBuffer` for the attached image buffer, if any.
370    fn pixel_buffer(&self) -> Option<CVPixelBuffer>;
371
372    /// Output presentation timestamp (after timing adjustments).
373    fn output_presentation_timestamp(&self) -> CMTime;
374
375    /// Override the output presentation timestamp.
376    ///
377    /// # Errors
378    ///
379    /// Returns the underlying `OSStatus` if `CoreMedia` rejects the new value.
380    fn set_output_presentation_timestamp(&self, time: CMTime) -> Result<(), i32>;
381
382    /// Size of one sample at `index` in bytes.
383    fn sample_size(&self, index: usize) -> usize;
384
385    /// Sum of all sample sizes in this buffer.
386    fn total_sample_size(&self) -> usize;
387
388    /// Whether the underlying data is ready for reading.
389    fn is_data_ready(&self) -> bool;
390
391    /// Mark the underlying data as ready (flushes any pending make-data-ready
392    /// callbacks).
393    ///
394    /// # Errors
395    ///
396    /// Returns the underlying `OSStatus` if `CoreMedia` reports failure.
397    fn make_data_ready(&self) -> Result<(), i32>;
398
399    /// Read the timing info for the sample at `index`.
400    ///
401    /// # Errors
402    ///
403    /// Returns the underlying `OSStatus` if `index` is out of range.
404    fn sample_timing_info(&self, index: usize) -> Result<CMSampleTimingInfo, i32>;
405
406    /// Build an [`apple_cf::cg::CGImage`] from the buffer's attached
407    /// `CVImageBuffer`.
408    ///
409    /// Backed by `VTCreateCGImageFromCVPixelBuffer`, which understands every
410    /// pixel format `ScreenCaptureKit` (or any other `CoreMedia` producer) can
411    /// emit — BGRA, 420v YCbCr 8-bit bi-planar video range, l10r 10-bit ARGB,
412    /// etc. — and uses Apple's hardware path when one exists. The resulting
413    /// `CGImage` is `IOSurface`-backed when the source was, so passing it
414    /// straight into `ImageIO` (`CGImageDestinationAddImage` / `imageio-rs`
415    /// `ImageDestination::add_cg_image`) or into Metal sampling avoids any
416    /// host-side pixel copy.
417    ///
418    /// Returns the canonical `apple_cf::cg::CGImage` (the same type used by
419    /// `imageio-rs` and every other doom-fish suite crate that consumes
420    /// `CGImage`s), so the result flows straight into safe APIs with no
421    /// pointer juggling at the callsite.
422    ///
423    /// # Errors
424    ///
425    /// Returns the underlying `OSStatus` from `VTCreateCGImageFromCVPixelBuffer`
426    /// (or `-12731` `kCMSampleBufferError_NoSampleBufferContent` when the
427    /// buffer has no image buffer attached — typical for audio-only or
428    /// timing-metadata-only samples).
429    fn cg_image(&self) -> Result<apple_cf::cg::CGImage, i32>;
430}
431
432impl CMSampleBufferExt for CMSampleBuffer {
433    fn create_for_image_buffer(
434        image_buffer: &CVPixelBuffer,
435        presentation_time: CMTime,
436        duration: CMTime,
437    ) -> Result<Self, i32> {
438        unsafe {
439            let mut sample_buffer_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
440            let status = ffi::cm_sample_buffer_create_for_image_buffer(
441                image_buffer.as_ptr(),
442                presentation_time.value,
443                presentation_time.timescale,
444                presentation_time.flags,
445                presentation_time.epoch,
446                duration.value,
447                duration.timescale,
448                duration.flags,
449                duration.epoch,
450                &raw mut sample_buffer_ptr,
451            );
452            if status == 0 && !sample_buffer_ptr.is_null() {
453                Ok(Self::from_ptr(sample_buffer_ptr))
454            } else {
455                Err(status)
456            }
457        }
458    }
459
460    fn pixel_buffer(&self) -> Option<CVPixelBuffer> {
461        let ptr = self.image_buffer_ptr_borrowed();
462        // SAFETY: the pointer is borrowed from `self` and remains live for the
463        // duration of the retain performed by `from_raw_borrowed`.
464        unsafe { CVPixelBuffer::from_raw_borrowed(ptr) }
465    }
466
467    fn output_presentation_timestamp(&self) -> CMTime {
468        unsafe {
469            let mut value: i64 = 0;
470            let mut timescale: i32 = 0;
471            let mut flags: u32 = 0;
472            let mut epoch: i64 = 0;
473            ffi::cm_sample_buffer_get_output_presentation_timestamp(
474                self.as_ptr(),
475                &raw mut value,
476                &raw mut timescale,
477                &raw mut flags,
478                &raw mut epoch,
479            );
480            CMTime {
481                value,
482                timescale,
483                flags,
484                epoch,
485            }
486        }
487    }
488
489    fn set_output_presentation_timestamp(&self, time: CMTime) -> Result<(), i32> {
490        let status = unsafe {
491            ffi::cm_sample_buffer_set_output_presentation_timestamp(
492                self.as_ptr(),
493                time.value,
494                time.timescale,
495                time.flags,
496                time.epoch,
497            )
498        };
499        if status == 0 {
500            Ok(())
501        } else {
502            Err(status)
503        }
504    }
505
506    fn sample_size(&self, index: usize) -> usize {
507        unsafe { ffi::cm_sample_buffer_get_sample_size(self.as_ptr(), index) }
508    }
509
510    fn total_sample_size(&self) -> usize {
511        unsafe { ffi::cm_sample_buffer_get_total_sample_size(self.as_ptr()) }
512    }
513
514    fn is_data_ready(&self) -> bool {
515        unsafe { ffi::cm_sample_buffer_is_ready_for_data_access(self.as_ptr()) }
516    }
517
518    fn make_data_ready(&self) -> Result<(), i32> {
519        let status = unsafe { ffi::cm_sample_buffer_make_data_ready(self.as_ptr()) };
520        if status == 0 {
521            Ok(())
522        } else {
523            Err(status)
524        }
525    }
526
527    fn sample_timing_info(&self, index: usize) -> Result<CMSampleTimingInfo, i32> {
528        unsafe {
529            let mut dur_v: i64 = 0;
530            let mut dur_s: i32 = 0;
531            let mut dur_f: u32 = 0;
532            let mut dur_e: i64 = 0;
533            let mut pts_v: i64 = 0;
534            let mut pts_s: i32 = 0;
535            let mut pts_f: u32 = 0;
536            let mut pts_e: i64 = 0;
537            let mut dts_v: i64 = 0;
538            let mut dts_s: i32 = 0;
539            let mut dts_f: u32 = 0;
540            let mut dts_e: i64 = 0;
541            let status = ffi::cm_sample_buffer_get_sample_timing_info(
542                self.as_ptr(),
543                index,
544                &raw mut dur_v,
545                &raw mut dur_s,
546                &raw mut dur_f,
547                &raw mut dur_e,
548                &raw mut pts_v,
549                &raw mut pts_s,
550                &raw mut pts_f,
551                &raw mut pts_e,
552                &raw mut dts_v,
553                &raw mut dts_s,
554                &raw mut dts_f,
555                &raw mut dts_e,
556            );
557            if status == 0 {
558                Ok(CMSampleTimingInfo {
559                    duration: CMTime {
560                        value: dur_v,
561                        timescale: dur_s,
562                        flags: dur_f,
563                        epoch: dur_e,
564                    },
565                    presentation_time_stamp: CMTime {
566                        value: pts_v,
567                        timescale: pts_s,
568                        flags: pts_f,
569                        epoch: pts_e,
570                    },
571                    decode_time_stamp: CMTime {
572                        value: dts_v,
573                        timescale: dts_s,
574                        flags: dts_f,
575                        epoch: dts_e,
576                    },
577                })
578            } else {
579                Err(status)
580            }
581        }
582    }
583
584    fn cg_image(&self) -> Result<apple_cf::cg::CGImage, i32> {
585        unsafe {
586            let mut status: i32 = 0;
587            let ptr = ffi::cm_sample_buffer_create_cg_image(self.as_ptr(), &raw mut status);
588            if !ptr.is_null() && status == 0 {
589                // Safety: the Swift bridge returns a retained CGImage on
590                // success; passing it straight to CGImage::from_raw takes
591                // ownership of that refcount.
592                Ok(apple_cf::cg::CGImage::from_raw(ptr.cast_mut()))
593            } else {
594                Err(status)
595            }
596        }
597    }
598}
599
600// ------------------------------------------------------------------
601// data_buffer wrapper that returns the *local* CMBlockBuffer type
602// (for backward compat). apple_cf::cm::CMSampleBuffer also has its own
603// data_buffer() returning apple_cf::cm::CMBlockBuffer; the local one
604// here returns crate::cm::CMBlockBuffer which currently is its own
605// type. (Merging the two is Phase 4.)
606// ------------------------------------------------------------------
607
608/// Convenience: like [`apple_cf::cm::CMSampleBuffer::data_buffer`] but
609/// returns the local `crate::cm::CMBlockBuffer` (which is currently a
610/// different wrapper around the same underlying type).
611pub trait CMSampleBufferDataBufferExt {
612    fn data_buffer_local(&self) -> Option<CMBlockBuffer>;
613}
614
615impl CMSampleBufferDataBufferExt for CMSampleBuffer {
616    fn data_buffer_local(&self) -> Option<CMBlockBuffer> {
617        unsafe {
618            let ptr = ffi::cm_sample_buffer_get_data_buffer(self.as_ptr());
619            if ptr.is_null() {
620                return None;
621            }
622            // `CMSampleBufferGetDataBuffer` returns a +0 (unretained) reference.
623            // `CMBlockBuffer::from_ptr` adopts a +1 reference and releases on
624            // drop, so we must retain first to keep the refcount balanced.
625            // (Mirrors apple-cf's own `CMSampleBuffer::data_buffer`.)
626            let retained = ffi::cm_block_buffer_retain(ptr);
627            (!retained.is_null()).then(|| CMBlockBuffer::from_ptr(retained))
628        }
629    }
630}