Skip to main content

camera_stream/platform/macos/
ext.rs

1use std::panic::AssertUnwindSafe;
2
3use objc2_av_foundation::{AVCaptureDevice, AVCaptureExposureMode, AVCaptureFocusMode};
4use objc2_core_foundation::CGPoint;
5use objc2_core_video::CVPixelBuffer;
6
7use crate::error::{Error, PlatformError};
8use crate::platform::macos::catch_objc;
9use crate::platform::macos::device::MacosCameraDevice;
10use crate::platform::macos::frame::MacosFrame;
11use crate::types::Ratio;
12
13// Re-export platform-specific enums for convenience
14pub use objc2_av_foundation::{
15    AVCaptureExposureMode as MacosExposureMode, AVCaptureFocusMode as MacosFocusMode,
16    AVCaptureTorchMode as MacosTorchMode, AVCaptureWhiteBalanceMode as MacosWhiteBalanceMode,
17};
18
19/// RAII guard for `AVCaptureDevice` configuration lock.
20pub struct ConfigLockGuard<'a> {
21    device: &'a AVCaptureDevice,
22}
23
24impl<'a> ConfigLockGuard<'a> {
25    pub fn device(&self) -> &AVCaptureDevice {
26        self.device
27    }
28}
29
30impl<'a> Drop for ConfigLockGuard<'a> {
31    fn drop(&mut self) {
32        unsafe { self.device.unlockForConfiguration() };
33    }
34}
35
36/// macOS-specific camera device controls.
37pub trait MacosCameraDeviceExt {
38    fn lock_for_configuration(&self) -> Result<ConfigLockGuard<'_>, Error>;
39
40    // Focus
41    fn focus_modes(&self) -> impl Iterator<Item = MacosFocusMode>;
42    fn set_focus_mode(&self, mode: MacosFocusMode) -> Result<(), Error>;
43    fn set_focus_point(&self, x: f64, y: f64) -> Result<(), Error>;
44
45    // Exposure
46    fn exposure_modes(&self) -> impl Iterator<Item = MacosExposureMode>;
47    fn set_exposure_mode(&self, mode: MacosExposureMode) -> Result<(), Error>;
48    fn set_exposure_point(&self, x: f64, y: f64) -> Result<(), Error>;
49    fn set_exposure_target_bias(&self, bias: f32) -> Result<(), Error>;
50
51    // White balance
52    fn set_white_balance_mode(&self, mode: MacosWhiteBalanceMode) -> Result<(), Error>;
53
54    // Torch
55    fn has_torch(&self) -> bool;
56    fn set_torch_mode(&self, mode: MacosTorchMode) -> Result<(), Error>;
57
58    // Zoom
59    fn max_zoom_factor(&self) -> f64;
60    fn set_zoom_factor(&self, factor: f64) -> Result<(), Error>;
61
62    // Active format / frame rate
63    fn set_active_video_min_frame_duration(&self, duration: Ratio) -> Result<(), Error>;
64    fn set_active_video_max_frame_duration(&self, duration: Ratio) -> Result<(), Error>;
65}
66
67impl MacosCameraDeviceExt for MacosCameraDevice {
68    fn lock_for_configuration(&self) -> Result<ConfigLockGuard<'_>, Error> {
69        unsafe { self.device.lockForConfiguration() }
70            .map_err(|e| Error::Platform(PlatformError::NsError(e)))?;
71        Ok(ConfigLockGuard {
72            device: &self.device,
73        })
74    }
75
76    fn focus_modes(&self) -> impl Iterator<Item = MacosFocusMode> {
77        let device = &self.device;
78        [
79            AVCaptureFocusMode(0), // Locked
80            AVCaptureFocusMode(1), // AutoFocus
81            AVCaptureFocusMode(2), // ContinuousAutoFocus
82        ]
83        .into_iter()
84        .filter(move |mode| unsafe { device.isFocusModeSupported(*mode) })
85    }
86
87    fn set_focus_mode(&self, mode: MacosFocusMode) -> Result<(), Error> {
88        let _guard = self.lock_for_configuration()?;
89        catch_objc(AssertUnwindSafe(|| unsafe {
90            self.device.setFocusMode(mode)
91        }))
92    }
93
94    fn set_focus_point(&self, x: f64, y: f64) -> Result<(), Error> {
95        if !unsafe { self.device.isFocusPointOfInterestSupported() } {
96            return Err(Error::Platform(PlatformError::Message(
97                "focus point of interest not supported",
98            )));
99        }
100        let _guard = self.lock_for_configuration()?;
101        catch_objc(AssertUnwindSafe(|| unsafe {
102            self.device.setFocusPointOfInterest(CGPoint { x, y });
103        }))
104    }
105
106    fn exposure_modes(&self) -> impl Iterator<Item = MacosExposureMode> {
107        let device = &self.device;
108        [
109            AVCaptureExposureMode(0), // Locked
110            AVCaptureExposureMode(1), // AutoExpose
111            AVCaptureExposureMode(2), // ContinuousAutoExposure
112            AVCaptureExposureMode(3), // Custom
113        ]
114        .into_iter()
115        .filter(move |mode| unsafe { device.isExposureModeSupported(*mode) })
116    }
117
118    fn set_exposure_mode(&self, mode: MacosExposureMode) -> Result<(), Error> {
119        let _guard = self.lock_for_configuration()?;
120        catch_objc(AssertUnwindSafe(|| unsafe {
121            self.device.setExposureMode(mode)
122        }))
123    }
124
125    fn set_exposure_point(&self, x: f64, y: f64) -> Result<(), Error> {
126        if !unsafe { self.device.isExposurePointOfInterestSupported() } {
127            return Err(Error::Platform(PlatformError::Message(
128                "exposure point of interest not supported",
129            )));
130        }
131        let _guard = self.lock_for_configuration()?;
132        catch_objc(AssertUnwindSafe(|| unsafe {
133            self.device.setExposurePointOfInterest(CGPoint { x, y });
134        }))
135    }
136
137    fn set_exposure_target_bias(&self, bias: f32) -> Result<(), Error> {
138        let _guard = self.lock_for_configuration()?;
139        catch_objc(AssertUnwindSafe(|| unsafe {
140            self.device
141                .setExposureTargetBias_completionHandler(bias, None);
142        }))
143    }
144
145    fn set_white_balance_mode(&self, mode: MacosWhiteBalanceMode) -> Result<(), Error> {
146        if !unsafe { self.device.isWhiteBalanceModeSupported(mode) } {
147            return Err(Error::Platform(PlatformError::Message(
148                "white balance mode not supported",
149            )));
150        }
151        let _guard = self.lock_for_configuration()?;
152        catch_objc(AssertUnwindSafe(|| unsafe {
153            self.device.setWhiteBalanceMode(mode)
154        }))
155    }
156
157    fn has_torch(&self) -> bool {
158        unsafe { self.device.hasTorch() }
159    }
160
161    fn set_torch_mode(&self, mode: MacosTorchMode) -> Result<(), Error> {
162        if !unsafe { self.device.isTorchModeSupported(mode) } {
163            return Err(Error::Platform(PlatformError::Message(
164                "torch mode not supported",
165            )));
166        }
167        let _guard = self.lock_for_configuration()?;
168        catch_objc(AssertUnwindSafe(|| unsafe {
169            self.device.setTorchMode(mode)
170        }))
171    }
172
173    fn max_zoom_factor(&self) -> f64 {
174        unsafe { self.device.maxAvailableVideoZoomFactor() }
175    }
176
177    fn set_zoom_factor(&self, factor: f64) -> Result<(), Error> {
178        let _guard = self.lock_for_configuration()?;
179        catch_objc(AssertUnwindSafe(|| unsafe {
180            self.device.setVideoZoomFactor(factor)
181        }))
182    }
183
184    fn set_active_video_min_frame_duration(&self, duration: Ratio) -> Result<(), Error> {
185        let _guard = self.lock_for_configuration()?;
186        let cm_time = objc2_core_media::CMTime {
187            value: duration.numerator as i64,
188            timescale: duration.denominator as i32,
189            flags: objc2_core_media::CMTimeFlags(1),
190            epoch: 0,
191        };
192        catch_objc(AssertUnwindSafe(|| unsafe {
193            self.device.setActiveVideoMinFrameDuration(cm_time);
194        }))
195    }
196
197    fn set_active_video_max_frame_duration(&self, duration: Ratio) -> Result<(), Error> {
198        let _guard = self.lock_for_configuration()?;
199        let cm_time = objc2_core_media::CMTime {
200            value: duration.numerator as i64,
201            timescale: duration.denominator as i32,
202            flags: objc2_core_media::CMTimeFlags(1),
203            epoch: 0,
204        };
205        catch_objc(AssertUnwindSafe(|| unsafe {
206            self.device.setActiveVideoMaxFrameDuration(cm_time);
207        }))
208    }
209}
210
211/// macOS-specific frame data.
212pub trait MacosFrameExt {
213    /// Access the underlying `CVPixelBuffer`.
214    ///
215    /// The buffer is valid for the lifetime of the frame (i.e. the callback
216    /// scope).  To keep it alive longer, retain it with
217    /// `CFRetained::retain(pixel_buffer)`.
218    fn pixel_buffer(&self) -> &CVPixelBuffer;
219}
220
221impl MacosFrameExt for MacosFrame<'_> {
222    fn pixel_buffer(&self) -> &CVPixelBuffer {
223        self.pixel_buffer_ref()
224    }
225}