use std::sync::{Arc, Mutex};
use std::time::Duration;
use block2::RcBlock;
use dispatch2::{DispatchQueue, DispatchRetained};
use objc2::rc::Retained;
use objc2::runtime::{Bool, ProtocolObject};
use objc2::{AnyThread, DefinedClass, define_class, msg_send};
use objc2_av_foundation::{
AVAuthorizationStatus, AVCaptureConnection, AVCaptureDevice, AVCaptureDeviceInput, AVCaptureOutput,
AVCaptureSession, AVCaptureVideoDataOutput, AVCaptureVideoDataOutputSampleBufferDelegate, AVMediaType,
AVMediaTypeVideo,
};
use objc2_core_media::CMSampleBuffer;
use objc2_foundation::{NSObject, NSObjectProtocol, NSString};
use super::surface::surface_frame;
use super::{Camera, Config, FrameChannel, FrameStream};
use crate::Error;
const FIRST_FRAME_TIMEOUT: Duration = Duration::from_secs(5);
const ACCESS_TIMEOUT: Duration = Duration::from_secs(60);
pub(super) fn cameras() -> Result<Vec<Camera>, Error> {
let media = unsafe { AVMediaTypeVideo }.ok_or_else(|| Error::Codec(anyhow::anyhow!("AVMediaTypeVideo")))?;
#[allow(deprecated)]
let devices = unsafe { AVCaptureDevice::devicesWithMediaType(media) };
Ok((0..devices.count())
.map(|index| devices.objectAtIndex(index))
.map(|device| Camera {
id: unsafe { device.uniqueID() }.to_string(),
name: unsafe { device.localizedName() }.to_string(),
})
.collect())
}
pub(super) async fn open(config: &Config, device: Option<&str>) -> Result<FrameStream, Error> {
let media = unsafe { AVMediaTypeVideo }.ok_or_else(|| Error::Codec(anyhow::anyhow!("AVMediaTypeVideo")))?;
ensure_camera_access(media).await?;
let device = match device {
Some(id) => {
let id = NSString::from_str(id);
unsafe { AVCaptureDevice::deviceWithUniqueID(&id) }
.ok_or_else(|| Error::Codec(anyhow::anyhow!("no camera with id {id}")))?
}
None => unsafe { AVCaptureDevice::defaultDeviceWithMediaType(media) }
.ok_or_else(|| Error::Codec(anyhow::anyhow!("no default camera")))?,
};
if config.width.is_some() || config.height.is_some() || config.framerate.is_some() {
tracing::warn!("width/height/framerate are ignored for camera capture on macOS; using the device default");
}
let device_id = unsafe { device.uniqueID() }.to_string();
let input = unsafe { AVCaptureDeviceInput::deviceInputWithDevice_error(&device) }
.map_err(|e| Error::Codec(anyhow::anyhow!("camera input: {e:?}")))?;
let chan = FrameChannel::new();
let delegate = Delegate::new(chan.clone());
let dispatch = DispatchQueue::new("dev.moq.video.capture", None);
let output = unsafe { AVCaptureVideoDataOutput::new() };
unsafe {
output.setAlwaysDiscardsLateVideoFrames(true);
let proto = ProtocolObject::from_ref(&*delegate);
output.setSampleBufferDelegate_queue(Some(proto), Some(&dispatch));
}
let session = unsafe { AVCaptureSession::new() };
unsafe {
session.beginConfiguration();
if !session.canAddInput(&input) {
return Err(Error::Codec(anyhow::anyhow!("cannot add camera input")));
}
session.addInput(&input);
if !session.canAddOutput(&output) {
return Err(Error::Codec(anyhow::anyhow!("cannot add video output")));
}
session.addOutput(&output);
session.commitConfiguration();
session.startRunning();
}
let guard = SessionGuard {
session,
chan: chan.clone(),
_delegate: delegate,
_dispatch: dispatch,
};
let first = match tokio::time::timeout(FIRST_FRAME_TIMEOUT, chan.recv()).await {
Ok(Some(frame)) => frame,
Ok(None) | Err(_) => {
return Err(Error::Codec(anyhow::anyhow!(
"no frames from camera {device_id} within {FIRST_FRAME_TIMEOUT:?} (permission denied?)"
)));
}
};
let (width, height) = (first.width(), first.height());
tracing::info!(device = %device_id, width, height, "opened camera (AVFoundation)");
Ok(FrameStream::new(
chan,
width,
height,
None,
device_id,
Some(first),
Box::new(guard),
))
}
async fn ensure_camera_access(media: &AVMediaType) -> Result<(), Error> {
let status = unsafe { AVCaptureDevice::authorizationStatusForMediaType(media) };
if status == AVAuthorizationStatus::Authorized {
return Ok(());
}
if status == AVAuthorizationStatus::NotDetermined {
let (tx, rx) = tokio::sync::oneshot::channel();
let tx = Mutex::new(Some(tx));
let handler = RcBlock::new(move |granted: Bool| {
if let Some(tx) = tx.lock().unwrap().take() {
let _ = tx.send(granted.as_bool());
}
});
unsafe { AVCaptureDevice::requestAccessForMediaType_completionHandler(media, &handler) };
return match tokio::time::timeout(ACCESS_TIMEOUT, rx).await {
Ok(Ok(true)) => Ok(()),
Ok(Ok(false)) => Err(Error::Codec(anyhow::anyhow!(
"camera access denied; enable it in System Settings > Privacy & Security > Camera"
))),
Ok(Err(_)) => Err(Error::Codec(anyhow::anyhow!(
"camera-permission prompt dismissed without a decision"
))),
Err(_) => Err(Error::Codec(anyhow::anyhow!(
"timed out after {ACCESS_TIMEOUT:?} waiting for the camera-permission prompt"
))),
};
}
Err(Error::Codec(anyhow::anyhow!(
"camera access not authorized (denied or restricted); enable it in System Settings > Privacy & Security > Camera"
)))
}
struct SessionGuard {
session: Retained<AVCaptureSession>,
chan: Arc<FrameChannel>,
_delegate: Retained<Delegate>,
_dispatch: DispatchRetained<DispatchQueue>,
}
impl Drop for SessionGuard {
fn drop(&mut self) {
unsafe { self.session.stopRunning() };
self.chan.close();
}
}
struct DelegateIvars {
chan: Arc<FrameChannel>,
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "MoqVideoCameraDelegate"]
#[ivars = DelegateIvars]
struct Delegate;
unsafe impl NSObjectProtocol for Delegate {}
unsafe impl AVCaptureVideoDataOutputSampleBufferDelegate for Delegate {
#[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))]
unsafe fn did_output(
&self,
_output: &AVCaptureOutput,
sample_buffer: &CMSampleBuffer,
_connection: &AVCaptureConnection,
) {
if let Some(frame) = surface_frame(sample_buffer) {
self.ivars().chan.push(frame);
}
}
}
);
impl Delegate {
fn new(chan: Arc<FrameChannel>) -> Retained<Self> {
let this = Self::alloc().set_ivars(DelegateIvars { chan });
unsafe { msg_send![super(this), init] }
}
}