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