cameras 0.1.3

A cross-platform camera library for Rust, built with data-oriented design. Explicit format negotiation, push-based frame delivery, typed errors, and zero trait objects.
use crate::error::Error;
use crate::macos::permission::ensure_authorized;
use crate::types::{
    Capabilities, Device, DeviceId, FormatDescriptor, FramerateRange, PixelFormat, Position,
    Resolution, Transport,
};
use objc2::rc::Retained;
use objc2_av_foundation::{
    AVCaptureDevice, AVCaptureDeviceDiscoverySession, AVCaptureDevicePosition, AVCaptureDeviceType,
    AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeExternal, AVMediaTypeVideo,
};
use objc2_core_media::CMVideoFormatDescriptionGetDimensions;
use objc2_foundation::{NSArray, NSString};

pub fn devices() -> Result<Vec<Device>, Error> {
    ensure_authorized()?;

    let session = discovery_session();
    let ns_devices = unsafe { session.devices() };

    let mut result = Vec::with_capacity(ns_devices.count());
    for index in 0..ns_devices.count() {
        let device = ns_devices.objectAtIndex(index);
        result.push(device_to_public(&device));
    }
    Ok(result)
}

pub fn probe(id: &DeviceId) -> Result<Capabilities, Error> {
    ensure_authorized()?;

    let device = find_device(id)?;
    let formats = unsafe { device.formats() };
    let mut descriptors = Vec::new();

    for index in 0..formats.count() {
        let format = formats.objectAtIndex(index);
        let description = unsafe { format.formatDescription() };
        let dimensions = unsafe { CMVideoFormatDescriptionGetDimensions(&description) };
        let subtype = unsafe { description.media_sub_type() };
        let pixel_format = fourcc_to_pixel_format(subtype);
        let resolution = Resolution {
            width: dimensions.width as u32,
            height: dimensions.height as u32,
        };
        let ranges = unsafe { format.videoSupportedFrameRateRanges() };
        for range_index in 0..ranges.count() {
            let range = ranges.objectAtIndex(range_index);
            descriptors.push(FormatDescriptor {
                resolution,
                framerate_range: FramerateRange {
                    min: unsafe { range.minFrameRate() },
                    max: unsafe { range.maxFrameRate() },
                },
                pixel_format,
            });
        }
    }

    Ok(Capabilities {
        formats: descriptors,
    })
}

fn fourcc_to_pixel_format(code: u32) -> PixelFormat {
    let bytes = code.to_be_bytes();
    match &bytes {
        b"yuvs" | b"yuv2" => PixelFormat::Yuyv,
        b"420f" | b"420v" => PixelFormat::Nv12,
        b"BGRA" => PixelFormat::Bgra8,
        b"RGBA" => PixelFormat::Rgba8,
        b"jpeg" | b"dmb1" => PixelFormat::Mjpeg,
        _ => PixelFormat::Bgra8,
    }
}

pub(super) fn find_device(id: &DeviceId) -> Result<Retained<AVCaptureDevice>, Error> {
    let ns_id = NSString::from_str(&id.0);
    unsafe { AVCaptureDevice::deviceWithUniqueID(&ns_id) }
        .ok_or_else(|| Error::DeviceNotFound(id.0.clone()))
}

pub(super) fn discovery_session() -> Retained<AVCaptureDeviceDiscoverySession> {
    let device_type_refs: [&AVCaptureDeviceType; 2] = unsafe {
        [
            AVCaptureDeviceTypeBuiltInWideAngleCamera,
            AVCaptureDeviceTypeExternal,
        ]
    };
    let device_types = NSArray::from_slice(&device_type_refs);
    let media_type = unsafe { AVMediaTypeVideo };
    unsafe {
        AVCaptureDeviceDiscoverySession::discoverySessionWithDeviceTypes_mediaType_position(
            &device_types,
            media_type,
            AVCaptureDevicePosition::Unspecified,
        )
    }
}

fn device_to_public(device: &AVCaptureDevice) -> Device {
    let unique_id = unsafe { device.uniqueID() };
    let localized = unsafe { device.localizedName() }.to_string();
    let manufacturer = unsafe { device.manufacturer() }.to_string();
    let model = unsafe { device.modelID() }.to_string();
    let name = compose_device_name(&manufacturer, &model, &localized);
    let raw_position = unsafe { device.position() };
    let position = if raw_position == AVCaptureDevicePosition::Front {
        Position::Front
    } else if raw_position == AVCaptureDevicePosition::Back {
        Position::Back
    } else {
        Position::External
    };
    let transport = transport_from_code(unsafe { device.transportType() });
    Device {
        id: DeviceId(unique_id.to_string()),
        name,
        position,
        transport,
    }
}

fn compose_device_name(manufacturer: &str, model: &str, localized: &str) -> String {
    let manufacturer = normalize_vendor_fragment(manufacturer);
    let model = normalize_model_fragment(model);
    let localized_lower = localized.to_lowercase();

    let mut parts: Vec<&str> = Vec::new();
    if !manufacturer.is_empty()
        && !fragment_already_present(&manufacturer, &localized_lower)
        && !fragment_already_present(&manufacturer, &model.to_lowercase())
    {
        parts.push(manufacturer.as_str());
    }
    if !model.is_empty() && !fragment_already_present(&model, &localized_lower) {
        parts.push(model.as_str());
    }
    parts.push(localized);

    let assembled = parts.join(" ");
    if assembled.trim().is_empty() {
        localized.to_string()
    } else {
        assembled
    }
}

fn normalize_vendor_fragment(value: &str) -> String {
    value.trim().trim_end_matches(['.', ',']).trim().to_string()
}

fn normalize_model_fragment(value: &str) -> String {
    let trimmed = value.trim();
    if trimmed.is_empty() || trimmed.contains("VendorID") || trimmed.contains("ProductID") {
        return String::new();
    }
    trimmed.to_string()
}

fn fragment_already_present(fragment: &str, haystack_lower: &str) -> bool {
    let first_token = fragment.to_lowercase();
    let first_token = first_token.split_whitespace().next().unwrap_or("");
    !first_token.is_empty() && haystack_lower.contains(first_token)
}

fn transport_from_code(code: i32) -> Transport {
    let bytes = code.to_be_bytes();
    match &bytes {
        b"usb " => Transport::Usb,
        b"bltn" => Transport::BuiltIn,
        b"virt" => Transport::Virtual,
        _ => Transport::Other,
    }
}