gpui_macos: Migrate screen capture to objc2 (#64251)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_remote_server_linux_x86_64 (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_remote_server_mac_aarch64 (push) Blocked by required conditions
run_tests / check_remote_server_windows_x86_64 (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions

# Objective

Get rid of instances of the `block` crate, as it will not compile in a
future version of Rust. This gets rid of the `block` crate inside of
`gpui_macos`, which means were on the home stretch.

## Solution

Migrate `screen_capture.rs` completely to `objc2`. While a more targeted
fix is theoretically possible, there were enough upsides where I thought
this was worth a full migration.

Completely gets rid of the `media` crate, which had manual bindings to
`core_media`, which `objc2` can provide. Gets rid of tons of unsafe
code, and a lot of code in general.

## Testing

Tested manual sharing inside of a Zed channel

Release Notes:

- N/A
- [GPUI] Migrated macOS screen capture to `objc2`
This commit is contained in:
tidely 2026-09-17 01:46:02 +00:00 • committed by GitHub
parent 7a01ac15c5
commit 4b47ceb9d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 186 additions and 652 deletions

51
Cargo.lock generated
View file

@ -7533,6 +7533,8 @@ dependencies = [
"lyon",
"num_cpus",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-video",
"objc2-metal 0.3.2",
"parking",
"parking_lot",
@ -7651,7 +7653,6 @@ dependencies = [
"accesskit_macos",
"anyhow",
"async-task",
"block",
"block2 0.6.2",
"cocoa 0.26.0",
"collections",
@ -7671,11 +7672,11 @@ dependencies = [
"libc",
"log",
"mach2 0.5.0",
"media",
"metal",
"objc",
"objc2 0.6.3",
"objc2-app-kit 0.3.2",
"objc2-core-graphics",
"objc2-core-media",
"objc2-foundation 0.3.2",
"objc2-screen-capture-kit",
"objc2-user-notifications",
@ -10306,6 +10307,7 @@ dependencies = [
"livekit_api",
"log",
"nanoid",
"objc2-core-foundation",
"parking_lot",
"postage",
"rodio",
@ -10863,19 +10865,6 @@ dependencies = [
"warp",
]
[[package]]
name = "media"
version = "0.1.0"
dependencies = [
"anyhow",
"bindgen",
"core-foundation 0.10.0",
"core-video",
"foreign-types 0.5.0",
"metal",
"objc",
]
[[package]]
name = "memchr"
version = "2.8.3"
@ -12048,6 +12037,21 @@ dependencies = [
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-core-media"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d"
dependencies = [
"bitflags 2.13.1",
"dispatch2",
"objc2 0.6.3",
"objc2-core-audio",
"objc2-core-audio-types",
"objc2-core-foundation",
"objc2-core-video",
]
[[package]]
name = "objc2-core-services"
version = "0.3.2"
@ -12058,6 +12062,19 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-core-video"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
dependencies = [
"bitflags 2.13.1",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-io-surface",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
@ -12180,8 +12197,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394"
dependencies = [
"block2 0.6.2",
"dispatch2",
"objc2 0.6.3",
"objc2-core-graphics",
"objc2-core-media",
"objc2-foundation 0.3.2",
]

View file

@ -136,7 +136,6 @@ members = [
"crates/lsp_locations",
"crates/markdown",
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/mermaid_render",
"crates/migrator",
@ -405,7 +404,6 @@ lsp_command_selector = { path = "crates/lsp_command_selector" }
lsp_locations = { path = "crates/lsp_locations" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
mermaid_render = { path = "crates/mermaid_render" }
migrator = { path = "crates/migrator" }
@ -717,6 +715,10 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [
"block2",
"objc2-core-foundation",
] }
objc2-core-foundation = { version = "=0.3.2", default-features = false }
objc2-core-graphics = { version = "=0.3.2", default-features = false, features = ["CGDirectDisplay"] }
objc2-core-media = { version = "=0.3.2", default-features = false, features = ["CMSampleBuffer", "objc2-core-video"] }
objc2-core-video = { version = "=0.3.2", default-features = false, features = ["CVBuffer", "CVImageBuffer"] }
objc2-foundation = { version = "=0.3.2", default-features = false, features = [
"NSArray",
"NSAttributedString",
@ -745,8 +747,11 @@ objc2-foundation = { version = "=0.3.2", default-features = false, features = [
] }
objc2-screen-capture-kit = { version = "=0.3.2", default-features = false, features = [
"block2",
"dispatch2",
"objc2-core-graphics",
"objc2-core-media",
"SCShareableContent",
"SCStream",
"std"
] }
objc2-user-notifications = "0.3"

View file

@ -63,7 +63,6 @@ profiles = ["release"]
# Own non-.rs files read at build time (protobufs, bindgen headers, doc/data).
proto = ["proto/**"]
livekit_api = ["vendored/protocol/**"]
media = ["src/bindings.h"]
gpui = [
"README.md",
"examples/image/exif-orientation-rotate-180.jpg",

View file

@ -120,6 +120,8 @@ uuid = { workspace = true, features = ["js"] }
[target.'cfg(target_os = "macos")'.dependencies]
core-video.workspace = true
objc2-core-foundation.workspace = true
objc2-core-video.workspace = true
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "94b0f28166665e8fd2f53ff6d268a14955c82269", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
log.workspace = true

View file

@ -32,7 +32,8 @@ pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
#[cfg(not(feature = "screen-capture"))]
pub(crate) type PlatformScreenCaptureFrame = ();
#[cfg(all(target_os = "macos", feature = "screen-capture"))]
pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
pub(crate) type PlatformScreenCaptureFrame =
objc2_core_foundation::CFRetained<objc2_core_video::CVImageBuffer>;
use crate::{
Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,

View file

@ -27,7 +27,6 @@ accesskit.workspace = true
accesskit_macos.workspace = true
anyhow.workspace = true
async-task.workspace = true
block.workspace = true
block2.workspace = true
cocoa.workspace = true
collections.workspace = true
@ -48,11 +47,11 @@ itertools.workspace = true
libc.workspace = true
log.workspace = true
mach2.workspace = true
media.workspace = true
metal.workspace = true
objc.workspace = true
objc2.workspace = true
objc2-app-kit.workspace = true
objc2-core-graphics.workspace = true
objc2-core-media.workspace = true
objc2-foundation.workspace = true
objc2-screen-capture-kit.workspace = true
objc2-user-notifications.workspace = true

View file

@ -1,36 +1,25 @@
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::{
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::{
AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send,
rc::Retained,
runtime::{NSObject, ProtocolObject},
};
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};
use objc2_core_graphics::{CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayMode};
use objc2_core_media::CMSampleBuffer;
use objc2_foundation::{NSArray, NSError, NSNumber, NSObjectProtocol, NSString};
use objc2_screen_capture_kit::{
SCContentFilter, SCDisplay, SCShareableContent, SCStream, SCStreamConfiguration,
SCStreamOutput, SCStreamOutputType, SCWindow,
};
use std::{cell::RefCell, rc::Rc};
#[derive(Clone)]
pub struct MacScreenCaptureSource {
@ -39,49 +28,36 @@ pub struct MacScreenCaptureSource {
}
pub struct MacScreenCaptureStream {
sc_stream: id,
sc_stream_output: id,
sc_stream: Retained<SCStream>,
sc_stream_output: Retained<StreamOutput>,
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 {
// SAFETY: `SCDisplay` is an Objective-C object, so objc2's object pointer has the
// same ABI as objc 0.2's `id`. This only casts a borrowed pointer; it does not
// transfer ownership or change the retain count. `self.sc_display` remains alive
// for the entire message send, and `displayID` does not retain the receiver.
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)),
)
};
impl MacScreenCaptureSource {
fn metadata(&self) -> SourceMetadata {
let display_id = unsafe { self.sc_display.displayID() };
let display_mode = CGDisplayCopyDisplayMode(display_id);
let width = CGDisplayMode::pixel_width(display_mode.as_deref());
let height = CGDisplayMode::pixel_height(display_mode.as_deref());
let size = 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 {
SourceMetadata {
id: display_id as u64,
label,
is_main,
resolution: size,
})
}
}
}
impl ScreenCaptureSource for MacScreenCaptureSource {
fn metadata(&self) -> Result<SourceMetadata> {
Ok(self.metadata())
}
fn stream(
@ -89,86 +65,68 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
_foreground_executor: &ForegroundExecutor,
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>> {
let (sender, receiver) = oneshot::channel();
let metadata = self.metadata();
unsafe {
// SAFETY: `SCDisplay` is an Objective-C object, so objc2's object pointer has the
// same ABI as objc 0.2's `id`. The cast removes pointer constness only because the
// legacy `id` alias is mutable; it does not mutate the display, transfer ownership,
// or change the retain count. The generated objc2 binding takes `display` as
// `&SCDisplay`, confirming that this initializer borrows rather than consumes it.
// `self.sc_display` remains alive through the call, and `SCContentFilter` must retain
// the display or copy any state that it needs after initialization.
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 _: id = msg_send![configuration, setShowsCursor: false];
// let _: id = msg_send![configuration, setCaptureResolution: 3];
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 excluded_windows = NSArray::<SCWindow>::new();
let filter = SCContentFilter::initWithDisplay_excludingWindows(
SCContentFilter::alloc(),
&self.sc_display,
&excluded_windows,
);
let configuration = SCStreamConfiguration::new();
configuration.setScalesToFit(true);
configuration.setPixelFormat(0x42475241);
configuration.setWidth(metadata.resolution.width.0 as usize);
configuration.setHeight(metadata.resolution.height.0 as usize);
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 stream = SCStream::initWithFilter_configuration_delegate(
SCStream::alloc(),
&filter,
&configuration,
None,
);
let output = StreamOutput::new(frame_callback);
let output_protocol = ProtocolObject::from_ref(&*output);
// Stream contains filter, configuration, and delegate internally so we release them here
// to prevent a memory leak when steam is dropped
let _: () = msg_send![filter, release];
let _: () = msg_send![configuration, release];
let _: () = msg_send![delegate, release];
if let Err(error) = stream.addStreamOutput_type_sampleHandlerQueue_error(
output_protocol,
SCStreamOutputType::Screen,
None,
) {
_ = sender.send(Err(anyhow!(
"failed to add stream output: {}",
error.localizedDescription()
)));
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;
return receiver;
}
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 state = Rc::new(RefCell::new(Some((sender, stream.clone(), output))));
let handler = RcBlock::new(move |error: *mut NSError| {
let Some((sender, stream, output)) = state.borrow_mut().take() else {
return;
};
let result = if let Some(error) = error.as_ref() {
Err(anyhow!(
"failed to start screen capture stream: {}",
error.localizedDescription()
))
} else {
Ok(Box::new(MacScreenCaptureStream {
meta: metadata.clone(),
sc_stream: stream,
sc_stream_output: output,
}) as Box<dyn ScreenCaptureStream>)
};
_ = sender.send(result);
});
let handler = handler.copy();
let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler];
rx
stream.startCaptureWithCompletionHandler(Some(&handler));
}
receiver
}
}
@ -181,23 +139,27 @@ impl ScreenCaptureStream for MacScreenCaptureStream {
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 output = ProtocolObject::from_ref(&*self.sc_stream_output);
if let Err(error) = self
.sc_stream
.removeStreamOutput_type_error(output, SCStreamOutputType::Screen)
{
log::error!(
"failed to remove screen capture stream output: {}",
error.localizedDescription()
);
}
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 handler = RcBlock::new(move |error: *mut NSError| {
if let Some(error) = error.as_ref() {
log::error!(
"failed to stop screen capture stream: {}",
error.localizedDescription()
);
}
});
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];
self.sc_stream
.stopCaptureWithCompletionHandler(Some(&handler));
}
}
}
@ -288,61 +250,42 @@ pub(crate) fn get_sources(
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();
}
struct StreamOutputIvars {
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
}
extern "C" fn output_video_effect_did_start_for_stream(_this: &Object, _: Sel, _stream: id) {}
define_class!(
// SAFETY: `NSObject` has no subclassing requirements, and `StreamOutput` does not
// implement `Drop`; `define_class!` manages destruction of its ivars.
#[unsafe(super(NSObject))]
#[ivars = StreamOutputIvars]
struct StreamOutput;
extern "C" fn output_video_effect_did_stop_for_stream(_this: &Object, _: Sel, _stream: id) {}
unsafe impl NSObjectProtocol for StreamOutput {}
extern "C" fn stream_did_stop_with_error(_this: &Object, _: Sel, _stream: id, _error: id) {}
unsafe impl SCStreamOutput for StreamOutput {
#[unsafe(method(stream:didOutputSampleBuffer:ofType:))]
fn stream_did_output_sample_buffer_of_type(
&self,
_stream: &SCStream,
sample_buffer: &CMSampleBuffer,
buffer_type: SCStreamOutputType,
) {
if buffer_type != SCStreamOutputType::Screen {
return;
}
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);
let Some(image_buffer) = (unsafe { sample_buffer.image_buffer() }) else {
return;
};
(self.ivars().frame_callback)(ScreenCaptureFrame(image_buffer));
}
}
);
impl StreamOutput {
fn new(frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>) -> Retained<Self> {
let this = Self::alloc().set_ivars(StreamOutputIvars { frame_callback });
unsafe { msg_send![super(this), init] }
}
}

View file

@ -57,6 +57,7 @@ scap.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-video.workspace = true
objc2-core-foundation.workspace = true
coreaudio-rs = "0.12.1"
[dev-dependencies]

View file

@ -753,11 +753,15 @@ fn video_frame_buffer_from_webrtc(buffer: Box<dyn VideoBuffer>) -> Option<Remote
#[cfg(target_os = "macos")]
fn video_frame_buffer_to_webrtc(frame: ScreenCaptureFrame) -> Option<impl AsRef<dyn VideoBuffer>> {
use livekit::webrtc;
use objc2_core_foundation::CFRetained;
let pixel_buffer = frame.0.as_concrete_TypeRef();
std::mem::forget(frame.0);
let pixel_buffer = CFRetained::into_raw(frame.0);
unsafe {
Some(webrtc::video_frame::native::NativeBuffer::from_cv_pixel_buffer(pixel_buffer as _))
Some(
webrtc::video_frame::native::NativeBuffer::from_cv_pixel_buffer(
pixel_buffer.as_ptr().cast(),
),
)
}
}

View file

@ -1,27 +0,0 @@
[package]
name = "media"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "Bindings to macos media handling APIs for Zed"
[lints]
workspace = true
[lib]
path = "src/media.rs"
doctest = false
[dependencies]
anyhow.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
foreign-types.workspace = true
metal.workspace = true
core-video.workspace = true
objc.workspace = true
[build-dependencies]
bindgen = "0.72"

View file

@ -1 +0,0 @@
../../LICENSE-APACHE

View file

@ -1,44 +0,0 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
#[cfg(target_os = "macos")]
fn main() {
use std::{env, path::PathBuf, process::Command};
let sdk_path = String::from_utf8(
Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.unwrap()
.stdout,
)
.unwrap();
let sdk_path = sdk_path.trim_end();
println!("cargo:rerun-if-changed=src/bindings.h");
let bindings = bindgen::Builder::default()
.header("src/bindings.h")
.clang_arg(format!("-isysroot{}", sdk_path))
.clang_arg("-xobjective-c")
.allowlist_type("CMItemIndex")
.allowlist_type("CMSampleTimingInfo")
.allowlist_type("CMVideoCodecType")
.allowlist_type("VTEncodeInfoFlags")
.allowlist_function("CMTimeMake")
.allowlist_var("kCVPixelFormatType_.*")
.allowlist_var("kCVReturn.*")
.allowlist_var("VTEncodeInfoFlags_.*")
.allowlist_var("kCMVideoCodecType_.*")
.allowlist_var("kCMTime.*")
.allowlist_var("kCMSampleAttachmentKey_.*")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.layout_tests(false)
.generate()
.expect("unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("couldn't write dispatch bindings");
}
#[cfg(not(target_os = "macos"))]
fn main() {}

View file

@ -1,5 +0,0 @@
#import <CoreMedia/CMFormatDescription.h>
#import <CoreMedia/CMSampleBuffer.h>
#import <CoreVideo/CVPixelFormatDescription.h>
#import <CoreVideo/CVReturn.h>
#import <VideoToolbox/VTCompressionSession.h>

View file

@ -1,10 +0,0 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(unused)]
#[cfg(target_os = "macos")]
use objc::*;
#[cfg(target_os = "macos")]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

View file

@ -1,352 +0,0 @@
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
mod bindings;
#[cfg(target_os = "macos")]
pub mod core_media {
#![allow(non_snake_case)]
pub use crate::bindings::{
CMItemIndex, CMSampleTimingInfo, CMTime, CMTimeMake, CMVideoCodecType,
kCMSampleAttachmentKey_NotSync, kCMTimeInvalid, kCMVideoCodecType_H264,
};
use anyhow::Result;
use core_foundation::{
array::{CFArray, CFArrayRef},
base::{CFTypeID, OSStatus, TCFType},
declare_TCFType,
dictionary::CFDictionary,
impl_CFTypeDescription, impl_TCFType,
string::CFString,
};
use core_video::image_buffer::{CVImageBuffer, CVImageBufferRef};
use std::{ffi::c_void, ptr};
#[repr(C)]
pub struct __CMSampleBuffer(c_void);
// The ref type must be a pointer to the underlying struct.
pub type CMSampleBufferRef = *const __CMSampleBuffer;
declare_TCFType!(CMSampleBuffer, CMSampleBufferRef);
impl_TCFType!(CMSampleBuffer, CMSampleBufferRef, CMSampleBufferGetTypeID);
impl_CFTypeDescription!(CMSampleBuffer);
impl CMSampleBuffer {
pub fn attachments(&self) -> Vec<CFDictionary<CFString>> {
unsafe {
let attachments =
CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true);
CFArray::<CFDictionary>::wrap_under_get_rule(attachments)
.into_iter()
.map(|attachments| {
CFDictionary::wrap_under_get_rule(attachments.as_concrete_TypeRef())
})
.collect()
}
}
pub fn image_buffer(&self) -> Option<CVImageBuffer> {
unsafe {
let ptr = CMSampleBufferGetImageBuffer(self.as_concrete_TypeRef());
if ptr.is_null() {
None
} else {
Some(CVImageBuffer::wrap_under_get_rule(ptr))
}
}
}
pub fn sample_timing_info(&self, index: usize) -> Result<CMSampleTimingInfo> {
unsafe {
let mut timing_info = CMSampleTimingInfo {
duration: kCMTimeInvalid,
presentationTimeStamp: kCMTimeInvalid,
decodeTimeStamp: kCMTimeInvalid,
};
let result = CMSampleBufferGetSampleTimingInfo(
self.as_concrete_TypeRef(),
index as CMItemIndex,
&mut timing_info,
);
anyhow::ensure!(
result == 0,
"error getting sample timing info, code {result}"
);
Ok(timing_info)
}
}
pub fn format_description(&self) -> CMFormatDescription {
unsafe {
CMFormatDescription::wrap_under_get_rule(CMSampleBufferGetFormatDescription(
self.as_concrete_TypeRef(),
))
}
}
pub fn data(&self) -> CMBlockBuffer {
unsafe {
CMBlockBuffer::wrap_under_get_rule(CMSampleBufferGetDataBuffer(
self.as_concrete_TypeRef(),
))
}
}
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMSampleBufferGetTypeID() -> CFTypeID;
fn CMSampleBufferGetSampleAttachmentsArray(
buffer: CMSampleBufferRef,
create_if_necessary: bool,
) -> CFArrayRef;
fn CMSampleBufferGetImageBuffer(buffer: CMSampleBufferRef) -> CVImageBufferRef;
fn CMSampleBufferGetSampleTimingInfo(
buffer: CMSampleBufferRef,
index: CMItemIndex,
timing_info_out: *mut CMSampleTimingInfo,
) -> OSStatus;
fn CMSampleBufferGetFormatDescription(buffer: CMSampleBufferRef) -> CMFormatDescriptionRef;
fn CMSampleBufferGetDataBuffer(sample_buffer: CMSampleBufferRef) -> CMBlockBufferRef;
}
#[repr(C)]
pub struct __CMFormatDescription(c_void);
pub type CMFormatDescriptionRef = *const __CMFormatDescription;
declare_TCFType!(CMFormatDescription, CMFormatDescriptionRef);
impl_TCFType!(
CMFormatDescription,
CMFormatDescriptionRef,
CMFormatDescriptionGetTypeID
);
impl_CFTypeDescription!(CMFormatDescription);
impl CMFormatDescription {
pub fn h264_parameter_set_count(&self) -> usize {
unsafe {
let mut count = 0;
let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
self.as_concrete_TypeRef(),
0,
ptr::null_mut(),
ptr::null_mut(),
&mut count,
ptr::null_mut(),
);
assert_eq!(result, 0);
count
}
}
pub fn h264_parameter_set_at_index(&self, index: usize) -> Result<&[u8]> {
unsafe {
let mut bytes = ptr::null();
let mut len = 0;
let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
self.as_concrete_TypeRef(),
index,
&mut bytes,
&mut len,
ptr::null_mut(),
ptr::null_mut(),
);
anyhow::ensure!(result == 0, "error getting parameter set, code: {result}");
Ok(std::slice::from_raw_parts(bytes, len))
}
}
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMFormatDescriptionGetTypeID() -> CFTypeID;
fn CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
video_desc: CMFormatDescriptionRef,
parameter_set_index: usize,
parameter_set_pointer_out: *mut *const u8,
parameter_set_size_out: *mut usize,
parameter_set_count_out: *mut usize,
NALUnitHeaderLengthOut: *mut isize,
) -> OSStatus;
}
#[repr(C)]
pub struct __CMBlockBuffer(c_void);
pub type CMBlockBufferRef = *const __CMBlockBuffer;
declare_TCFType!(CMBlockBuffer, CMBlockBufferRef);
impl_TCFType!(CMBlockBuffer, CMBlockBufferRef, CMBlockBufferGetTypeID);
impl_CFTypeDescription!(CMBlockBuffer);
impl CMBlockBuffer {
pub fn bytes(&self) -> &[u8] {
unsafe {
let mut bytes = ptr::null();
let mut len = 0;
let result = CMBlockBufferGetDataPointer(
self.as_concrete_TypeRef(),
0,
&mut 0,
&mut len,
&mut bytes,
);
assert!(result == 0, "could not get block buffer data");
std::slice::from_raw_parts(bytes, len)
}
}
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMBlockBufferGetTypeID() -> CFTypeID;
fn CMBlockBufferGetDataPointer(
buffer: CMBlockBufferRef,
offset: usize,
length_at_offset_out: *mut usize,
total_length_out: *mut usize,
data_pointer_out: *mut *const u8,
) -> OSStatus;
}
}
#[cfg(target_os = "macos")]
pub mod core_video {
#![allow(non_snake_case)]
#[cfg(target_os = "macos")]
use core_foundation::{
base::{CFTypeID, TCFType},
declare_TCFType, impl_CFTypeDescription, impl_TCFType,
};
#[cfg(target_os = "macos")]
use std::ffi::c_void;
use crate::bindings::{CVReturn, kCVReturnSuccess};
pub use crate::bindings::{
kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
};
use anyhow::Result;
use core_foundation::{
base::kCFAllocatorDefault, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef,
};
use foreign_types::ForeignTypeRef;
use metal::{MTLDevice, MTLPixelFormat};
use std::ptr;
#[repr(C)]
pub struct __CVMetalTextureCache(c_void);
pub type CVMetalTextureCacheRef = *const __CVMetalTextureCache;
declare_TCFType!(CVMetalTextureCache, CVMetalTextureCacheRef);
impl_TCFType!(
CVMetalTextureCache,
CVMetalTextureCacheRef,
CVMetalTextureCacheGetTypeID
);
impl_CFTypeDescription!(CVMetalTextureCache);
impl CVMetalTextureCache {
/// # Safety
///
/// metal_device must be valid according to CVMetalTextureCacheCreate
pub unsafe fn new(metal_device: *mut MTLDevice) -> Result<Self> {
let mut this = ptr::null();
let result = unsafe {
CVMetalTextureCacheCreate(
kCFAllocatorDefault,
ptr::null(),
metal_device,
ptr::null(),
&mut this,
)
};
anyhow::ensure!(
result == kCVReturnSuccess,
"could not create texture cache, code: {result}"
);
unsafe { Ok(CVMetalTextureCache::wrap_under_create_rule(this)) }
}
/// # Safety
///
/// The arguments to this function must be valid according to CVMetalTextureCacheCreateTextureFromImage
pub unsafe fn create_texture_from_image(
&self,
source: ::core_video::image_buffer::CVImageBufferRef,
texture_attributes: CFDictionaryRef,
pixel_format: MTLPixelFormat,
width: usize,
height: usize,
plane_index: usize,
) -> Result<CVMetalTexture> {
let mut this = ptr::null();
let result = unsafe {
CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault,
self.as_concrete_TypeRef(),
source,
texture_attributes,
pixel_format,
width,
height,
plane_index,
&mut this,
)
};
anyhow::ensure!(
result == kCVReturnSuccess,
"could not create texture, code: {result}"
);
unsafe { Ok(CVMetalTexture::wrap_under_create_rule(this)) }
}
}
#[link(name = "CoreVideo", kind = "framework")]
unsafe extern "C" {
fn CVMetalTextureCacheGetTypeID() -> CFTypeID;
fn CVMetalTextureCacheCreate(
allocator: CFAllocatorRef,
cache_attributes: CFDictionaryRef,
metal_device: *const MTLDevice,
texture_attributes: CFDictionaryRef,
cache_out: *mut CVMetalTextureCacheRef,
) -> CVReturn;
fn CVMetalTextureCacheCreateTextureFromImage(
allocator: CFAllocatorRef,
texture_cache: CVMetalTextureCacheRef,
source_image: ::core_video::image_buffer::CVImageBufferRef,
texture_attributes: CFDictionaryRef,
pixel_format: MTLPixelFormat,
width: usize,
height: usize,
plane_index: usize,
texture_out: *mut CVMetalTextureRef,
) -> CVReturn;
}
#[repr(C)]
pub struct __CVMetalTexture(c_void);
pub type CVMetalTextureRef = *const __CVMetalTexture;
declare_TCFType!(CVMetalTexture, CVMetalTextureRef);
impl_TCFType!(CVMetalTexture, CVMetalTextureRef, CVMetalTextureGetTypeID);
impl_CFTypeDescription!(CVMetalTexture);
impl CVMetalTexture {
pub fn as_texture_ref(&self) -> &metal::TextureRef {
unsafe {
let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
metal::TextureRef::from_ptr(texture as *mut _)
}
}
}
#[link(name = "CoreVideo", kind = "framework")]
unsafe extern "C" {
fn CVMetalTextureGetTypeID() -> CFTypeID;
fn CVMetalTextureGetTexture(texture: CVMetalTextureRef) -> *mut c_void;
}
}