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}