Skip to main content

camera_stream/platform/macos/
ext.rs

1use core::ffi::c_void;
2use std::panic::AssertUnwindSafe;
3
4use objc2_av_foundation::{AVCaptureDevice, AVCaptureExposureMode, AVCaptureFocusMode};
5use objc2_core_foundation::CGPoint;
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 { self.device.setFocusMode(mode) }))
90    }
91
92    fn set_focus_point(&self, x: f64, y: f64) -> Result<(), Error> {
93        if !unsafe { self.device.isFocusPointOfInterestSupported() } {
94            return Err(Error::Platform(PlatformError::Message(
95                "focus point of interest not supported",
96            )));
97        }
98        let _guard = self.lock_for_configuration()?;
99        catch_objc(AssertUnwindSafe(|| unsafe {
100            self.device.setFocusPointOfInterest(CGPoint { x, y });
101        }))
102    }
103
104    fn exposure_modes(&self) -> impl Iterator<Item = MacosExposureMode> {
105        let device = &self.device;
106        [
107            AVCaptureExposureMode(0), // Locked
108            AVCaptureExposureMode(1), // AutoExpose
109            AVCaptureExposureMode(2), // ContinuousAutoExposure
110            AVCaptureExposureMode(3), // Custom
111        ]
112        .into_iter()
113        .filter(move |mode| unsafe { device.isExposureModeSupported(*mode) })
114    }
115
116    fn set_exposure_mode(&self, mode: MacosExposureMode) -> Result<(), Error> {
117        let _guard = self.lock_for_configuration()?;
118        catch_objc(AssertUnwindSafe(|| unsafe { self.device.setExposureMode(mode) }))
119    }
120
121    fn set_exposure_point(&self, x: f64, y: f64) -> Result<(), Error> {
122        if !unsafe { self.device.isExposurePointOfInterestSupported() } {
123            return Err(Error::Platform(PlatformError::Message(
124                "exposure point of interest not supported",
125            )));
126        }
127        let _guard = self.lock_for_configuration()?;
128        catch_objc(AssertUnwindSafe(|| unsafe {
129            self.device.setExposurePointOfInterest(CGPoint { x, y });
130        }))
131    }
132
133    fn set_exposure_target_bias(&self, bias: f32) -> Result<(), Error> {
134        let _guard = self.lock_for_configuration()?;
135        catch_objc(AssertUnwindSafe(|| unsafe {
136            self.device
137                .setExposureTargetBias_completionHandler(bias, None);
138        }))
139    }
140
141    fn set_white_balance_mode(&self, mode: MacosWhiteBalanceMode) -> Result<(), Error> {
142        if !unsafe { self.device.isWhiteBalanceModeSupported(mode) } {
143            return Err(Error::Platform(PlatformError::Message(
144                "white balance mode not supported",
145            )));
146        }
147        let _guard = self.lock_for_configuration()?;
148        catch_objc(AssertUnwindSafe(|| unsafe { self.device.setWhiteBalanceMode(mode) }))
149    }
150
151    fn has_torch(&self) -> bool {
152        unsafe { self.device.hasTorch() }
153    }
154
155    fn set_torch_mode(&self, mode: MacosTorchMode) -> Result<(), Error> {
156        if !unsafe { self.device.isTorchModeSupported(mode) } {
157            return Err(Error::Platform(PlatformError::Message(
158                "torch mode not supported",
159            )));
160        }
161        let _guard = self.lock_for_configuration()?;
162        catch_objc(AssertUnwindSafe(|| unsafe { self.device.setTorchMode(mode) }))
163    }
164
165    fn max_zoom_factor(&self) -> f64 {
166        unsafe { self.device.maxAvailableVideoZoomFactor() }
167    }
168
169    fn set_zoom_factor(&self, factor: f64) -> Result<(), Error> {
170        let _guard = self.lock_for_configuration()?;
171        catch_objc(AssertUnwindSafe(|| unsafe { self.device.setVideoZoomFactor(factor) }))
172    }
173
174    fn set_active_video_min_frame_duration(&self, duration: Ratio) -> Result<(), Error> {
175        let _guard = self.lock_for_configuration()?;
176        let cm_time = objc2_core_media::CMTime {
177            value: duration.numerator as i64,
178            timescale: duration.denominator as i32,
179            flags: objc2_core_media::CMTimeFlags(1),
180            epoch: 0,
181        };
182        catch_objc(AssertUnwindSafe(|| unsafe {
183            self.device.setActiveVideoMinFrameDuration(cm_time);
184        }))
185    }
186
187    fn set_active_video_max_frame_duration(&self, duration: Ratio) -> Result<(), Error> {
188        let _guard = self.lock_for_configuration()?;
189        let cm_time = objc2_core_media::CMTime {
190            value: duration.numerator as i64,
191            timescale: duration.denominator as i32,
192            flags: objc2_core_media::CMTimeFlags(1),
193            epoch: 0,
194        };
195        catch_objc(AssertUnwindSafe(|| unsafe {
196            self.device.setActiveVideoMaxFrameDuration(cm_time);
197        }))
198    }
199}
200
201/// macOS-specific frame data.
202pub trait MacosFrameExt {
203    fn sample_buffer_ptr(&self) -> *const c_void;
204}
205
206impl MacosFrameExt for MacosFrame<'_> {
207    fn sample_buffer_ptr(&self) -> *const c_void {
208        self.pixel_buffer_ptr()
209    }
210}