Skip to main content

camera_stream/platform/macos/
device.rs

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