#![allow(unsafe_code)]
use std::collections::VecDeque;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Mutex};
use crate::camera::{CameraCapture, CameraCaptureConfig, CaptureOutputPreference};
use crate::{CaptureError, Select};
use dispatch2::{DispatchQueue, DispatchQueueAttr, DispatchRetained};
use mediaway_common::{
Bytes, CodecKind, PixelFormat, Rational, StreamInfo, VideoFrame, VideoFrameStorage,
VideoGeometry,
};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, ProtocolObject};
use objc2::{AnyThread, DefinedClass, define_class, msg_send};
use objc2_av_foundation::{
AVCaptureConnection, AVCaptureDevice, AVCaptureDeviceInput, AVCaptureOutput, AVCaptureSession,
AVCaptureVideoDataOutput, AVCaptureVideoDataOutputSampleBufferDelegate, AVMediaTypeVideo,
};
use objc2_core_media::CMSampleBuffer;
use objc2_core_video::{
kCVPixelBufferPixelFormatTypeKey, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
};
use objc2_foundation::{NSDictionary, NSNumber, NSObjectProtocol, NSString};
struct FrameQueue {
frames: Mutex<VecDeque<VideoFrame>>,
}
const FRAME_QUEUE_CAP: usize = 4;
struct CameraDelegateIvars {
queue: Arc<FrameQueue>,
next_pts: AtomicI64,
}
define_class!(
#[unsafe(super(objc2_foundation::NSObject))]
#[thread_kind = AnyThread]
#[name = "MediawayCameraDelegate"]
#[ivars = CameraDelegateIvars]
struct CameraDelegate;
unsafe impl NSObjectProtocol for CameraDelegate {}
unsafe impl AVCaptureVideoDataOutputSampleBufferDelegate for CameraDelegate {
#[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))]
unsafe fn capture_output_did_output_sample_buffer_from_connection(
&self,
_output: &AVCaptureOutput,
sample_buffer: &CMSampleBuffer,
_connection: &AVCaptureConnection,
) {
if let Some(data) = unsafe { super::pixel::extract_nv12(sample_buffer) } {
let pts = self.ivars().next_pts.fetch_add(1, Ordering::Relaxed);
push_frame(self.ivars().queue.as_ref(), data, pts);
}
}
}
);
impl CameraDelegate {
fn new(queue: Arc<FrameQueue>) -> Retained<Self> {
let this = Self::alloc();
let this = this.set_ivars(CameraDelegateIvars {
queue,
next_pts: AtomicI64::new(0),
});
unsafe { msg_send![super(this), init] }
}
}
struct CameraSession {
stream_info: StreamInfo,
queue: Arc<FrameQueue>,
session: Retained<AVCaptureSession>,
_input: Retained<AVCaptureDeviceInput>,
_output: Retained<AVCaptureVideoDataOutput>,
_delegate: Retained<CameraDelegate>,
_dispatch_queue: DispatchRetained<DispatchQueue>,
}
pub struct AppleCameraCapture {
inner: Option<CameraSession>,
}
impl AppleCameraCapture {
pub fn open(config: &CameraCaptureConfig) -> Result<Self, CaptureError> {
if config.select != Select::Default {
return Err(CaptureError::Unsupported);
}
if config.output != CaptureOutputPreference::CpuFramesOk {
return Err(CaptureError::Unsupported);
}
let media_type = unsafe { AVMediaTypeVideo }.ok_or(CaptureError::Backend)?;
let device = unsafe { AVCaptureDevice::defaultDeviceWithMediaType(media_type) }
.ok_or(CaptureError::InvalidInput)?;
let input = unsafe { AVCaptureDeviceInput::deviceInputWithDevice_error(&device) }
.map_err(|_| CaptureError::Backend)?;
let session = unsafe { AVCaptureSession::new() };
if !unsafe { session.canAddInput(&input) } {
return Err(CaptureError::Backend);
}
unsafe { session.addInput(&input) };
let output = unsafe { AVCaptureVideoDataOutput::new() };
let available = unsafe { output.availableVideoCVPixelFormatTypes() };
let requested_format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
#[allow(
clippy::cast_possible_wrap,
reason = "requested_format is a fixed FourCC-style OSType constant, not user input"
)]
let requested_format_i32 = requested_format as i32;
let format_available = available
.iter()
.any(|n| n.intValue() == requested_format_i32);
if !format_available {
return Err(CaptureError::Unsupported);
}
let key: &NSString =
unsafe { &*std::ptr::from_ref(kCVPixelBufferPixelFormatTypeKey).cast::<NSString>() };
let value = NSNumber::numberWithUnsignedInt(requested_format);
let value: &AnyObject = &value;
let settings = NSDictionary::from_slices(&[key], &[value]);
unsafe { output.setVideoSettings(Some(&settings)) };
let queue = Arc::new(FrameQueue {
frames: Mutex::new(VecDeque::new()),
});
let delegate = CameraDelegate::new(Arc::clone(&queue));
let dispatch_queue = DispatchQueue::new("dev.mediaway.camera", DispatchQueueAttr::SERIAL);
let delegate_protocol = ProtocolObject::from_ref(&*delegate);
unsafe {
output.setSampleBufferDelegate_queue(Some(delegate_protocol), Some(&dispatch_queue));
}
if !unsafe { session.canAddOutput(&output) } {
return Err(CaptureError::Backend);
}
unsafe { session.addOutput(&output) };
unsafe { session.startRunning() };
let info = StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base: config.time_base,
geometry: VideoGeometry {
width: 0,
height: 0,
},
extra_data: Bytes::new(),
};
Ok(Self {
inner: Some(CameraSession {
stream_info: info,
queue,
session,
_input: input,
_output: output,
_delegate: delegate,
_dispatch_queue: dispatch_queue,
}),
})
}
}
impl CameraCapture for AppleCameraCapture {
fn stream_info(&self) -> &StreamInfo {
#[allow(
clippy::option_if_let_else,
reason = "map_or_else forces 'static vs 'self lifetime clash"
)]
if let Some(s) = self.inner.as_ref() {
&s.stream_info
} else {
closed_video_info()
}
}
fn poll_frame(&mut self) -> Result<Option<VideoFrame>, CaptureError> {
let Some(session) = self.inner.as_ref() else {
return Err(CaptureError::Closed);
};
let mut q = session
.queue
.frames
.lock()
.map_err(|_| CaptureError::Backend)?;
Ok(q.pop_front())
}
fn release_frame(&mut self) -> Result<(), CaptureError> {
if self.inner.is_none() {
return Err(CaptureError::Closed);
}
Ok(())
}
fn close(&mut self) -> Result<(), CaptureError> {
let Some(session) = self.inner.take() else {
return Ok(());
};
unsafe { session.session.stopRunning() };
Ok(())
}
}
impl Drop for AppleCameraCapture {
fn drop(&mut self) {
let _ = self.close();
}
}
fn closed_video_info() -> &'static StreamInfo {
use std::sync::OnceLock;
static INFO: OnceLock<StreamInfo> = OnceLock::new();
INFO.get_or_init(|| StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base: Rational::new(1, 30),
geometry: VideoGeometry {
width: 0,
height: 0,
},
extra_data: Bytes::new(),
})
}
fn push_frame(queue: &FrameQueue, data_and_size: (Bytes, u32, u32), pts: i64) {
let (data, width, height) = data_and_size;
let frame = VideoFrame {
pts,
duration: 1,
width,
height,
format: PixelFormat::Nv12,
storage: VideoFrameStorage::Cpu { data },
};
if let Ok(mut q) = queue.frames.lock() {
if q.len() >= FRAME_QUEUE_CAP {
let _ = q.pop_front();
}
q.push_back(frame);
}
}
#[cfg(test)]
#[path = "camera_tests.rs"]
mod tests;