#![allow(unsafe_code)]
use block2::RcBlock;
use cranpose_services::{set_platform_camera, Camera, CameraError, CameraFrame, CameraStill};
use dispatch2::{DispatchQueue, DispatchRetained};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Bool, ProtocolObject};
use objc2::{define_class, msg_send, AllocAnyThread};
use objc2_av_foundation::{
AVCaptureAutoFocusRangeRestriction, AVCaptureConnection, AVCaptureDevice, AVCaptureDeviceInput,
AVCaptureDevicePosition, AVCaptureDeviceType, AVCaptureDeviceTypeBuiltInDualWideCamera,
AVCaptureDeviceTypeBuiltInTripleCamera, AVCaptureFocusMode, AVCaptureOutput, AVCapturePhoto,
AVCapturePhotoCaptureDelegate, AVCapturePhotoOutput, AVCapturePhotoSettings,
AVCapturePrimaryConstituentDeviceRestrictedSwitchingBehaviorConditions,
AVCapturePrimaryConstituentDeviceSwitchingBehavior, AVCaptureSession,
AVCaptureSessionPresetPhoto, AVCaptureTorchMode, AVCaptureVideoDataOutput,
AVCaptureVideoDataOutputSampleBufferDelegate, AVMediaType, AVMediaTypeVideo, AVVideoCodecKey,
AVVideoCodecTypeJPEG,
};
use objc2_core_media::CMSampleBuffer;
use objc2_core_video::{
kCVPixelBufferPixelFormatTypeKey, CVBuffer, CVPixelBuffer, CVPixelBufferGetBaseAddress,
CVPixelBufferGetBytesPerRow, CVPixelBufferGetHeight, CVPixelBufferGetWidth,
CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
};
use objc2_foundation::{NSDictionary, NSError, NSNumber, NSObject, NSObjectProtocol, NSString};
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::OnceLock;
use std::time::Duration;
const PIXEL_FORMAT_32BGRA: u32 = 0x4247_5241;
fn latest() -> &'static Mutex<Option<CameraFrame>> {
static SLOT: OnceLock<Mutex<Option<CameraFrame>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
fn buffer_pool() -> &'static Mutex<Vec<Vec<u8>>> {
static POOL: OnceLock<Mutex<Vec<Vec<u8>>>> = OnceLock::new();
POOL.get_or_init(|| Mutex::new(Vec::new()))
}
struct SessionHolder {
session: Retained<AVCaptureSession>,
photo_output: Retained<AVCapturePhotoOutput>,
device: Retained<AVCaptureDevice>,
_delegate: Retained<FrameDelegate>,
_queue: DispatchRetained<DispatchQueue>,
}
unsafe impl Send for SessionHolder {}
fn session_slot() -> &'static Mutex<Option<SessionHolder>> {
static SLOT: OnceLock<Mutex<Option<SessionHolder>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
pub(crate) fn register() {
set_platform_camera(Arc::new(IosCamera));
}
struct IosCamera;
impl Camera for IosCamera {
fn start(&self) -> Result<String, CameraError> {
if session_slot().lock().map(|s| s.is_some()).unwrap_or(false) {
return Ok("camera".into());
}
start_session()
}
fn latest_frame(&self) -> Option<CameraFrame> {
latest().lock().ok().and_then(|f| f.clone())
}
fn capture_still(&self) -> Option<CameraStill> {
capture_photo()
}
fn set_torch(&self, on: bool) -> bool {
let Ok(slot) = session_slot().lock() else {
return false;
};
let Some(holder) = slot.as_ref() else {
return false;
};
let device = &holder.device;
let mode = if on {
AVCaptureTorchMode::On
} else {
AVCaptureTorchMode::Off
};
if !unsafe { device.hasTorch() } || !unsafe { device.isTorchModeSupported(mode) } {
return false;
}
if unsafe { device.lockForConfiguration() }.is_err() {
return false;
}
unsafe { device.setTorchMode(mode) };
unsafe { device.unlockForConfiguration() };
true
}
fn stop(&self) {
if let Ok(mut slot) = session_slot().lock() {
if let Some(holder) = slot.take() {
unsafe { holder.session.stopRunning() };
}
}
if let Ok(mut f) = latest().lock() {
*f = None;
}
}
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "CranposeCameraDelegate"]
#[ivars = ()]
struct FrameDelegate;
unsafe impl NSObjectProtocol for FrameDelegate {}
unsafe impl AVCaptureVideoDataOutputSampleBufferDelegate for FrameDelegate {
#[unsafe(method(captureOutput:didOutputSampleBuffer:fromConnection:))]
unsafe fn did_output(
&self,
_output: &AVCaptureOutput,
sample_buffer: &CMSampleBuffer,
_connection: &AVCaptureConnection,
) {
if let Some(frame) = frame_from_sample(sample_buffer) {
if let Ok(mut slot) = latest().lock() {
if let Some(old) = slot.replace(frame) {
if let Ok(mut pool) = buffer_pool().lock() {
if pool.len() < 3 {
pool.push(old.rgba);
}
}
}
}
}
}
}
);
impl FrameDelegate {
fn new() -> Retained<Self> {
let this = Self::alloc().set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
type PhotoSender = mpsc::Sender<Option<Vec<u8>>>;
fn photo_result_slot() -> &'static Mutex<Option<PhotoSender>> {
static SLOT: OnceLock<Mutex<Option<PhotoSender>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
define_class!(
#[unsafe(super(NSObject))]
#[name = "CranposePhotoDelegate"]
#[ivars = ()]
struct PhotoDelegate;
unsafe impl NSObjectProtocol for PhotoDelegate {}
unsafe impl AVCapturePhotoCaptureDelegate for PhotoDelegate {
#[unsafe(method(captureOutput:didFinishProcessingPhoto:error:))]
unsafe fn did_finish_photo(
&self,
_output: &AVCapturePhotoOutput,
photo: &AVCapturePhoto,
error: Option<&NSError>,
) {
let jpeg = if error.is_some() {
None
} else {
unsafe { photo.fileDataRepresentation() }.map(|data| data.to_vec())
};
if let Ok(mut slot) = photo_result_slot().lock() {
if let Some(sender) = slot.take() {
let _ = sender.send(jpeg);
}
}
}
}
);
impl PhotoDelegate {
fn new() -> Retained<Self> {
let this = Self::alloc().set_ivars(());
unsafe { msg_send![super(this), init] }
}
}
fn capture_photo() -> Option<CameraStill> {
static CAPTURE_GATE: Mutex<()> = Mutex::new(());
let _gate = CAPTURE_GATE.lock().ok()?;
let (sender, receiver) = mpsc::channel::<Option<Vec<u8>>>();
let delegate = PhotoDelegate::new();
{
let slot = session_slot().lock().ok()?;
let holder = slot.as_ref()?;
if let Ok(mut result) = photo_result_slot().lock() {
*result = Some(sender);
}
let codec_key: &NSString = unsafe { AVVideoCodecKey }?;
let codec_value: &NSString = unsafe { AVVideoCodecTypeJPEG }?;
let codec: &AnyObject = codec_value.as_ref();
let format = NSDictionary::from_slices(&[codec_key], &[codec]);
let settings = unsafe { AVCapturePhotoSettings::photoSettingsWithFormat(Some(&format)) };
#[allow(deprecated)]
unsafe {
settings.setHighResolutionPhotoEnabled(true);
}
unsafe {
holder
.photo_output
.capturePhotoWithSettings_delegate(&settings, ProtocolObject::from_ref(&*delegate));
}
}
let jpeg = receiver.recv_timeout(Duration::from_secs(5)).ok().flatten();
if jpeg.is_none() {
if let Ok(mut result) = photo_result_slot().lock() {
*result = None;
}
}
drop(delegate);
jpeg.map(|jpeg| CameraStill { jpeg })
}
fn frame_from_sample(sample: &CMSampleBuffer) -> Option<CameraFrame> {
let image_buffer = unsafe { sample.image_buffer() }?;
let pixel_buffer: &CVPixelBuffer =
unsafe { &*((&*image_buffer as *const CVBuffer) as *const CVPixelBuffer) };
let flags = CVPixelBufferLockFlags::ReadOnly;
unsafe { CVPixelBufferLockBaseAddress(pixel_buffer, flags) };
let width = CVPixelBufferGetWidth(pixel_buffer);
let height = CVPixelBufferGetHeight(pixel_buffer);
let bytes_per_row = CVPixelBufferGetBytesPerRow(pixel_buffer);
let base = CVPixelBufferGetBaseAddress(pixel_buffer) as *const u8;
let out_w = height;
let out_h = width;
let mut rgba = buffer_pool()
.lock()
.ok()
.and_then(|mut pool| pool.pop())
.unwrap_or_default();
rgba.clear();
rgba.resize(out_w * out_h * 4, 0);
if !base.is_null() && bytes_per_row >= width * 4 {
for sy in 0..height {
let src_row = unsafe { base.add(sy * bytes_per_row) };
let dx = height - 1 - sy;
for sx in 0..width {
let src = unsafe { src_row.add(sx * 4) };
let (b, g, r, a) = unsafe { (*src, *src.add(1), *src.add(2), *src.add(3)) };
let dst = (sx * out_w + dx) * 4;
rgba[dst] = r;
rgba[dst + 1] = g;
rgba[dst + 2] = b;
rgba[dst + 3] = a;
}
}
}
unsafe { CVPixelBufferUnlockBaseAddress(pixel_buffer, flags) };
Some(CameraFrame {
width: out_w as u32,
height: out_h as u32,
rgba,
})
}
fn select_camera_device(media_type: &AVMediaType) -> Option<Retained<AVCaptureDevice>> {
let virtual_types: [&AVCaptureDeviceType; 2] = unsafe {
[
AVCaptureDeviceTypeBuiltInTripleCamera,
AVCaptureDeviceTypeBuiltInDualWideCamera,
]
};
for device_type in virtual_types {
if let Some(device) = unsafe {
AVCaptureDevice::defaultDeviceWithDeviceType_mediaType_position(
device_type,
Some(media_type),
AVCaptureDevicePosition::Back,
)
} {
return Some(device);
}
}
unsafe { AVCaptureDevice::defaultDeviceWithMediaType(media_type) }
}
fn start_session() -> Result<String, CameraError> {
let media_type = unsafe { AVMediaTypeVideo }
.ok_or_else(|| CameraError::Failed("AVMediaTypeVideo unavailable".into()))?;
let handler = RcBlock::new(|_granted: Bool| {});
unsafe { AVCaptureDevice::requestAccessForMediaType_completionHandler(media_type, &handler) };
let device = select_camera_device(media_type).ok_or(CameraError::Unsupported)?;
let name = unsafe { device.localizedName() }.to_string();
if unsafe { device.lockForConfiguration() }.is_ok() {
if unsafe { device.isAutoFocusRangeRestrictionSupported() } {
unsafe {
device.setAutoFocusRangeRestriction(AVCaptureAutoFocusRangeRestriction::Near)
};
}
if unsafe { device.isFocusModeSupported(AVCaptureFocusMode::ContinuousAutoFocus) } {
unsafe { device.setFocusMode(AVCaptureFocusMode::ContinuousAutoFocus) };
}
if unsafe { device.primaryConstituentDeviceSwitchingBehavior() }
!= AVCapturePrimaryConstituentDeviceSwitchingBehavior::Unsupported
{
unsafe {
device.setPrimaryConstituentDeviceSwitchingBehavior_restrictedSwitchingBehaviorConditions(
AVCapturePrimaryConstituentDeviceSwitchingBehavior::Auto,
AVCapturePrimaryConstituentDeviceRestrictedSwitchingBehaviorConditions(0),
)
};
}
unsafe { device.unlockForConfiguration() };
}
let input = unsafe { AVCaptureDeviceInput::deviceInputWithDevice_error(&device) }
.map_err(|_| CameraError::Failed("could not open camera input".into()))?;
let session = unsafe { AVCaptureSession::new() };
unsafe { session.beginConfiguration() };
let preset = unsafe { AVCaptureSessionPresetPhoto };
if unsafe { session.canSetSessionPreset(preset) } {
unsafe { session.setSessionPreset(preset) };
}
if !unsafe { session.canAddInput(&input) } {
return Err(CameraError::Failed("cannot add camera input".into()));
}
unsafe { session.addInput(&input) };
let output = unsafe { AVCaptureVideoDataOutput::new() };
let key: &NSString =
unsafe { &*(kCVPixelBufferPixelFormatTypeKey as *const _ as *const NSString) };
let value = NSNumber::numberWithUnsignedInt(PIXEL_FORMAT_32BGRA);
let value_obj: &AnyObject = &value;
let settings = NSDictionary::from_slices(&[key], &[value_obj]);
unsafe { output.setVideoSettings(Some(&settings)) };
unsafe { output.setAlwaysDiscardsLateVideoFrames(true) };
let delegate = FrameDelegate::new();
let queue = DispatchQueue::new("com.cranpose.camera", None);
unsafe {
output
.setSampleBufferDelegate_queue(Some(ProtocolObject::from_ref(&*delegate)), Some(&queue))
};
if !unsafe { session.canAddOutput(&output) } {
return Err(CameraError::Failed("cannot add camera output".into()));
}
unsafe { session.addOutput(&output) };
let photo_output = unsafe { AVCapturePhotoOutput::new() };
if !unsafe { session.canAddOutput(&photo_output) } {
return Err(CameraError::Failed("cannot add photo output".into()));
}
unsafe { session.addOutput(&photo_output) };
#[allow(deprecated)]
unsafe {
photo_output.setHighResolutionCaptureEnabled(true);
}
unsafe { session.commitConfiguration() };
unsafe { session.startRunning() };
if let Ok(mut slot) = session_slot().lock() {
*slot = Some(SessionHolder {
session,
photo_output,
device,
_delegate: delegate,
_queue: queue,
});
}
Ok(name)
}