use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Mutex};
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::runtime::ProtocolObject;
use objc2::{AllocAnyThread, DefinedClass, define_class, msg_send};
use objc2_av_foundation::{
AVCaptureConnection, AVCaptureDevice, AVCaptureDeviceFormat, AVCaptureDeviceInput,
AVCaptureOutput, AVCaptureSession, AVCaptureVideoDataOutput,
AVCaptureVideoDataOutputSampleBufferDelegate,
};
use objc2_core_media::CMSampleBuffer;
use objc2_core_video::{
CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
kCVPixelBufferPixelFormatTypeKey,
};
use objc2_foundation::{NSDictionary, NSNumber, NSObjectProtocol, NSString};
use crate::error::{Error, PlatformError};
use crate::platform::macos::catch_objc;
use crate::platform::macos::device::pixel_format_to_fourcc;
use crate::platform::macos::frame::{MacosFrame, MacosTimestamp};
use crate::stream::CameraStream;
use crate::types::StreamConfig;
type FrameCallback = Box<dyn FnMut(&MacosFrame<'_>) + Send + 'static>;
struct DelegateIvars {
callback: Arc<Mutex<Option<FrameCallback>>>,
}
define_class!(
#[unsafe(super(objc2_foundation::NSObject))]
#[ivars = DelegateIvars]
#[name = "CameraStreamSampleBufferDelegate"]
struct SampleBufferDelegate;
impl SampleBufferDelegate {
}
unsafe impl NSObjectProtocol for SampleBufferDelegate {}
unsafe impl AVCaptureVideoDataOutputSampleBufferDelegate for SampleBufferDelegate {
#[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))]
#[allow(non_snake_case)]
unsafe fn captureOutput_didOutputSampleBuffer_fromConnection(
&self,
_output: &AVCaptureOutput,
sample_buffer: &CMSampleBuffer,
_connection: &AVCaptureConnection,
) {
let pixel_buffer = match unsafe { sample_buffer.image_buffer() } {
Some(pb) => pb,
None => return,
};
let cm_time = unsafe { sample_buffer.presentation_time_stamp() };
let timestamp = MacosTimestamp {
value: cm_time.value,
timescale: cm_time.timescale,
flags: cm_time.flags.0,
epoch: cm_time.epoch,
};
let lock_flags = CVPixelBufferLockFlags::ReadOnly;
unsafe {
CVPixelBufferLockBaseAddress(&pixel_buffer, lock_flags);
}
let frame = unsafe { MacosFrame::from_locked_pixel_buffer(&pixel_buffer, timestamp) };
if let Ok(mut guard) = self.ivars().callback.lock()
&& let Some(ref mut cb) = *guard {
cb(&frame);
}
unsafe {
CVPixelBufferUnlockBaseAddress(&pixel_buffer, lock_flags);
}
}
}
);
impl SampleBufferDelegate {
fn new(callback: FrameCallback) -> Retained<Self> {
let ivars = DelegateIvars {
callback: Arc::new(Mutex::new(Some(callback))),
};
let obj = Self::alloc().set_ivars(ivars);
unsafe { msg_send![super(obj), init] }
}
}
pub struct MacosCameraStream {
session: Retained<AVCaptureSession>,
device: Retained<AVCaptureDevice>,
output: Retained<AVCaptureVideoDataOutput>,
delegate: Option<Retained<SampleBufferDelegate>>,
config_locked: bool,
running: bool,
}
impl MacosCameraStream {
pub(crate) fn new(
device: Retained<AVCaptureDevice>,
config: &StreamConfig,
) -> Result<Self, Error> {
let session = unsafe { AVCaptureSession::new() };
let input = unsafe { AVCaptureDeviceInput::deviceInputWithDevice_error(&device) }
.map_err(|e| Error::Platform(PlatformError::NsError(e)))?;
let output = unsafe { AVCaptureVideoDataOutput::new() };
let target_fourcc = pixel_format_to_fourcc(&config.pixel_format);
unsafe {
let key: &NSString = std::mem::transmute::<&objc2_core_foundation::CFString, &NSString>(
kCVPixelBufferPixelFormatTypeKey,
);
let value = NSNumber::new_u32(target_fourcc);
let settings: Retained<NSDictionary<NSString, AnyObject>> =
NSDictionary::dictionaryWithObject_forKey(&value, ProtocolObject::from_ref(key));
output.setVideoSettings(Some(&settings));
}
let formats = unsafe { device.formats() };
let mut matched_format: Option<Retained<AVCaptureDeviceFormat>> = None;
for format in formats.iter() {
let desc = unsafe { format.formatDescription() };
let sub_type = unsafe { desc.media_sub_type() };
let dims = unsafe { objc2_core_media::CMVideoFormatDescriptionGetDimensions(&desc) };
if sub_type == target_fourcc
&& dims.width as u32 == config.size.width
&& dims.height as u32 == config.size.height
{
matched_format = Some(format.clone());
break;
}
}
let matched = matched_format.ok_or(Error::UnsupportedFormat)?;
let frame_duration = objc2_core_media::CMTime {
value: config.frame_rate.denominator as i64,
timescale: config.frame_rate.numerator as i32,
flags: objc2_core_media::CMTimeFlags(1), epoch: 0,
};
catch_objc(AssertUnwindSafe(|| unsafe {
session.beginConfiguration();
if !session.canAddInput(&input) {
session.commitConfiguration();
return Err(Error::Platform(PlatformError::Message(
"cannot add input to session",
)));
}
session.addInput(&input);
if !session.canAddOutput(&output) {
session.commitConfiguration();
return Err(Error::Platform(PlatformError::Message(
"cannot add output to session",
)));
}
session.addOutput(&output);
session.commitConfiguration();
Ok::<(), Error>(())
}))??;
unsafe { device.lockForConfiguration() }
.map_err(|e| Error::Platform(PlatformError::NsError(e)))?;
catch_objc(AssertUnwindSafe(|| unsafe {
device.setActiveFormat(&matched);
}))?;
let _ = catch_objc(AssertUnwindSafe(|| unsafe {
device.setActiveVideoMinFrameDuration(frame_duration);
}));
let _ = catch_objc(AssertUnwindSafe(|| unsafe {
device.setActiveVideoMaxFrameDuration(frame_duration);
}));
Ok(MacosCameraStream {
session,
device,
output,
delegate: None,
config_locked: true,
running: false,
})
}
}
impl CameraStream for MacosCameraStream {
type Frame<'a> = MacosFrame<'a>;
type Error = Error;
fn start<F>(&mut self, callback: F) -> Result<(), Self::Error>
where
F: FnMut(&Self::Frame<'_>) + Send + 'static,
{
if self.running {
return Err(Error::AlreadyStarted);
}
let delegate = SampleBufferDelegate::new(Box::new(callback));
let queue = dispatch2::DispatchQueue::new(
"camera-stream.callback",
dispatch2::DispatchQueueAttr::SERIAL,
);
unsafe {
self.output.setSampleBufferDelegate_queue(
Some(ProtocolObject::from_ref(&*delegate)),
Some(&queue),
);
}
self.delegate = Some(delegate);
catch_objc(AssertUnwindSafe(|| unsafe { self.session.startRunning() }))?;
self.running = true;
if self.config_locked {
unsafe { self.device.unlockForConfiguration() };
self.config_locked = false;
}
Ok(())
}
fn stop(&mut self) -> Result<(), Self::Error> {
if !self.running {
return Err(Error::NotStarted);
}
unsafe { self.session.stopRunning() };
unsafe {
self.output.setSampleBufferDelegate_queue(None, None);
}
if let Some(ref delegate) = self.delegate
&& let Ok(mut guard) = delegate.ivars().callback.lock()
{
*guard = None;
}
self.delegate = None;
self.running = false;
Ok(())
}
}
impl Drop for MacosCameraStream {
fn drop(&mut self) {
if self.running {
let _ = self.stop();
}
if self.config_locked {
unsafe { self.device.unlockForConfiguration() };
self.config_locked = false;
}
}
}