use anyhow::{Result, anyhow};
use block::ConcreteBlock;
use block2::RcBlock;
use cocoa::{
base::{id, nil},
foundation::NSArray,
};
use collections::HashMap;
use core_foundation::base::TCFType;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
CGDisplayModeGetPixelWidth, CGDisplayModeRelease,
};
use ctor::ctor;
use futures::channel::oneshot;
use gpui_platform::{
DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
SharedString, SourceMetadata, size,
};
use media::core_media::{CMSampleBuffer, CMSampleBufferRef};
use metal::NSInteger;
use objc::{
class,
declare::ClassDecl,
msg_send,
runtime::{Class, Object, Sel},
sel, sel_impl,
};
use objc2::{MainThreadMarker, rc::Retained};
use objc2_app_kit::NSScreen;
use objc2_foundation::{NSError, NSNumber, NSString};
use objc2_screen_capture_kit::{SCDisplay, SCShareableContent};
use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc};
#[derive(Clone)]
pub struct MacScreenCaptureSource {
sc_display: Retained<SCDisplay>,
meta: Option<ScreenMeta>,
}
pub struct MacScreenCaptureStream {
sc_stream: id,
sc_stream_output: id,
meta: SourceMetadata,
}
static mut DELEGATE_CLASS: *const Class = ptr::null();
static mut OUTPUT_CLASS: *const Class = ptr::null();
const FRAME_CALLBACK_IVAR: &str = "frame_callback";
#[allow(non_upper_case_globals)]
const SCStreamOutputTypeScreen: NSInteger = 0;
impl ScreenCaptureSource for MacScreenCaptureSource {
fn metadata(&self) -> Result<SourceMetadata> {
let (display_id, size) = unsafe {
let sc_display: id = Retained::as_ptr(&self.sc_display).cast_mut().cast();
let display_id: CGDirectDisplayID = msg_send![sc_display, displayID];
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
CGDisplayModeRelease(display_mode_ref);
(
display_id,
size(DevicePixels(width as i32), DevicePixels(height as i32)),
)
};
let (label, is_main) = self
.meta
.clone()
.map(|meta| (meta.label, meta.is_main))
.unzip();
Ok(SourceMetadata {
id: display_id as u64,
label,
is_main,
resolution: size,
})
}
fn stream(
&self,
_foreground_executor: &ForegroundExecutor,
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
unsafe {
let sc_display: id = Retained::as_ptr(&self.sc_display).cast_mut().cast();
let stream: id = msg_send![class!(SCStream), alloc];
let filter: id = msg_send![class!(SCContentFilter), alloc];
let configuration: id = msg_send![class!(SCStreamConfiguration), alloc];
let delegate: id = msg_send![DELEGATE_CLASS, alloc];
let output: id = msg_send![OUTPUT_CLASS, alloc];
let excluded_windows = NSArray::array(nil);
let filter: id =
msg_send![filter, initWithDisplay:sc_display excludingWindows:excluded_windows];
let configuration: id = msg_send![configuration, init];
let _: id = msg_send![configuration, setScalesToFit: true];
let _: id = msg_send![configuration, setPixelFormat: 0x42475241];
let delegate: id = msg_send![delegate, init];
let output: id = msg_send![output, init];
output.as_mut().unwrap().set_ivar(
FRAME_CALLBACK_IVAR,
Box::into_raw(Box::new(frame_callback)) as *mut c_void,
);
let meta = self.metadata().unwrap();
let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64];
let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64];
let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
let _: () = msg_send![filter, release];
let _: () = msg_send![configuration, release];
let _: () = msg_send![delegate, release];
let (tx, rx) = oneshot::channel();
let mut error: id = nil;
let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id];
if error != nil {
let message: id = msg_send![error, localizedDescription];
let _: () = msg_send![stream, release];
let _: () = msg_send![output, release];
tx.send(Err(anyhow!("failed to add stream output {message:?}")))
.ok();
return rx;
}
let tx = Rc::new(RefCell::new(Some(tx)));
let handler = ConcreteBlock::new({
move |error: id| {
let result = if error == nil {
let stream = MacScreenCaptureStream {
meta: meta.clone(),
sc_stream: stream,
sc_stream_output: output,
};
Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>)
} else {
let _: () = msg_send![stream, release];
let _: () = msg_send![output, release];
let message: id = msg_send![error, localizedDescription];
Err(anyhow!("failed to start screen capture stream {message:?}"))
};
if let Some(tx) = tx.borrow_mut().take() {
tx.send(result).ok();
}
}
});
let handler = handler.copy();
let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler];
rx
}
}
}
impl ScreenCaptureStream for MacScreenCaptureStream {
fn metadata(&self) -> Result<SourceMetadata> {
Ok(self.meta.clone())
}
}
impl Drop for MacScreenCaptureStream {
fn drop(&mut self) {
unsafe {
let mut error: id = nil;
let _: () = msg_send![self.sc_stream, removeStreamOutput:self.sc_stream_output type:SCStreamOutputTypeScreen error:&mut error as *mut _];
if error != nil {
let message: id = msg_send![error, localizedDescription];
log::error!("failed to add stream output {message:?}");
}
let handler = ConcreteBlock::new(move |error: id| {
if error != nil {
let message: id = msg_send![error, localizedDescription];
log::error!("failed to stop screen capture stream {message:?}");
}
});
let block = handler.copy();
let _: () = msg_send![self.sc_stream, stopCaptureWithCompletionHandler:block];
let _: () = msg_send![self.sc_stream, release];
let _: () = msg_send![self.sc_stream_output, release];
}
}
}
#[derive(Clone)]
struct ScreenMeta {
label: SharedString,
is_main: bool,
}
fn screen_id_to_human_label(marker: MainThreadMarker) -> HashMap<CGDirectDisplayID, ScreenMeta> {
let screens = NSScreen::screens(marker);
let mut map = HashMap::default();
let screen_number_key = NSString::from_str("NSScreenNumber");
for (i, screen) in screens.iter().enumerate() {
let description = screen.deviceDescription();
let Some(obj) = description.objectForKey(&screen_number_key) else {
continue;
};
let Some(screen_id) = obj.downcast_ref::<NSNumber>() else {
continue;
};
let name = screen.localizedName().to_string();
map.insert(
screen_id.as_u32(),
ScreenMeta {
label: name.into(),
is_main: i == 0,
},
);
}
map
}
pub(crate) fn get_sources(
marker: MainThreadMarker,
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (tx, rx) = oneshot::channel();
let tx = Rc::new(RefCell::new(Some(tx)));
let screen_id_to_label = screen_id_to_human_label(marker);
let handler = RcBlock::new(
move |content: *mut SCShareableContent, error: *mut NSError| {
let Some(tx) = tx.borrow_mut().take() else {
return;
};
let result = if let Some(error) = unsafe { error.as_ref() } {
Err(anyhow!(
"Screen share failed: {}",
error.localizedDescription()
))
} else if let Some(content) = unsafe { content.as_ref() } {
let result = unsafe { content.displays() }
.into_iter()
.map(|display| {
let id = unsafe { display.displayID() };
let metadata = screen_id_to_label.get(&id).cloned();
let source = MacScreenCaptureSource {
sc_display: display,
meta: metadata,
};
Rc::new(source) as Rc<dyn ScreenCaptureSource>
})
.collect::<Vec<_>>();
Ok(result)
} else {
Err(anyhow!("Screen share failed"))
};
_ = tx.send(result);
},
);
unsafe {
SCShareableContent::getShareableContentExcludingDesktopWindows_onScreenWindowsOnly_completionHandler(true, true, &handler);
}
rx
}
#[ctor(unsafe)]
unsafe fn build_classes() {
let mut decl = ClassDecl::new("GPUIStreamDelegate", class!(NSObject)).unwrap();
unsafe {
decl.add_method(
sel!(outputVideoEffectDidStartForStream:),
output_video_effect_did_start_for_stream as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(outputVideoEffectDidStopForStream:),
output_video_effect_did_stop_for_stream as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(stream:didStopWithError:),
stream_did_stop_with_error as extern "C" fn(&Object, Sel, id, id),
);
DELEGATE_CLASS = decl.register();
let mut decl = ClassDecl::new("GPUIStreamOutput", class!(NSObject)).unwrap();
decl.add_method(
sel!(stream:didOutputSampleBuffer:ofType:),
stream_did_output_sample_buffer_of_type
as extern "C" fn(&Object, Sel, id, id, NSInteger),
);
decl.add_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR);
OUTPUT_CLASS = decl.register();
}
}
extern "C" fn output_video_effect_did_start_for_stream(_this: &Object, _: Sel, _stream: id) {}
extern "C" fn output_video_effect_did_stop_for_stream(_this: &Object, _: Sel, _stream: id) {}
extern "C" fn stream_did_stop_with_error(_this: &Object, _: Sel, _stream: id, _error: id) {}
extern "C" fn stream_did_output_sample_buffer_of_type(
this: &Object,
_: Sel,
_stream: id,
sample_buffer: id,
buffer_type: NSInteger,
) {
if buffer_type != SCStreamOutputTypeScreen {
return;
}
unsafe {
let sample_buffer = sample_buffer as CMSampleBufferRef;
let sample_buffer = CMSampleBuffer::wrap_under_get_rule(sample_buffer);
if let Some(buffer) = sample_buffer.image_buffer() {
let callback: Box<Box<dyn Fn(ScreenCaptureFrame)>> =
Box::from_raw(*this.get_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR) as *mut _);
callback(ScreenCaptureFrame(buffer));
mem::forget(callback);
}
}
}