| Crates.io | screencapturekit |
| lib.rs | screencapturekit |
| version | 1.5.0 |
| created_at | 2023-08-21 09:04:46.871337+00 |
| updated_at | 2025-12-20 10:33:17.185458+00 |
| description | Safe Rust bindings for Apple's ScreenCaptureKit framework - screen and audio capture on macOS |
| homepage | https://github.com/doom-fish/screencapturekit-rs |
| repository | https://github.com/doom-fish/screencapturekit-rs |
| max_upload_size | |
| id | 949817 |
| size | 1,074,134 |
๐ผ Looking for a hosted desktop recording API?
Check out Recall.ai - an API for recording Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
Safe, idiomatic Rust bindings for Apple's ScreenCaptureKit framework.
Capture screen content, windows, and applications with high performance and low overhead on macOS 12.3+.
๐ฅ Screen & Window Capture - Capture displays, windows, or specific applications
๐ Audio Capture - Capture system audio and microphone input
โก Real-time Processing - High-performance frame callbacks with custom dispatch queues
๐๏ธ Builder Pattern API - Clean, type-safe configuration with ::builder()
๐ Async Support - Runtime-agnostic async API (works with Tokio, async-std, smol, etc.)
๐จ IOSurface Access - Zero-copy GPU texture access for Metal/OpenGL
๐ก๏ธ Memory Safe - Proper reference counting and leak-free by design
๐ฆ Zero Dependencies - No runtime dependencies (only dev dependencies for examples)
https://github.com/user-attachments/assets/8a272c48-7ec3-4132-9111-4602b4fa991d
Add to your Cargo.toml:
[dependencies]
screencapturekit = "1"
For async support:
[dependencies]
screencapturekit = { version = "1", features = ["async"] }
For latest macOS features:
[dependencies]
screencapturekit = { version = "1", features = ["macos_26_0"] }
use screencapturekit::prelude::*;
struct Handler;
impl SCStreamOutputTrait for Handler {
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, _type: SCStreamOutputType) {
println!("๐น Received frame at {:?}", sample.presentation_timestamp());
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get available displays
let content = SCShareableContent::get()?;
let display = &content.displays()[0];
// Configure capture
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080)
.with_pixel_format(PixelFormat::BGRA);
// Start streaming
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(Handler, SCStreamOutputType::Screen);
stream.start_capture()?;
// Capture runs in background...
std::thread::sleep(std::time::Duration::from_secs(5));
stream.stop_capture()?;
Ok(())
}
use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
use screencapturekit::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get content asynchronously
let content = AsyncSCShareableContent::get().await?;
let display = &content.displays()[0];
// Create filter and config
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080);
// Create async stream with frame buffer
let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
stream.start_capture()?;
// Capture frames asynchronously
for _ in 0..10 {
if let Some(frame) = stream.next().await {
println!("๐น Got frame!");
}
}
stream.stop_capture()?;
Ok(())
}
use screencapturekit::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = SCShareableContent::get()?;
// Find a specific window
let windows = content.windows();
let window = windows
.iter()
.find(|w| w.title().as_deref() == Some("Safari"))
.ok_or("Safari window not found")?;
// Capture window with audio
let filter = SCContentFilter::create()
.with_window(window)
.build();
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080)
.with_captures_audio(true)
.with_sample_rate(48000)
.with_channel_count(2);
let mut stream = SCStream::new(&filter, &config);
// Add handlers...
stream.start_capture()?;
Ok(())
}
Use the system picker UI to let users choose what to capture:
use screencapturekit::content_sharing_picker::*;
use screencapturekit::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = SCContentSharingPickerConfiguration::new();
// Show picker - callback receives result when user selects or cancels
SCContentSharingPicker::show(&config, |outcome| {
match outcome {
SCPickerOutcome::Picked(result) => {
// Get dimensions from the picked content
let (width, height) = result.pixel_size();
println!("Selected: {}x{} (scale: {})", width, height, result.scale());
let stream_config = SCStreamConfiguration::new()
.with_width(width)
.with_height(height);
// Get filter for streaming
let filter = result.filter();
let mut stream = SCStream::new(&filter, &stream_config);
// ...
}
SCPickerOutcome::Cancelled => println!("User cancelled"),
SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
}
});
Ok(())
}
Use the async version in async contexts to avoid blocking:
use screencapturekit::async_api::AsyncSCContentSharingPicker;
use screencapturekit::content_sharing_picker::*;
use screencapturekit::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = SCContentSharingPickerConfiguration::new();
// Async picker - doesn't block the executor
match AsyncSCContentSharingPicker::show(&config).await {
SCPickerOutcome::Picked(result) => {
let (width, height) = result.pixel_size();
println!("Selected: {}x{}", width, height);
let filter = result.filter();
// Use filter with stream...
}
SCPickerOutcome::Cancelled => println!("User cancelled"),
SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
}
Ok(())
}
All types use a consistent ::new() with .with_*() chainable methods pattern:
// Stream configuration
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080)
.with_pixel_format(PixelFormat::BGRA)
.with_captures_audio(true);
// Content retrieval options
let content = SCShareableContent::create()
.with_on_screen_windows_only(true)
.with_exclude_desktop_windows(true)
.get()?;
// Content filters
let filter = SCContentFilter::create()
.with_display(&display)
.with_excluding_windows(&windows)
.build();
Control callback threading with custom dispatch queues:
use screencapturekit::prelude::*;
use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().unwrap();
let filter = SCContentFilter::create()
.with_display(&display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new();
let mut stream = SCStream::new(&filter, &config);
let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
stream.add_output_handler_with_queue(
|_sample: CMSampleBuffer, _of_type: SCStreamOutputType| { /* process frame */ },
SCStreamOutputType::Screen,
Some(&queue)
);
Ok(())
}
Quality of Service Levels:
Background - Maintenance tasksUtility - Long-running tasksDefault - Standard priorityUserInitiated - User-initiated tasksUserInteractive - UI updates (highest priority)Zero-copy GPU texture access:
use screencapturekit::prelude::*;
struct Handler;
impl SCStreamOutputTrait for Handler {
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
if let Some(pixel_buffer) = sample.image_buffer() {
if let Some(surface) = pixel_buffer.io_surface() {
let width = surface.width();
let height = surface.height();
// Use with Metal/OpenGL...
println!("IOSurface: {}x{}", width, height);
}
}
}
}
Built-in Metal types for hardware-accelerated rendering without external crates:
use screencapturekit::prelude::*;
use screencapturekit::metal::{
MetalDevice, MetalRenderPassDescriptor, MetalRenderPipelineDescriptor,
MTLLoadAction, MTLStoreAction, MTLPrimitiveType, MTLPixelFormat,
Uniforms, SHADER_SOURCE,
};
// Get the system default Metal device
let device = MetalDevice::system_default().expect("No Metal device");
let command_queue = device.create_command_queue().unwrap();
// Compile built-in shaders (supports BGRA, YCbCr, UI overlays)
let library = device.create_library_with_source(SHADER_SOURCE).unwrap();
// Create render pipeline for textured rendering
let vert_fn = library.get_function("vertex_fullscreen").unwrap();
let frag_fn = library.get_function("fragment_textured").unwrap();
let pipeline_desc = MetalRenderPipelineDescriptor::new();
pipeline_desc.set_vertex_function(&vert_fn);
pipeline_desc.set_fragment_function(&frag_fn);
pipeline_desc.set_color_attachment_pixel_format(0, MTLPixelFormat::BGRA8Unorm);
let _pipeline = device.create_render_pipeline_state(&pipeline_desc).unwrap();
Built-in Shader Functions:
vertex_fullscreen - Aspect-ratio-preserving fullscreen quadfragment_textured - BGRA/L10R single-texture renderingfragment_ycbcr - YCbCr biplanar (420v/420f) to RGB conversionvertex_colored / fragment_colored - UI overlay renderingMetal Types:
MetalDevice, MetalCommandQueue, MetalCommandBufferMetalTexture, MetalBuffer, MetalLayer, MetalDrawableMetalRenderPipelineState, MetalRenderPassDescriptorCapturedTextures<T> - Multi-plane texture container (Y + CbCr for YCbCr formats)| Feature | Description |
|---|---|
async |
Runtime-agnostic async API (works with any executor) |
Feature flags enable APIs for specific macOS versions. They are cumulative (enabling macos_15_0 enables all earlier versions).
| Feature | macOS | APIs Enabled |
|---|---|---|
macos_13_0 |
13.0 Ventura | Audio capture, synchronization clock |
macos_14_0 |
14.0 Sonoma | Content picker, screenshots, content info |
macos_14_2 |
14.2 | Menu bar capture, child windows, presenter overlay |
macos_14_4 |
14.4 | Current process shareable content |
macos_15_0 |
15.0 Sequoia | Recording output, HDR capture, microphone |
macos_15_2 |
15.2 | Screenshot in rect, stream active/inactive delegates |
macos_26_0 |
26.0 | Advanced screenshot config, HDR screenshot output |
let mut config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080);
#[cfg(feature = "macos_13_0")]
config.set_should_be_opaque(true);
#[cfg(feature = "macos_14_2")]
{
config.set_ignores_shadows_single_window(true);
config.set_includes_child_windows(false);
}
| Type | Description |
|---|---|
SCShareableContent |
Query available displays, windows, and applications |
SCContentFilter |
Define what to capture (display/window/app) |
SCStreamConfiguration |
Configure resolution, format, audio, etc. |
SCStream |
Main capture stream with output handlers |
CMSampleBuffer |
Frame data with timing and metadata |
async feature)| Type | Description |
|---|---|
AsyncSCShareableContent |
Async content queries |
AsyncSCStream |
Async stream with frame iteration |
AsyncSCScreenshotManager |
Async screenshot capture (macOS 14.0+) |
AsyncSCContentSharingPicker |
Async content picker UI (macOS 14.0+) |
| Type | Description |
|---|---|
SCDisplay |
Display information (resolution, ID, frame) |
SCWindow |
Window information (title, bounds, owner, layer) |
SCRunningApplication |
Application information (name, bundle ID, PID) |
| Type | Description |
|---|---|
CMSampleBuffer |
Sample buffer with timing and attachments |
CMTime |
High-precision timestamps with timescale |
IOSurface |
GPU-backed pixel buffers for zero-copy access |
CGImage |
Core Graphics images for screenshots |
CVPixelBuffer |
Core Video pixel buffer with lock guards |
metal module)| Type | Description |
|---|---|
MetalDevice |
Metal GPU device wrapper |
MetalTexture |
Metal texture with automatic retain/release |
MetalBuffer |
Vertex/uniform buffer |
MetalCommandQueue / MetalCommandBuffer |
Command submission |
MetalLayer |
CAMetalLayer for window rendering |
MetalRenderPipelineState |
Compiled render pipeline |
CapturedTextures<T> |
Multi-plane texture container (Y + CbCr for YCbCr) |
Uniforms |
Shader uniform structure matching SHADER_SOURCE |
| Type | Description |
|---|---|
PixelFormat |
BGRA, YCbCr420v, YCbCr420f, l10r (10-bit) |
SCPresenterOverlayAlertSetting |
Privacy alert behavior |
SCCaptureDynamicRange |
HDR/SDR modes (macOS 15.0+) |
SCScreenshotConfiguration |
Advanced screenshot config (macOS 26.0+) |
SCScreenshotDynamicRange |
SDR/HDR screenshot output (macOS 26.0+) |
The examples/ directory contains focused API demonstrations:
01_basic_capture.rs - Simplest screen capture02_window_capture.rs - Capture specific windows03_audio_capture.rs - Audio + video capture04_pixel_access.rs - Read pixel data with std::io::Cursor05_screenshot.rs - Single screenshot, HDR capture (macOS 14.0+, 26.0+)06_iosurface.rs - Zero-copy GPU buffers07_list_content.rs - List available content08_async.rs - Async/await API with multiple examples09_closure_handlers.rs - Closure-based handlers and delegates10_recording_output.rs - Direct video file recording (macOS 15.0+)11_content_picker.rs - System UI for content selection (macOS 14.0+)12_stream_updates.rs - Dynamic config/filter updates13_advanced_config.rs - HDR, presets, microphone (macOS 15.0+)14_app_capture.rs - Application-based filtering15_memory_leak_check.rs - Memory leak detection with leaks16_full_metal_app/ - Full Metal GUI application (macOS 14.0+)17_metal_textures.rs - Metal texture creation from IOSurface18_wgpu_integration.rs - Zero-copy wgpu integration19_ffmpeg_encoding.rs - Real-time H.264 encoding via FFmpeg20_egui_viewer.rs - egui screen viewer integration21_bevy_streaming.rs - Bevy texture streaming22_tauri_app/ - Tauri 2.0 desktop app with WebGL (macOS 14.0+)23_client_server/ - Client/server screen sharingSee examples/README.md for detailed descriptions.
Run an example:
# Basic examples
cargo run --example 01_basic_capture
cargo run --example 09_closure_handlers
cargo run --example 12_stream_updates
cargo run --example 14_app_capture
cargo run --example 17_metal_textures
cargo run --example 18_wgpu_integration
cargo run --example 19_ffmpeg_encoding # Requires: brew install ffmpeg
cargo run --example 20_egui_viewer
cargo run --example 21_bevy_streaming
# Feature-gated examples
cargo run --example 05_screenshot --features macos_14_0
cargo run --example 08_async --features async
cargo run --example 10_recording_output --features macos_15_0
cargo run --example 11_content_picker --features macos_14_0
cargo run --example 13_advanced_config --features macos_15_0
cargo run --example 16_full_metal_app --features macos_14_0
# Tauri app (separate project)
cd examples/22_tauri_app && npm install && npm run tauri dev
# Client/server screen sharing
cargo run --example 23_client_server_server # Terminal 1
cargo run --example 23_client_server_client # Terminal 2
# All tests
cargo test
# With features
cargo test --features async
cargo test --all-features
# Specific test
cargo test test_stream_configuration
cargo clippy --all-features -- -D warnings
cargo fmt --check
screencapturekit/
โโโ cm/ # Core Media (CMSampleBuffer, CMTime, IOSurface)
โโโ cv/ # Core Video (CVPixelBuffer, CVPixelBufferPool)
โโโ cg/ # Core Graphics (CGRect, CGPoint, CGSize)
โโโ metal/ # Metal GPU integration (textures, shaders)
โโโ stream/ # Stream management
โ โโโ configuration/ # SCStreamConfiguration
โ โโโ content_filter/ # SCContentFilter
โ โโโ sc_stream/ # SCStream
โโโ shareable_content/ # SCShareableContent, SCDisplay, SCWindow
โโโ dispatch_queue/ # Custom dispatch queues
โโโ error/ # Error types
โโโ screenshot_manager/ # SCScreenshotManager (macOS 14.0+)
โโโ content_sharing_picker/ # SCContentSharingPicker (macOS 14.0+)
โโโ recording_output/ # SCRecordingOutput (macOS 15.0+)
โโโ async_api/ # Async wrappers (feature = "async")
โโโ utils/ # FFI strings, FourCharCode utilities
โโโ prelude/ # Convenience re-exports
Problem: SCShareableContent::get() returns an error or empty lists.
Solution: Grant screen recording permission:
For development, you may need to add Terminal.app to the allowed list.
Problem: App crashes or permissions fail after notarization.
Solution: Add required entitlements to your entitlements.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.screen-capture</key>
<true/>
</dict>
</plist>
Problem: Frames are received but contain no visible content.
Solutions:
pixel_format matches your processing expectationsProblem: Audio samples not received or empty.
Solutions:
.with_captures_audio(true)stream.add_output_handler(handler, SCStreamOutputType::Audio)sample_rate and channel_count are set correctlyProblem: Compilation fails with Swift bridge errors.
Solutions:
xcode-select --installcargo clean && cargo buildScreenCaptureKit supportScreen recording requires explicit user permission. For development:
For distribution:
NSScreenCaptureUsageDescription to your Info.plistRun benchmarks to measure performance on your hardware:
cargo bench
See docs/BENCHMARKS.md for detailed benchmark documentation including:
IOSurface access patterns| Resolution | Expected FPS | First Frame Latency |
|---|---|---|
| 1080p | 30-60 FPS | 30-100ms |
| 4K | 15-30 FPS | 50-150ms |
Upgrading from an older version? See docs/MIGRATION.md for:
Contributions welcome! Please:
::new() and .with_*() methods)cargo test and cargo clippyThis crate is used by some amazing projects:
Using screencapturekit-rs? Let us know and we'll add you to the list!
Thanks to everyone who has contributed to this project!
Licensed under either of:
at your option.