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