Skip to main content

camera_stream/platform/macos/
device.rs

1use objc2::rc::Retained;
2use objc2_av_foundation::{AVCaptureDevice, AVCaptureDeviceFormat, AVMediaTypeVideo};
3use objc2_core_media::CMVideoFormatDescriptionGetDimensions;
4
5use crate::device::{CameraDevice, CameraManager};
6use crate::error::{Error, PlatformError};
7use crate::platform::macos::stream::MacosCameraStream;
8use crate::types::*;
9
10/// macOS camera manager using AVFoundation.
11#[derive(Default)]
12pub struct MacosCameraManager;
13
14impl CameraManager for MacosCameraManager {
15    type Device = MacosCameraDevice;
16    type Error = Error;
17
18    fn discover_devices(&self) -> Result<impl Iterator<Item = Self::Device>, Self::Error> {
19        let media_type = unsafe { AVMediaTypeVideo }.ok_or_else(|| {
20            Error::Platform(PlatformError::Message(
21                "AVMediaTypeVideo not available",
22            ))
23        })?;
24
25        #[allow(deprecated)]
26        let devices: Vec<_> = unsafe { AVCaptureDevice::devicesWithMediaType(media_type) }
27            .iter()
28            .map(|d| MacosCameraDevice::new(d.clone()))
29            .collect();
30
31        Ok(devices.into_iter())
32    }
33
34    fn default_device(&self) -> Result<Option<Self::Device>, Self::Error> {
35        let media_type = unsafe { AVMediaTypeVideo }.ok_or_else(|| {
36            Error::Platform(PlatformError::Message(
37                "AVMediaTypeVideo not available",
38            ))
39        })?;
40
41        let device = unsafe { AVCaptureDevice::defaultDeviceWithMediaType(media_type) };
42        Ok(device.map(MacosCameraDevice::new))
43    }
44}
45
46/// Wraps an `AVCaptureDevice`.
47pub struct MacosCameraDevice {
48    pub(crate) device: Retained<AVCaptureDevice>,
49    id_cache: String,
50    name_cache: String,
51}
52
53impl MacosCameraDevice {
54    pub(crate) fn new(device: Retained<AVCaptureDevice>) -> Self {
55        let id_cache = unsafe { device.uniqueID() }.to_string();
56        let name_cache = unsafe { device.localizedName() }.to_string();
57        MacosCameraDevice {
58            device,
59            id_cache,
60            name_cache,
61        }
62    }
63
64    /// Access the underlying `AVCaptureDevice`.
65    pub fn av_device(&self) -> &AVCaptureDevice {
66        &self.device
67    }
68}
69
70pub(crate) fn format_to_descriptors(
71    format: &AVCaptureDeviceFormat,
72) -> impl Iterator<Item = FormatDescriptor> + use<> {
73    let desc = unsafe { format.formatDescription() };
74    let media_sub_type = unsafe { desc.media_sub_type() };
75    let pixel_format = fourcc_to_pixel_format(media_sub_type);
76
77    let dims = unsafe { CMVideoFormatDescriptionGetDimensions(&desc) };
78    let size = Size {
79        width: dims.width as u32,
80        height: dims.height as u32,
81    };
82
83    let ranges = unsafe { format.videoSupportedFrameRateRanges() };
84    let frame_rate_ranges: Vec<_> = ranges
85        .iter()
86        .map(|r| {
87            let min_rate = unsafe { r.minFrameRate() };
88            let max_rate = unsafe { r.maxFrameRate() };
89            FrameRateRange {
90                min: f64_to_frame_rate(min_rate),
91                max: f64_to_frame_rate(max_rate),
92            }
93        })
94        .collect();
95
96    let descriptors: Vec<_> = pixel_format
97        .into_iter()
98        .flat_map(move |pf| FormatDescriptor::from_ranges(pf, size, frame_rate_ranges.clone()))
99        .collect();
100
101    descriptors.into_iter()
102}
103
104pub(crate) fn fourcc_to_pixel_format(fourcc: u32) -> Option<PixelFormat> {
105    // kCVPixelFormatType values
106    #[allow(clippy::mistyped_literal_suffixes)]
107    match fourcc {
108        0x34_32_30_76 => Some(PixelFormat::Nv12),   // '420v'
109        0x34_32_30_66 => Some(PixelFormat::Nv12),   // '420f'
110        0x79_75_76_32 => Some(PixelFormat::Yuyv),   // 'yuvs' / 'yuv2'
111        0x32_76_75_79 => Some(PixelFormat::Uyvy),   // '2vuy'
112        0x42_47_52_41 => Some(PixelFormat::Bgra32), // 'BGRA'
113        0x6A_70_65_67 => Some(PixelFormat::Jpeg),   // 'jpeg'
114        _ => None,
115    }
116}
117
118pub(crate) fn pixel_format_to_fourcc(pf: &PixelFormat) -> u32 {
119    #[allow(clippy::mistyped_literal_suffixes)]
120    match pf {
121        PixelFormat::Nv12 => 0x34_32_30_76,   // '420v'
122        PixelFormat::Yuyv => 0x79_75_76_32,   // 'yuvs'
123        PixelFormat::Uyvy => 0x32_76_75_79,   // '2vuy'
124        PixelFormat::Bgra32 => 0x42_47_52_41, // 'BGRA'
125        PixelFormat::Jpeg => 0x6A_70_65_67,   // 'jpeg'
126    }
127}
128
129fn f64_to_frame_rate(fps: f64) -> FrameRate {
130    // Express as integer ratio: fps ≈ numerator/1
131    // For common rates, use 1000-based denominator for precision.
132    let denominator = 1000u32;
133    let numerator = (fps * denominator as f64).round() as u32;
134    FrameRate {
135        numerator,
136        denominator,
137    }
138}
139
140impl CameraDevice for MacosCameraDevice {
141    type Stream = MacosCameraStream;
142    type Error = Error;
143
144    fn id(&self) -> &str {
145        &self.id_cache
146    }
147
148    fn name(&self) -> &str {
149        &self.name_cache
150    }
151
152    fn supported_formats(&self) -> Result<impl Iterator<Item = FormatDescriptor>, Self::Error> {
153        let formats: Vec<_> = unsafe { self.device.formats() }
154            .iter()
155            .flat_map(|f| format_to_descriptors(&f))
156            .collect();
157        Ok(formats.into_iter())
158    }
159
160    fn open(self, config: &StreamConfig) -> Result<Self::Stream, Self::Error> {
161        MacosCameraStream::new(self.device, config)
162    }
163}