#![expect(
unsafe_code,
reason = "AVFoundation / CoreMedia / CoreVideo capture FFI"
)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_possible_wrap,
reason = "pixel dimensions and FourCC constants are bounded and copied verbatim"
)]
use std::ffi::{CString, c_void};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
use block2::RcBlock;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Bool, NSObject};
use objc2::{AnyThread, class, define_class, msg_send};
pub use crate::capture_types::{CaptureError, Frame};
const PIXEL_FORMAT_32BGRA: u32 = 0x4247_5241;
const LOCK_READ_ONLY: u64 = 1;
static LATEST: OnceLock<Mutex<Option<Arc<Frame>>>> = OnceLock::new();
fn latest() -> &'static Mutex<Option<Arc<Frame>>> {
LATEST.get_or_init(|| Mutex::new(None))
}
static FRAME_GEN: AtomicU64 = AtomicU64::new(0);
static PREVIEW_TARGET_W: AtomicU32 = AtomicU32::new(0);
#[link(name = "AVFoundation", kind = "framework")]
unsafe extern "C" {
static AVMediaTypeVideo: *const AnyObject;
static AVCaptureSessionPreset1280x720: *const AnyObject;
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMSampleBufferGetImageBuffer(sbuf: *mut AnyObject) -> *mut AnyObject;
}
#[link(name = "CoreVideo", kind = "framework")]
unsafe extern "C" {
static kCVPixelBufferPixelFormatTypeKey: *const AnyObject;
fn CVPixelBufferLockBaseAddress(pb: *mut AnyObject, flags: u64) -> i32;
fn CVPixelBufferUnlockBaseAddress(pb: *mut AnyObject, flags: u64) -> i32;
fn CVPixelBufferGetBaseAddress(pb: *mut AnyObject) -> *mut c_void;
fn CVPixelBufferGetBytesPerRow(pb: *mut AnyObject) -> usize;
fn CVPixelBufferGetWidth(pb: *mut AnyObject) -> usize;
fn CVPixelBufferGetHeight(pb: *mut AnyObject) -> usize;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
static kCFRunLoopDefaultMode: *const c_void;
fn CFRunLoopRunInMode(
mode: *const c_void,
seconds: f64,
return_after_source_handled: u8,
) -> i32;
}
unsafe extern "C" {
fn dispatch_queue_create(label: *const i8, attr: *const c_void) -> *mut AnyObject;
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "OLCameraFrameDelegate"]
struct FrameDelegate;
impl FrameDelegate {
#[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))]
fn did_output(
&self,
_output: *mut AnyObject,
sbuf: *mut AnyObject,
_conn: *mut AnyObject,
) {
unsafe {
let pb = CMSampleBufferGetImageBuffer(sbuf);
if pb.is_null() || CVPixelBufferLockBaseAddress(pb, LOCK_READ_ONLY) != 0 {
return;
}
let base = CVPixelBufferGetBaseAddress(pb).cast::<u8>();
let bytes_per_row = CVPixelBufferGetBytesPerRow(pb);
let width = CVPixelBufferGetWidth(pb);
let height = CVPixelBufferGetHeight(pb);
let target = PREVIEW_TARGET_W.load(Ordering::Relaxed) as usize;
let step = if target > 0 && width > target {
width.div_ceil(target)
} else {
1
};
let out_w = width / step;
let out_h = height / step;
if !base.is_null() && out_w > 0 && out_h > 0 {
let mut bgra = vec![0u8; out_w * out_h * 4];
let dst = bgra.as_mut_ptr();
if step == 1 {
for oy in 0..out_h {
let row = base.add(oy * bytes_per_row);
std::ptr::copy_nonoverlapping(row, dst.add(oy * out_w * 4), out_w * 4);
}
} else {
for oy in 0..out_h {
let row = base.add(oy * step * bytes_per_row);
for ox in 0..out_w {
let src = row.add(ox * step * 4);
let out = (oy * out_w + ox) * 4;
std::ptr::copy_nonoverlapping(src, dst.add(out), 4);
}
}
}
if let Ok(mut slot) = latest().lock() {
*slot = Some(Arc::new(Frame {
width: out_w as u32,
height: out_h as u32,
bgra,
}));
FRAME_GEN.fetch_add(1, Ordering::Relaxed);
}
}
CVPixelBufferUnlockBaseAddress(pb, LOCK_READ_ONLY);
}
}
}
);
impl FrameDelegate {
fn new() -> Retained<Self> {
let this = Self::alloc().set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
fn authorization() -> Option<bool> {
let cls = class!(AVCaptureDevice);
let status: isize =
unsafe { msg_send![cls, authorizationStatusForMediaType: AVMediaTypeVideo] };
match status {
3 => Some(true),
1 | 2 => Some(false),
_ => None,
}
}
fn request_access(timeout: Duration) -> bool {
let answered = std::sync::Arc::new(Mutex::new(None::<bool>));
let sink = answered.clone();
let handler = RcBlock::new(move |granted: Bool| {
if let Ok(mut slot) = sink.lock() {
*slot = Some(granted.as_bool());
}
});
let cls = class!(AVCaptureDevice);
unsafe {
let _: () = msg_send![
cls,
requestAccessForMediaType: AVMediaTypeVideo,
completionHandler: &*handler
];
}
let deadline = Instant::now() + timeout;
loop {
if let Ok(slot) = answered.lock()
&& let Some(granted) = *slot
{
return granted;
}
if Instant::now() >= deadline {
return false;
}
run_loop_tick(0.05);
}
}
fn ensure_access() -> Result<(), CaptureError> {
match authorization() {
Some(true) => Ok(()),
None if request_access(Duration::from_secs(30)) => Ok(()),
_ => Err(CaptureError::AccessDenied),
}
}
#[must_use]
pub fn camera_access_granted() -> bool {
matches!(authorization(), Some(true))
}
pub fn request_camera_access() {
let handler = RcBlock::new(move |_granted: Bool| {});
let cls = class!(AVCaptureDevice);
unsafe {
let _: () = msg_send![
cls,
requestAccessForMediaType: AVMediaTypeVideo,
completionHandler: &*handler
];
}
}
#[must_use]
pub fn camera_authorization() -> crate::CameraAuthorization {
match authorization() {
Some(true) => crate::CameraAuthorization::Granted,
Some(false) => crate::CameraAuthorization::Denied,
None => crate::CameraAuthorization::Undetermined,
}
}
fn run_loop_tick(seconds: f64) {
unsafe {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, 0);
}
}
fn device_with_unique_id(unique_id: &str) -> Option<Retained<AnyObject>> {
let cls = class!(AVCaptureDevice);
let Ok(ns) = CString::new(unique_id) else {
return None;
};
unsafe {
let nsstr: *mut AnyObject = msg_send![class!(NSString), stringWithUTF8String: ns.as_ptr()];
let device: *mut AnyObject = msg_send![cls, deviceWithUniqueID: nsstr];
Retained::retain(device)
}
}
struct Session {
handle: Retained<AnyObject>,
_output: Retained<AnyObject>,
_delegate: Retained<FrameDelegate>,
}
impl Drop for Session {
fn drop(&mut self) {
unsafe {
let _: () = msg_send![&*self.handle, stopRunning];
}
}
}
fn open_session(unique_id: &str, low_res: bool) -> Result<Session, CaptureError> {
ensure_access()?;
let device = device_with_unique_id(unique_id).ok_or(CaptureError::NotFound)?;
if let Ok(mut slot) = latest().lock() {
*slot = None;
}
PREVIEW_TARGET_W.store(if low_res { 1280 } else { 0 }, Ordering::Relaxed);
unsafe {
let session: *mut AnyObject = msg_send![class!(AVCaptureSession), new];
let Some(session) = Retained::from_raw(session) else {
return Err(CaptureError::Setup("AVCaptureSession".into()));
};
let mut err: *mut AnyObject = std::ptr::null_mut();
let input: *mut AnyObject = msg_send![
class!(AVCaptureDeviceInput),
deviceInputWithDevice: &*device,
error: &mut err
];
if input.is_null() {
return Err(CaptureError::Setup("AVCaptureDeviceInput".into()));
}
let can_in: bool = msg_send![&*session, canAddInput: input];
if !can_in {
return Err(CaptureError::Setup("session rejected input".into()));
}
let _: () = msg_send![&*session, addInput: input];
if low_res {
let can: bool =
msg_send![&*session, canSetSessionPreset: AVCaptureSessionPreset1280x720];
if can {
let _: () = msg_send![&*session, setSessionPreset: AVCaptureSessionPreset1280x720];
}
}
let output: *mut AnyObject = msg_send![class!(AVCaptureVideoDataOutput), new];
let Some(output) = Retained::from_raw(output) else {
return Err(CaptureError::Setup("AVCaptureVideoDataOutput".into()));
};
let num: *mut AnyObject =
msg_send![class!(NSNumber), numberWithUnsignedInt: PIXEL_FORMAT_32BGRA];
let settings: *mut AnyObject = msg_send![
class!(NSDictionary),
dictionaryWithObject: num,
forKey: kCVPixelBufferPixelFormatTypeKey
];
let _: () = msg_send![&*output, setVideoSettings: settings];
let _: () = msg_send![&*output, setAlwaysDiscardsLateVideoFrames: true];
let delegate = FrameDelegate::new();
let queue = dispatch_queue_create(c"org.openlogi.camera".as_ptr(), std::ptr::null());
let _: () = msg_send![&*output, setSampleBufferDelegate: &*delegate, queue: queue];
let can_out: bool = msg_send![&*session, canAddOutput: &*output];
if !can_out {
return Err(CaptureError::Setup("session rejected output".into()));
}
let _: () = msg_send![&*session, addOutput: &*output];
if low_res {
let conn: *mut AnyObject =
msg_send![&*output, connectionWithMediaType: AVMediaTypeVideo];
if !conn.is_null() {
let supported: bool = msg_send![conn, isVideoMirroringSupported];
if supported {
let _: () = msg_send![conn, setAutomaticallyAdjustsVideoMirroring: false];
let _: () = msg_send![conn, setVideoMirrored: true];
}
}
}
let _: () = msg_send![&*session, startRunning];
Ok(Session {
handle: session,
_output: output,
_delegate: delegate,
})
}
}
pub fn capture_frame(unique_id: &str, timeout: Duration) -> Result<Frame, CaptureError> {
let _session = open_session(unique_id, false)?;
let deadline = Instant::now() + timeout;
loop {
if let Ok(mut slot) = latest().lock()
&& let Some(frame) = slot.take()
{
return Ok(Arc::unwrap_or_clone(frame));
}
if Instant::now() >= deadline {
return Err(CaptureError::Timeout);
}
run_loop_tick(0.03);
}
}
pub struct CameraStream {
_session: Session,
}
impl CameraStream {
#[must_use]
pub fn latest_frame(&self) -> Option<Arc<Frame>> {
latest().lock().ok().and_then(|slot| slot.clone())
}
#[must_use]
pub fn take_frame(&self) -> Option<Arc<Frame>> {
latest().lock().ok().and_then(|mut slot| slot.take())
}
#[must_use]
pub fn frame_generation(&self) -> u64 {
FRAME_GEN.load(Ordering::Relaxed)
}
}
pub fn start_stream(unique_id: &str) -> Result<CameraStream, CaptureError> {
Ok(CameraStream {
_session: open_session(unique_id, true)?,
})
}