Skip to main content

camera_stream/platform/macos/
stream.rs

1use std::panic::AssertUnwindSafe;
2use std::sync::{Arc, Mutex};
3
4use objc2::rc::Retained;
5use objc2::runtime::AnyObject;
6use objc2::runtime::ProtocolObject;
7use objc2::{AllocAnyThread, DefinedClass, define_class, msg_send};
8use objc2_av_foundation::{
9    AVCaptureConnection, AVCaptureDevice, AVCaptureDeviceFormat, AVCaptureDeviceInput,
10    AVCaptureOutput, AVCaptureSession, AVCaptureVideoDataOutput,
11    AVCaptureVideoDataOutputSampleBufferDelegate,
12};
13use objc2_core_media::CMSampleBuffer;
14use objc2_core_video::{
15    CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
16    kCVPixelBufferPixelFormatTypeKey,
17};
18use objc2_foundation::{NSDictionary, NSNumber, NSObjectProtocol, NSString};
19
20use crate::error::{Error, PlatformError};
21use crate::platform::macos::catch_objc;
22use crate::platform::macos::device::pixel_format_to_fourcc;
23use crate::platform::macos::frame::{MacosFrame, MacosTimestamp};
24use crate::stream::CameraStream;
25use crate::types::StreamConfig;
26
27type FrameCallback = Box<dyn FnMut(&MacosFrame<'_>) + Send + 'static>;
28
29struct DelegateIvars {
30    callback: Arc<Mutex<Option<FrameCallback>>>,
31}
32
33define_class!(
34    #[unsafe(super(objc2_foundation::NSObject))]
35    #[ivars = DelegateIvars]
36    #[name = "CameraStreamSampleBufferDelegate"]
37    struct SampleBufferDelegate;
38
39    impl SampleBufferDelegate {
40    }
41
42    unsafe impl NSObjectProtocol for SampleBufferDelegate {}
43
44    unsafe impl AVCaptureVideoDataOutputSampleBufferDelegate for SampleBufferDelegate {
45        #[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))]
46        #[allow(non_snake_case)]
47        unsafe fn captureOutput_didOutputSampleBuffer_fromConnection(
48            &self,
49            _output: &AVCaptureOutput,
50            sample_buffer: &CMSampleBuffer,
51            _connection: &AVCaptureConnection,
52        ) {
53            // Get the pixel buffer from the sample buffer
54            let pixel_buffer = match unsafe { sample_buffer.image_buffer() } {
55                Some(pb) => pb,
56                None => return,
57            };
58
59            // Get timestamp
60            let cm_time = unsafe { sample_buffer.presentation_time_stamp() };
61            let timestamp = MacosTimestamp {
62                value: cm_time.value,
63                timescale: cm_time.timescale,
64                flags: cm_time.flags.0,
65                epoch: cm_time.epoch,
66            };
67
68            // Lock, build frame, call callback, unlock
69            let lock_flags = CVPixelBufferLockFlags::ReadOnly;
70            unsafe {
71                CVPixelBufferLockBaseAddress(&pixel_buffer, lock_flags);
72            }
73
74            let frame = unsafe { MacosFrame::from_locked_pixel_buffer(&pixel_buffer, timestamp) };
75
76            if let Ok(mut guard) = self.ivars().callback.lock()
77                && let Some(ref mut cb) = *guard {
78                    cb(&frame);
79                }
80
81            unsafe {
82                CVPixelBufferUnlockBaseAddress(&pixel_buffer, lock_flags);
83            }
84        }
85    }
86);
87
88impl SampleBufferDelegate {
89    fn new(callback: FrameCallback) -> Retained<Self> {
90        let ivars = DelegateIvars {
91            callback: Arc::new(Mutex::new(Some(callback))),
92        };
93        let obj = Self::alloc().set_ivars(ivars);
94        unsafe { msg_send![super(obj), init] }
95    }
96}
97
98/// macOS camera stream backed by `AVCaptureSession`.
99pub struct MacosCameraStream {
100    session: Retained<AVCaptureSession>,
101    device: Retained<AVCaptureDevice>,
102    output: Retained<AVCaptureVideoDataOutput>,
103    delegate: Option<Retained<SampleBufferDelegate>>,
104    /// True while the device config lock is held (between open and start).
105    config_locked: bool,
106    running: bool,
107}
108
109impl MacosCameraStream {
110    pub(crate) fn new(
111        device: Retained<AVCaptureDevice>,
112        config: &StreamConfig,
113    ) -> Result<Self, Error> {
114        let session = unsafe { AVCaptureSession::new() };
115
116        // Create device input
117        let input = unsafe { AVCaptureDeviceInput::deviceInputWithDevice_error(&device) }
118            .map_err(|e| Error::Platform(PlatformError::NsError(e)))?;
119
120        // Create video data output
121        let output = unsafe { AVCaptureVideoDataOutput::new() };
122
123        // Tell the output to deliver frames in the requested pixel format
124        // rather than its own default (which is typically UYVY).
125        let target_fourcc = pixel_format_to_fourcc(&config.pixel_format);
126        unsafe {
127            let key: &NSString = std::mem::transmute::<&objc2_core_foundation::CFString, &NSString>(
128                kCVPixelBufferPixelFormatTypeKey,
129            );
130            let value = NSNumber::new_u32(target_fourcc);
131            let settings: Retained<NSDictionary<NSString, AnyObject>> =
132                NSDictionary::dictionaryWithObject_forKey(&value, ProtocolObject::from_ref(key));
133            output.setVideoSettings(Some(&settings));
134        }
135
136        // Find matching format before configuring the session
137        let formats = unsafe { device.formats() };
138        let mut matched_format: Option<Retained<AVCaptureDeviceFormat>> = None;
139
140        for format in formats.iter() {
141            let desc = unsafe { format.formatDescription() };
142            let sub_type = unsafe { desc.media_sub_type() };
143            let dims = unsafe { objc2_core_media::CMVideoFormatDescriptionGetDimensions(&desc) };
144
145            if sub_type == target_fourcc
146                && dims.width as u32 == config.size.width
147                && dims.height as u32 == config.size.height
148            {
149                matched_format = Some(format.clone());
150                break;
151            }
152        }
153
154        let matched = matched_format.ok_or(Error::UnsupportedFormat)?;
155
156        let frame_duration = objc2_core_media::CMTime {
157            value: config.frame_rate.denominator as i64,
158            timescale: config.frame_rate.numerator as i32,
159            flags: objc2_core_media::CMTimeFlags(1), // kCMTimeFlags_Valid
160            epoch: 0,
161        };
162
163        catch_objc(AssertUnwindSafe(|| unsafe {
164            session.beginConfiguration();
165
166            // Add input
167            if !session.canAddInput(&input) {
168                session.commitConfiguration();
169                return Err(Error::Platform(PlatformError::Message(
170                    "cannot add input to session",
171                )));
172            }
173            session.addInput(&input);
174
175            // Add output
176            if !session.canAddOutput(&output) {
177                session.commitConfiguration();
178                return Err(Error::Platform(PlatformError::Message(
179                    "cannot add output to session",
180                )));
181            }
182            session.addOutput(&output);
183
184            session.commitConfiguration();
185            Ok::<(), Error>(())
186        }))??;
187
188        // Lock the device for configuration and set the active format.
189        // The lock is intentionally held across startRunning() — if we
190        // unlock before startRunning the session's preset overrides
191        // our format choice.
192        unsafe { device.lockForConfiguration() }
193            .map_err(|e| Error::Platform(PlatformError::NsError(e)))?;
194
195        catch_objc(AssertUnwindSafe(|| unsafe {
196            device.setActiveFormat(&matched);
197        }))?;
198
199        // Frame-duration control is not supported on all devices (e.g.
200        // Continuity Camera), so treat failures as non-fatal.
201        let _ = catch_objc(AssertUnwindSafe(|| unsafe {
202            device.setActiveVideoMinFrameDuration(frame_duration);
203        }));
204        let _ = catch_objc(AssertUnwindSafe(|| unsafe {
205            device.setActiveVideoMaxFrameDuration(frame_duration);
206        }));
207
208        Ok(MacosCameraStream {
209            session,
210            device,
211            output,
212            delegate: None,
213            config_locked: true,
214            running: false,
215        })
216    }
217}
218
219impl CameraStream for MacosCameraStream {
220    type Frame<'a> = MacosFrame<'a>;
221    type Error = Error;
222
223    fn start<F>(&mut self, callback: F) -> Result<(), Self::Error>
224    where
225        F: FnMut(&Self::Frame<'_>) + Send + 'static,
226    {
227        if self.running {
228            return Err(Error::AlreadyStarted);
229        }
230
231        let delegate = SampleBufferDelegate::new(Box::new(callback));
232
233        let queue = dispatch2::DispatchQueue::new(
234            "camera-stream.callback",
235            dispatch2::DispatchQueueAttr::SERIAL,
236        );
237
238        unsafe {
239            self.output.setSampleBufferDelegate_queue(
240                Some(ProtocolObject::from_ref(&*delegate)),
241                Some(&queue),
242            );
243        }
244
245        self.delegate = Some(delegate);
246
247        catch_objc(AssertUnwindSafe(|| unsafe { self.session.startRunning() }))?;
248        self.running = true;
249
250        // Now that the session is running with our format, release the
251        // device config lock.
252        if self.config_locked {
253            unsafe { self.device.unlockForConfiguration() };
254            self.config_locked = false;
255        }
256
257        Ok(())
258    }
259
260    fn stop(&mut self) -> Result<(), Self::Error> {
261        if !self.running {
262            return Err(Error::NotStarted);
263        }
264
265        unsafe { self.session.stopRunning() };
266
267        unsafe {
268            self.output.setSampleBufferDelegate_queue(None, None);
269        }
270
271        // Clear the callback
272        if let Some(ref delegate) = self.delegate
273            && let Ok(mut guard) = delegate.ivars().callback.lock()
274        {
275            *guard = None;
276        }
277        self.delegate = None;
278        self.running = false;
279
280        Ok(())
281    }
282}
283
284impl Drop for MacosCameraStream {
285    fn drop(&mut self) {
286        if self.running {
287            let _ = self.stop();
288        }
289        if self.config_locked {
290            unsafe { self.device.unlockForConfiguration() };
291            self.config_locked = false;
292        }
293    }
294}