#![expect(
unsafe_code,
reason = "AVFoundation (AVCaptureDevice) camera-enumeration FFI"
)]
use std::ffi::CStr;
use std::os::raw::c_char;
use objc2::encode::{Encoding, RefEncode};
use objc2::msg_send;
use objc2::rc::autoreleasepool;
use objc2::runtime::{AnyClass, AnyObject};
pub(crate) struct RawCamera {
pub name: String,
pub unique_id: String,
pub model_id: String,
pub max_width: u32,
pub max_height: u32,
pub max_fps: u32,
}
#[link(name = "AVFoundation", kind = "framework")]
unsafe extern "C" {
static AVMediaTypeVideo: *const AnyObject;
}
#[repr(C)]
struct CMVideoDimensions {
width: i32,
height: i32,
}
#[repr(C)]
struct CMFormatDescription {
_private: [u8; 0],
}
unsafe impl RefEncode for CMFormatDescription {
const ENCODING_REF: Encoding =
Encoding::Pointer(&Encoding::Struct("opaqueCMFormatDescription", &[]));
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMVideoFormatDescriptionGetDimensions(desc: *mut CMFormatDescription) -> CMVideoDimensions;
}
pub(crate) fn enumerate() -> Vec<RawCamera> {
let Some(device_cls) = AnyClass::get(c"AVCaptureDevice") else {
return Vec::new();
};
autoreleasepool(|_| {
unsafe {
let devices: *mut AnyObject =
msg_send![device_cls, devicesWithMediaType: AVMediaTypeVideo];
let mut out = Vec::new();
if !devices.is_null() {
let count: usize = msg_send![devices, count];
out.reserve(count);
for i in 0..count {
let device: *mut AnyObject = msg_send![devices, objectAtIndex: i];
if device.is_null() {
continue;
}
let name_obj: *mut AnyObject = msg_send![device, localizedName];
let uid_obj: *mut AnyObject = msg_send![device, uniqueID];
let model_obj: *mut AnyObject = msg_send![device, modelID];
if let (Some(name), Some(unique_id), Some(model_id)) =
(nsstring(name_obj), nsstring(uid_obj), nsstring(model_obj))
{
let (max_width, max_height, max_fps) = best_format(device);
out.push(RawCamera {
name,
unique_id,
model_id,
max_width,
max_height,
max_fps,
});
}
}
}
out
}
})
}
fn best_format(device: *mut AnyObject) -> (u32, u32, u32) {
unsafe {
let formats: *mut AnyObject = msg_send![device, formats];
if formats.is_null() {
return (0, 0, 0);
}
let count: usize = msg_send![formats, count];
let mut best = (0u32, 0u32, 0u32);
for i in 0..count {
let format: *mut AnyObject = msg_send![formats, objectAtIndex: i];
if format.is_null() {
continue;
}
let desc: *mut CMFormatDescription = msg_send![format, formatDescription];
if desc.is_null() {
continue;
}
let dims = CMVideoFormatDescriptionGetDimensions(desc);
let w = u32::try_from(dims.width).unwrap_or(0);
let h = u32::try_from(dims.height).unwrap_or(0);
let fps = max_frame_rate(format);
let area = u64::from(w) * u64::from(h);
let best_area = u64::from(best.0) * u64::from(best.1);
if area > best_area || (w == best.0 && h == best.1 && fps > best.2) {
best = (w, h, fps);
}
}
best
}
}
fn max_frame_rate(format: *mut AnyObject) -> u32 {
unsafe {
let ranges: *mut AnyObject = msg_send![format, videoSupportedFrameRateRanges];
if ranges.is_null() {
return 0;
}
let count: usize = msg_send![ranges, count];
let mut max = 0.0f64;
for i in 0..count {
let range: *mut AnyObject = msg_send![ranges, objectAtIndex: i];
if range.is_null() {
continue;
}
let r: f64 = msg_send![range, maxFrameRate];
if r > max {
max = r;
}
}
round_fps(max)
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "fps is rounded, finite, and clamped to a small non-negative range"
)]
fn round_fps(rate: f64) -> u32 {
if rate.is_finite() && rate > 0.0 {
rate.round() as u32
} else {
0
}
}
fn nsstring(s: *mut AnyObject) -> Option<String> {
if s.is_null() {
return None;
}
unsafe {
let utf8: *const c_char = msg_send![s, UTF8String];
if utf8.is_null() {
return None;
}
Some(CStr::from_ptr(utf8).to_string_lossy().into_owned())
}
}