#![allow(unsafe_code)]
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{SyncSender, sync_channel};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::desktop::{
CaptureOutputPreference, DesktopCaptureSource, DesktopVideoCapture, DesktopVideoCaptureConfig,
};
use crate::{CaptureError, Select};
use block2::RcBlock;
use dispatch2::{DispatchQueue, DispatchQueueAttr, DispatchRetained};
use mediaway_common::{
Bytes, CodecKind, PixelFormat, Rational, StreamInfo, VideoFrame, VideoFrameStorage,
VideoGeometry,
};
use objc2::rc::Retained;
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2::{AnyThread, DefinedClass, define_class, msg_send};
use objc2_core_media::CMSampleBuffer;
use objc2_core_video::kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
use objc2_foundation::{NSArray, NSError, NSObject};
use objc2_screen_capture_kit::{
SCContentFilter, SCShareableContent, SCStream, SCStreamConfiguration, SCStreamDelegate,
SCStreamOutput, SCStreamOutputType, SCWindow,
};
const CAPTURE_WIDTH: usize = 1920;
const CAPTURE_HEIGHT: usize = 1080;
const COMPLETION_TIMEOUT: Duration = Duration::from_secs(10);
struct FrameQueue {
frames: Mutex<VecDeque<VideoFrame>>,
}
const FRAME_QUEUE_CAP: usize = 4;
struct StreamOutputIvars {
queue: Arc<FrameQueue>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = AnyThread]
#[name = "MediawayScreenStreamOutput"]
#[ivars = StreamOutputIvars]
struct StreamOutput;
unsafe impl NSObjectProtocol for StreamOutput {}
unsafe impl SCStreamOutput for StreamOutput {
#[unsafe(method(stream:didOutputSampleBuffer:ofType:))]
unsafe fn stream_did_output_sample_buffer_of_type(
&self,
_stream: &SCStream,
sample_buffer: &CMSampleBuffer,
of_type: SCStreamOutputType,
) {
if of_type != SCStreamOutputType::Screen {
return;
}
if let Some(data) = unsafe { super::pixel::extract_nv12(sample_buffer) } {
push_frame(self.ivars().queue.as_ref(), data);
}
}
}
);
impl StreamOutput {
fn new(queue: Arc<FrameQueue>) -> Retained<Self> {
let this = Self::alloc();
let this = this.set_ivars(StreamOutputIvars { queue });
unsafe { msg_send![super(this), init] }
}
}
struct StreamDelegateIvars {
stopped: Arc<AtomicBool>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = AnyThread]
#[name = "MediawayScreenStreamDelegate"]
#[ivars = StreamDelegateIvars]
struct StreamDelegate;
unsafe impl NSObjectProtocol for StreamDelegate {}
unsafe impl SCStreamDelegate for StreamDelegate {
#[unsafe(method(stream:didStopWithError:))]
unsafe fn stream_did_stop_with_error(&self, _stream: &SCStream, _error: &NSError) {
self.ivars().stopped.store(true, Ordering::SeqCst);
}
}
);
impl StreamDelegate {
fn new(stopped: Arc<AtomicBool>) -> Retained<Self> {
let this = Self::alloc();
let this = this.set_ivars(StreamDelegateIvars { stopped });
unsafe { msg_send![super(this), init] }
}
}
struct Session {
stream_info: StreamInfo,
queue: Arc<FrameQueue>,
stopped: Arc<AtomicBool>,
stream: Retained<SCStream>,
_output: Retained<StreamOutput>,
_delegate: Retained<StreamDelegate>,
_dispatch_queue: DispatchRetained<DispatchQueue>,
}
impl Session {
fn poll_frame(&self) -> Result<Option<VideoFrame>, CaptureError> {
if self.stopped.load(Ordering::Relaxed) {
return Err(CaptureError::DeviceLost);
}
let mut q = self
.queue
.frames
.lock()
.map_err(|_| CaptureError::Backend)?;
Ok(q.pop_front())
}
fn close(&self) -> Result<(), CaptureError> {
stop_capture(&self.stream)
}
}
fn open_stream(
filter: &SCContentFilter,
config: &DesktopVideoCaptureConfig,
) -> Result<Session, CaptureError> {
if config.output != CaptureOutputPreference::CpuFramesOk {
return Err(CaptureError::Unsupported);
}
let stream_config = unsafe { SCStreamConfiguration::new() };
unsafe {
stream_config.setWidth(CAPTURE_WIDTH);
stream_config.setHeight(CAPTURE_HEIGHT);
stream_config.setPixelFormat(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange);
stream_config.setShowsCursor(true);
}
let queue = Arc::new(FrameQueue {
frames: Mutex::new(VecDeque::new()),
});
let output = StreamOutput::new(Arc::clone(&queue));
let stopped = Arc::new(AtomicBool::new(false));
let delegate = StreamDelegate::new(Arc::clone(&stopped));
let stream = SCStream::alloc();
let delegate_protocol = ProtocolObject::from_ref(&*delegate);
let stream = unsafe {
SCStream::initWithFilter_configuration_delegate(
stream,
filter,
&stream_config,
Some(delegate_protocol),
)
};
let dispatch_queue =
DispatchQueue::new("dev.mediaway.screencapturekit", DispatchQueueAttr::SERIAL);
let output_protocol = ProtocolObject::from_ref(&*output);
unsafe {
stream.addStreamOutput_type_sampleHandlerQueue_error(
output_protocol,
SCStreamOutputType::Screen,
Some(&dispatch_queue),
)
}
.map_err(|_| CaptureError::Backend)?;
start_capture(&stream)?;
let info = StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base: config.time_base,
geometry: VideoGeometry {
width: CAPTURE_WIDTH.try_into().unwrap_or(0),
height: CAPTURE_HEIGHT.try_into().unwrap_or(0),
},
extra_data: Bytes::new(),
};
Ok(Session {
stream_info: info,
queue,
stopped,
stream,
_output: output,
_delegate: delegate,
_dispatch_queue: dispatch_queue,
})
}
pub struct AppleScreenCapture {
inner: Option<Session>,
}
impl AppleScreenCapture {
pub fn open(config: &DesktopVideoCaptureConfig) -> Result<Self, CaptureError> {
let DesktopCaptureSource::Screen { select } = &config.source else {
return Err(CaptureError::Unsupported);
};
if *select != Select::Default {
return Err(CaptureError::Unsupported);
}
let content = fetch_shareable_content()?;
let displays = unsafe { content.displays() };
let display = displays.firstObject().ok_or(CaptureError::InvalidInput)?;
let excluded = NSArray::<SCWindow>::new();
let filter = SCContentFilter::alloc();
let filter = unsafe {
SCContentFilter::initWithDisplay_excludingWindows(filter, &display, &excluded)
};
Ok(Self {
inner: Some(open_stream(&filter, config)?),
})
}
}
impl DesktopVideoCapture for AppleScreenCapture {
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> {
self.inner
.as_ref()
.ok_or(CaptureError::Closed)?
.poll_frame()
}
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(());
};
let _ = session.close();
Ok(())
}
}
impl Drop for AppleScreenCapture {
fn drop(&mut self) {
let _ = self.close();
}
}
pub struct AppleWindowCapture {
inner: Option<Session>,
}
impl AppleWindowCapture {
pub fn open(config: &DesktopVideoCaptureConfig) -> Result<Self, CaptureError> {
let DesktopCaptureSource::Window { window } = &config.source else {
return Err(CaptureError::Unsupported);
};
let window_id = u32::try_from(window.get()).map_err(|_| CaptureError::InvalidInput)?;
let content = fetch_shareable_content()?;
let windows = unsafe { content.windows() };
let target = windows
.iter()
.find(|w| {
unsafe { w.windowID() == window_id }
})
.ok_or(CaptureError::InvalidInput)?;
let filter = SCContentFilter::alloc();
let filter = unsafe { SCContentFilter::initWithDesktopIndependentWindow(filter, &target) };
Ok(Self {
inner: Some(open_stream(&filter, config)?),
})
}
}
impl DesktopVideoCapture for AppleWindowCapture {
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> {
self.inner
.as_ref()
.ok_or(CaptureError::Closed)?
.poll_frame()
}
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(());
};
let _ = session.close();
Ok(())
}
}
impl Drop for AppleWindowCapture {
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 take_sender<T>(state: &Mutex<Option<SyncSender<T>>>) -> Option<SyncSender<T>> {
state.lock().ok().and_then(|mut guard| guard.take())
}
struct SendRetainedPtr<T>(*mut T);
unsafe impl<T> Send for SendRetainedPtr<T> {}
fn fetch_shareable_content() -> Result<Retained<SCShareableContent>, CaptureError> {
let (tx, rx) = sync_channel::<Result<SendRetainedPtr<SCShareableContent>, CaptureError>>(1);
let tx = Arc::new(Mutex::new(Some(tx)));
let block: RcBlock<dyn Fn(*mut SCShareableContent, *mut NSError)> = RcBlock::new(
move |content: *mut SCShareableContent, _error: *mut NSError| {
let result = if content.is_null() {
Err(CaptureError::Backend)
} else {
unsafe { Retained::retain(content) }
.map(|r| SendRetainedPtr(Retained::into_raw(r)))
.ok_or(CaptureError::Backend)
};
if let Some(tx) = take_sender(&tx) {
let _ = tx.send(result);
}
},
);
unsafe { SCShareableContent::getShareableContentWithCompletionHandler(&block) };
let ptr = rx
.recv_timeout(COMPLETION_TIMEOUT)
.map_err(|_| CaptureError::Backend)??;
unsafe { Retained::from_raw(ptr.0) }.ok_or(CaptureError::Backend)
}
fn start_capture(stream: &SCStream) -> Result<(), CaptureError> {
let (tx, rx) = sync_channel::<Result<(), CaptureError>>(1);
let tx = Arc::new(Mutex::new(Some(tx)));
let block: RcBlock<dyn Fn(*mut NSError)> = RcBlock::new(move |error: *mut NSError| {
let result = if error.is_null() {
Ok(())
} else {
Err(CaptureError::Backend)
};
if let Some(tx) = take_sender(&tx) {
let _ = tx.send(result);
}
});
unsafe { stream.startCaptureWithCompletionHandler(Some(&block)) };
rx.recv_timeout(COMPLETION_TIMEOUT)
.map_err(|_| CaptureError::Backend)?
}
fn stop_capture(stream: &SCStream) -> Result<(), CaptureError> {
let (tx, rx) = sync_channel::<Result<(), CaptureError>>(1);
let tx = Arc::new(Mutex::new(Some(tx)));
let block: RcBlock<dyn Fn(*mut NSError)> = RcBlock::new(move |error: *mut NSError| {
let result = if error.is_null() {
Ok(())
} else {
Err(CaptureError::Backend)
};
if let Some(tx) = take_sender(&tx) {
let _ = tx.send(result);
}
});
unsafe { stream.stopCaptureWithCompletionHandler(Some(&block)) };
rx.recv_timeout(COMPLETION_TIMEOUT)
.map_err(|_| CaptureError::Backend)?
}
fn push_frame(queue: &FrameQueue, data_and_size: (Bytes, u32, u32)) {
let (data, width, height) = data_and_size;
let frame = VideoFrame {
pts: 0,
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 = "screencapturekit_tests.rs"]
mod tests;