Expand description
ScreenCaptureKit-rs
Safe, idiomatic Rust bindings for Apple's ScreenCaptureKit framework.
Capture screens, windows, and applications on macOS 12.3+ with high performance and low overhead.
💼 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.
https://github.com/user-attachments/assets/8a272c48-7ec3-4132-9111-4602b4fa991d
§Highlights
- 🎥 Screen, window, and app capture with a clean builder-pattern API
- 🔊 System audio + microphone capture (macOS 13.0+ / 15.0+)
- ⚡ Real-time, zero-copy frame delivery via
IOSurface/ Metal - 🔄 Async support that works with any executor (Tokio, async-std, smol, …)
- 📸 Screenshots + direct-to-file recording (macOS 14.0+ / 15.0+)
- 🖱️ System content picker UI (macOS 14.0+)
- 🛡️ Memory safe — proper retain/release, leak-tested
- 📦 Zero runtime dependencies
§Table of Contents
- Install · Quick Start · Recipes
- Feature Flags · Examples · Documentation
- Requirements & Permissions · Performance
- Troubleshooting · Migration · Contributing
§Install
[dependencies]
screencapturekit = "2"Opt-in features (additive):
| Feature | Enables |
|---|---|
async | Runtime-agnostic async API (Tokio / async-std / smol / …) |
macos_13_0 | Audio capture, sync clock |
macos_14_0 | Screenshots, content picker, content info |
macos_14_2 | Menu bar capture, child windows, presenter overlay |
macos_14_4 | Current-process shareable content |
macos_15_0 | Recording output, HDR capture, microphone |
macos_15_2 | Screenshot in rect, stream active/inactive delegates |
macos_26_0 | Advanced screenshot config, HDR screenshot output |
macos_* features are cumulative — enabling macos_15_0 automatically enables every earlier version. Pick the highest version your minimum-supported macOS will satisfy:
screencapturekit = { version = "2", features = ["async", "macos_15_0"] }Upgrading from 1.x? See
docs/MIGRATION.md— the headline 2.0 changes are aSend + Syncbound on output / delegate traits,#[non_exhaustive]onPixelFormatandSCStreamErrorCode, and a newPixelFormat::Unknown(FourCharCode)variant.
§Quick Start
A minimal screen capture in ~25 lines. Everything else builds on these four steps: (1) list shareable content, (2) build a content filter, (3) configure the stream, (4) add an output handler and start.
use screencapturekit::prelude::*;
struct Handler;
impl SCStreamOutputTrait for Handler {
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, _: SCStreamOutputType) {
println!("📹 frame @ {:?}", sample.presentation_timestamp());
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = SCShareableContent::get()?;
let display = &content.displays()[0];
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);
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(Handler, SCStreamOutputType::Screen);
stream.start_capture()?;
std::thread::sleep(std::time::Duration::from_secs(5));
stream.stop_capture()?;
Ok(())
}Output / delegate handlers must be
Send + Sync— Apple’s dispatch queues may invoke them concurrently from arbitrary threads.
Permission required — see Requirements & Permissions.
Run it: cargo run --example 01_basic_capture.
§Recipes
Short snippets for the most common follow-on tasks. Every recipe is a runnable
example in examples/ — see the Examples table.
Window capture with audio
use screencapturekit::prelude::*;
let content = SCShareableContent::get()?;
let window = content.windows().into_iter()
.find(|w| w.title().as_deref() == Some("Safari"))
.ok_or("Safari window not found")?;
let filter = SCContentFilter::create().with_window(&window).build();
let config = SCStreamConfiguration::new()
.with_captures_audio(true)
.with_sample_rate(48_000)
.with_channel_count(2);
let mut stream = SCStream::new(&filter, &config);
// stream.add_output_handler(...) for Screen and/or Audio
stream.start_capture()?;Closure-based handler (no trait impl needed)
stream.add_output_handler(
|sample: CMSampleBuffer, _of_type: SCStreamOutputType| {
println!("📹 frame @ {:?}", sample.presentation_timestamp());
},
SCStreamOutputType::Screen,
);Closures must be Fn + Send + Sync + 'static.
Async capture (any executor)
use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
use screencapturekit::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = AsyncSCShareableContent::get().await?;
let display = &content.displays()[0];
let filter = SCContentFilter::create()
.with_display(display).with_excluding_windows(&[]).build();
let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
// 30-frame ring buffer; oldest frames are dropped if the consumer can't keep up.
let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
stream.start_capture()?;
while let Some(_frame) = stream.next().await {
// process frame
}
stream.stop_capture()?;
Ok(())
}Requires the async feature. Works with Tokio, async-std, smol, or any
custom executor — the binding does not spawn its own runtime.
Screenshot (macOS 14.0+)
use screencapturekit::screenshot_manager::SCScreenshotManager;
let img = SCScreenshotManager::capture_image(filter, config)?;
let pixels = img.bgra_data()?; // native BGRA — skips R↔B swap
// For sustained loops, reuse a buffer:
// img.bgra_data_into(&mut buffer)?;System content picker (macOS 14.0+)
use screencapturekit::content_sharing_picker::*;
use screencapturekit::prelude::*;
let config = SCContentSharingPickerConfiguration::new();
SCContentSharingPicker::show(&config, |outcome| match outcome {
SCPickerOutcome::Picked(result) => {
let (w, h) = result.pixel_size();
let filter = result.filter();
// Use `filter` with SCStream as in the Quick Start.
let _ = (w, h, filter);
}
SCPickerOutcome::Cancelled => println!("user cancelled"),
SCPickerOutcome::Error(e) => eprintln!("picker error: {e}"),
});For async contexts, use AsyncSCContentSharingPicker::show.
Direct-to-file recording (macOS 15.0+)
See examples/10_recording_output.rs — it
covers SCRecordingOutput, SCRecordingOutputConfiguration, and the
delegate callbacks for start / finish / error.
Custom dispatch queue / QoS
use screencapturekit::prelude::*;
use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
stream.add_output_handler_with_queue(
|_sample, _of_type| { /* runs on `queue` */ },
SCStreamOutputType::Screen,
Some(&queue),
);QoS levels: Background, Utility, Default, UserInitiated, UserInteractive (Quality of Service).
Zero-copy GPU access (IOSurface → Metal / wgpu)
use screencapturekit::prelude::*;
struct H;
impl SCStreamOutputTrait for H {
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, _: SCStreamOutputType) {
if let Some(pb) = sample.image_buffer() {
if let Some(surface) = pb.io_surface() {
let _ = (surface.width(), surface.height());
// Wrap as `MTLTexture` (see examples 17/18) — no copy.
}
}
}
}Built-in Metal helpers live in screencapturekit::metal and ship a small
shader library (SHADER_SOURCE) covering BGRA, YCbCr, and UI overlay
rendering. See examples/16_full_metal_app/
for a complete app and examples/18_wgpu_integration.rs
for the wgpu equivalent.
§Examples
23 runnable examples cover every API surface. The full table with feature
requirements lives in examples/README.md. A few
favourites to start with:
| Example | What it shows |
|---|---|
01_basic_capture | Minimal screen capture — start here |
08_async | Async API, picker, runtime-agnostic patterns |
09_closure_handlers | Closures + delegate callbacks |
10_recording_output | Direct-to-file recording (macOS 15.0+) |
11_content_picker | System picker UI (macOS 14.0+) |
16_full_metal_app/ | Full Metal viewer app (macOS 14.0+) |
18_wgpu_integration | Zero-copy wgpu integration |
19_ffmpeg_encoding | Real-time H.264 via ffmpeg |
24_batched_apis_showcase | Batched FFI vs per-element (perf) |
cargo run --example 01_basic_capture
cargo run --example 10_recording_output --features macos_15_0
cargo run --example 08_async --features "async,macos_14_0"§Feature Flags
See the full feature table under Install. One small example of gating version-specific options:
let mut config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
#[cfg(feature = "macos_14_2")]
{
config.set_ignores_shadows_single_window(true);
config.set_includes_child_windows(false);
}§Documentation
| Where | What |
|---|---|
| docs.rs | Full API reference |
docs/MIGRATION.md | Upgrading between major versions |
docs/BENCHMARKS.md | Benchmark methodology + results |
examples/README.md | All 23 examples + feature requirements |
CHANGELOG.md | Release notes |
§Requirements & Permissions
- macOS 12.3+ (Monterey) — base
ScreenCaptureKit - macOS 13.0+ — audio capture · 14.0+ — picker / screenshots · 15.0+ — recording / HDR / mic · 26.0+ — advanced screenshots
- Xcode Command Line Tools at build time (
xcode-select --install)
Screen capture always requires user permission. To grant it:
- System Settings → Privacy & Security → Screen Recording
- Enable your binary (during development this is usually your terminal or IDE)
- Restart the app
For distribution, add NSScreenCaptureUsageDescription to Info.plist and the
appropriate entitlements:
<key>com.apple.security.app-sandbox</key> <true/>
<key>com.apple.security.screen-capture</key> <true/>§Performance
Full capture (60 fps + 48 kHz stereo) costs ~1.9% of one core end-to-end
on Apple Silicon — the binding itself is below the noise floor of a 4 kHz
sampling profiler; nearly all CPU lives in Apple’s SkyLight /
libdispatch / libxpc pipeline.
| Resolution | Expected FPS | First-frame latency |
|---|---|---|
| 1080p | 30–60 | 30–100 ms |
| 4K | 15–30 | 50–150 ms |
Hot-path tips:
- Prefer
BGRAto skip the per-pixel R↔B swap when uploading to Metal / wgpu / ffmpeg (SCScreenshotManager::bgra_datais ~5% faster thanrgba_data). - Reuse a
Vec<u8>across screenshots with the*_data_intovariants (saves a ~33 MB allocation per 4K frame — new in 2.1). - When iterating many windows / displays / apps, use the batched
SCShareableContent::snapshot()API — collapses1 + N + 6NFFI calls into one round-trip per category (~2× faster on a typical desktop). - Read every
SCStreamFrameInfoattachment in one cast viaCMSampleBuffer::frame_info().
use screencapturekit::prelude::*;
use screencapturekit::shareable_content::ContentSnapshot;
let content = SCShareableContent::get()?;
let ContentSnapshot { displays, windows, applications } =
content.snapshot().ok_or("snapshot failed")?;
for w in &windows {
let app = w.owning_app_index.and_then(|i| applications.get(i));
println!("{} - {}", app.map(|a| &*a.application_name).unwrap_or(""),
w.title.as_deref().unwrap_or(""));
}Run benchmarks on your hardware:
cargo bench
cargo bench --bench hotspots --features macos_14_0See docs/BENCHMARKS.md for methodology, throughput
numbers at various resolutions, and tuning guidance.
§Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
SCShareableContent::get() returns empty / errors | Missing Screen Recording permission — grant it in System Settings, then restart |
| Black / empty frames | Captured window minimized; pixel format mismatch; filter doesn’t include the right display/window |
| No audio samples | Did you set .with_captures_audio(true) and add a handler for SCStreamOutputType::Audio? |
| Build fails with Swift bridge errors | xcode-select --install; then cargo clean && cargo build |
| App crashes after notarization | Add the screen-capture entitlement (see Requirements) |
match on PixelFormat / SCStreamErrorCode no longer compiles | Both are #[non_exhaustive] in 2.0 — add a wildcard _ => … arm |
§Migration
Upgrading? See docs/MIGRATION.md for the full guide.
The 2.0 highlights:
SCStreamOutputTrait/SCStreamDelegateTrait(and closure overloads) now requireSend + SyncPixelFormatis#[non_exhaustive]and gainedUnknown(FourCharCode)for forward-compat with future Apple pixel formatsSCStreamErrorCodeis#[non_exhaustive]PixelFormat’sPartialEq/Hashare normalised throughFourCharCode- Every
macos_*Cargo feature now propagates to the Swift bridge build (the build fails loudly on SDK detection failure rather than silently dropping symbols)
2.1 added the bgra_data_into / rgba_data_into buffer-reuse APIs and a
native-BGRA fast path on SCScreenshotManager — both are non-breaking.
§Contributing
Contributions welcome! Please:
- Follow existing patterns — builder pattern with
::new()and.with_*() - Add tests for new functionality
cargo fmt && cargo clippy --all-features -- -D warnings && cargo test- Update docs and
CHANGELOG.md
See CLAUDE.md / AGENTS.md for the project conventions agents follow.
§Used By
Powering 50+ open-source projects across screen recording, AI agents, meeting transcription, and remote desktop. A few highlights:
- AFFiNE — knowledge base, Notion / Miro alternative (68k+ ⭐)
- voicebox — open-source AI voice studio (25k+ ⭐)
- Cap — open-source Loom alternative (19k+ ⭐)
- Observer — local AI screen observer (1.4k+ ⭐)
- my-translator — real-time speech translation (1k+ ⭐)
- hylarana — cross-platform screen casting in Rust
- gst-screencapturekit —
GStreamerplugin - open-agent, watson.ai, harana/search, agent-native by Builder.io
And many more…
fl_caption, Lycoris, Hindsight, kivio, Drift, Phantom, ruhear, Tab5-Screen-Streamer, macloop, beer, phantom-ear, Logia, VibeTube, silly-ai, aresampler, xos, scriberr-desktop, echonote, zest-wallpaper, mira, overlay-ai, open-rec, omnirec, oxiremote, LocalWhisper, Hush, cocuyo, openhush, tucknotes, domino, bridge, screen-recorder, orbit, audio-capture, AFFiNE-teto, loom.
Using screencapturekit-rs? Open an issue and we’ll add you.
§Contributors
Thanks to everyone who has contributed!
Per Johansson (maintainer) · Iason Paraskevopoulos · Kris Krolak · Tokuhiro Matsuno · Pranav Joglekar · Alex Jiao · Charles · bigduu · Andrew N
§License
Licensed under either of Apache-2.0 or MIT at your option.
§API Documentation
Safe, idiomatic Rust bindings for Apple’s ScreenCaptureKit framework.
Capture screen content, windows, and applications with high performance on macOS 12.3+.
§Features
- Screen and window capture - Capture displays, windows, or specific applications
- Audio capture - System audio and microphone input (macOS 13.0+)
- Real-time frame processing - High-performance callbacks with custom dispatch queues
- Async support - Runtime-agnostic async API (Tokio, async-std, smol, etc.)
- Zero-copy GPU access - Direct
IOSurfaceaccess for Metal/OpenGL integration - Screenshots - Single-frame capture without streaming (macOS 14.0+)
- Recording - Direct-to-file video recording (macOS 15.0+)
- Content Picker - System UI for user content selection (macOS 14.0+)
§Installation
Add to your Cargo.toml:
[dependencies]
screencapturekit = "1"For async support:
[dependencies]
screencapturekit = { version = "1", features = ["async"] }§Quick Start
§1. Request Permission
Screen recording requires user permission. Add to your Info.plist:
<key>NSScreenCaptureUsageDescription</key>
<string>This app needs screen recording permission.</string>§2. Implement a Frame Handler
You can use either a struct or a closure:
Struct-based handler:
use screencapturekit::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct FrameHandler {
count: Arc<AtomicUsize>,
}
impl SCStreamOutputTrait for FrameHandler {
fn did_output_sample_buffer(&self, sample: CMSampleBuffer, of_type: SCStreamOutputType) {
match of_type {
SCStreamOutputType::Screen => {
let n = self.count.fetch_add(1, Ordering::Relaxed);
if n % 60 == 0 {
println!("Frame {n}");
}
}
SCStreamOutputType::Audio => {
println!("Got audio samples!");
}
_ => {}
}
}
}Closure-based handler:
use screencapturekit::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let frame_count = Arc::new(AtomicUsize::new(0));
let count_clone = frame_count.clone();
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(
move |_sample: CMSampleBuffer, _of_type: SCStreamOutputType| {
count_clone.fetch_add(1, Ordering::Relaxed);
},
SCStreamOutputType::Screen
);§3. Start Capturing
use screencapturekit::prelude::*;
// Get available displays
let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;
// Configure what to capture
let filter = SCContentFilter::create()
.with_display(&display)
.with_excluding_windows(&[])
.build();
// Configure how to capture
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080)
.with_pixel_format(PixelFormat::BGRA)
.with_shows_cursor(true);
// Create stream and add handler
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);
// Start capturing
stream.start_capture()?;
// ... capture runs in background ...
std::thread::sleep(std::time::Duration::from_secs(5));
stream.stop_capture()?;§Configuration Options
Use the builder pattern for fluent configuration:
use screencapturekit::prelude::*;
// For 60 FPS, use CMTime to specify frame interval
let frame_interval = CMTime::new(1, 60); // 1/60th of a second
let config = SCStreamConfiguration::new()
// Video settings
.with_width(1920)
.with_height(1080)
.with_pixel_format(PixelFormat::BGRA)
.with_shows_cursor(true)
.with_minimum_frame_interval(&frame_interval)
// Audio settings
.with_captures_audio(true)
.with_sample_rate(48000)
.with_channel_count(2);§Available Pixel Formats
| Format | Description | Use Case |
|---|---|---|
PixelFormat::BGRA | 32-bit BGRA | General purpose, easy to use |
PixelFormat::l10r | 10-bit RGB | HDR content |
PixelFormat::YCbCr_420v | YCbCr 4:2:0 | Video encoding (H.264/HEVC) |
PixelFormat::YCbCr_420f | YCbCr 4:2:0 full range | Video encoding |
§Accessing Frame Data
§Pixel Data (CPU)
Lock the pixel buffer for direct CPU access:
use screencapturekit::prelude::*;
use screencapturekit::cv::{CVPixelBuffer, CVPixelBufferLockFlags, PixelBufferCursorExt};
use std::io::{Read, Seek, SeekFrom};
if let Some(buffer) = sample.image_buffer() {
if let Ok(guard) = buffer.lock(CVPixelBufferLockFlags::READ_ONLY) {
// Method 1: Direct slice access (fast)
let pixels = guard.as_slice();
let width = guard.width();
let height = guard.height();
// Method 2: Use cursor for reading specific pixels
let mut cursor = guard.cursor();
// Read first pixel (BGRA)
if let Ok(pixel) = cursor.read_pixel() {
println!("First pixel: {:?}", pixel);
}
// Seek to center pixel
let center_x = width / 2;
let center_y = height / 2;
if cursor.seek_to_pixel(center_x, center_y, guard.bytes_per_row()).is_ok() {
if let Ok(pixel) = cursor.read_pixel() {
println!("Center pixel: {:?}", pixel);
}
}
}
}§IOSurface (GPU)
For Metal/OpenGL integration, access the underlying IOSurface:
use screencapturekit::prelude::*;
use screencapturekit::cm::IOSurfaceLockOptions;
use screencapturekit::cv::PixelBufferCursorExt;
if let Some(buffer) = sample.image_buffer() {
// Check if IOSurface-backed (usually true for ScreenCaptureKit)
if buffer.is_backed_by_io_surface() {
if let Some(surface) = buffer.io_surface() {
println!("Dimensions: {}x{}", surface.width(), surface.height());
println!("Pixel format: 0x{:08X}", surface.pixel_format());
println!("Bytes per row: {}", surface.bytes_per_row());
println!("In use: {}", surface.is_in_use());
// Lock for CPU access to IOSurface data
if let Ok(guard) = surface.lock(IOSurfaceLockOptions::READ_ONLY) {
let mut cursor = guard.cursor();
if let Ok(pixel) = cursor.read_pixel() {
println!("First pixel: {:?}", pixel);
}
}
}
}
}§Audio + Video Capture
Capture system audio alongside video:
use screencapturekit::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct AVHandler {
video_count: Arc<AtomicUsize>,
audio_count: Arc<AtomicUsize>,
}
impl SCStreamOutputTrait for AVHandler {
fn did_output_sample_buffer(&self, _sample: CMSampleBuffer, of_type: SCStreamOutputType) {
match of_type {
SCStreamOutputType::Screen => {
self.video_count.fetch_add(1, Ordering::Relaxed);
}
SCStreamOutputType::Audio => {
self.audio_count.fetch_add(1, Ordering::Relaxed);
}
SCStreamOutputType::Microphone => {
// Requires macOS 15.0+ and .with_captures_microphone(true)
}
}
}
}
let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;
let filter = SCContentFilter::create()
.with_display(&display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080)
.with_captures_audio(true) // Enable system audio
.with_sample_rate(48000) // 48kHz
.with_channel_count(2); // Stereo
let handler = AVHandler {
video_count: Arc::new(AtomicUsize::new(0)),
audio_count: Arc::new(AtomicUsize::new(0)),
};
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(handler, SCStreamOutputType::Screen);
stream.start_capture()?;§Dynamic Stream Updates
Update configuration or content filter while streaming:
use screencapturekit::prelude::*;
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);
stream.start_capture()?;
// Capture at initial resolution...
std::thread::sleep(std::time::Duration::from_secs(2));
// Update to higher resolution while streaming
let new_config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080);
stream.update_configuration(&new_config)?;
// Switch to a different window
let windows = content.windows();
if let Some(window) = windows.iter().find(|w| w.is_on_screen()) {
let window_filter = SCContentFilter::create().with_window(window).build();
stream.update_content_filter(&window_filter)?;
}
stream.stop_capture()?;§Error Handling with Delegates
Handle stream errors gracefully using delegates:
use screencapturekit::prelude::*;
use screencapturekit::stream::ErrorHandler;
// Create an error handler using a closure
let error_handler = ErrorHandler::new(|error| {
eprintln!("Stream error: {error}");
});
// Create stream with delegate
let mut stream = SCStream::new_with_delegate(&filter, &config, error_handler);
stream.add_output_handler(
|_sample, _type| { /* process frames */ },
SCStreamOutputType::Screen
);
stream.start_capture()?;§Custom Dispatch Queues
Control which thread/queue handles frame callbacks:
use screencapturekit::prelude::*;
use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
let mut stream = SCStream::new(&filter, &config);
// Create a high-priority queue for frame processing
let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
stream.add_output_handler_with_queue(
|_sample, _type| { /* called on custom queue */ },
SCStreamOutputType::Screen,
Some(&queue)
);§Async API
Enable the async feature for async/await support. The async API is
executor-agnostic and works with Tokio, async-std, smol, or any runtime:
use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
use screencapturekit::prelude::*;
async fn capture() -> Result<(), Box<dyn std::error::Error>> {
// Get content asynchronously (true async - no blocking)
let content = AsyncSCShareableContent::get().await?;
let display = &content.displays()[0];
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 30-frame buffer
let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
stream.start_capture()?;
// Async iteration over frames
let mut count = 0;
while count < 100 {
if let Some(_frame) = stream.next().await {
count += 1;
}
}
stream.stop_capture()?;
Ok(())
}
// Concurrent async operations
async fn concurrent_queries() -> Result<(), Box<dyn std::error::Error>> {
let (result1, result2) = tokio::join!(
AsyncSCShareableContent::get(),
AsyncSCShareableContent::with_options()
.on_screen_windows_only(true)
.get(),
);
Ok(())
}§Screenshots (macOS 14.0+)
Take single screenshots without setting up a stream:
use screencapturekit::prelude::*;
use screencapturekit::screenshot_manager::SCScreenshotManager;
let content = SCShareableContent::get()?;
let display = &content.displays()[0];
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080);
// Capture screenshot as CGImage
let image = SCScreenshotManager::capture_image(&filter, &config)?;
println!("Screenshot: {}x{}", image.width(), image.height());
// Or capture as CMSampleBuffer for more control
let sample_buffer = SCScreenshotManager::capture_sample_buffer(&filter, &config)?;§Recording (macOS 15.0+)
Record directly to a video file:
use screencapturekit::prelude::*;
use screencapturekit::recording_output::{
SCRecordingOutput, SCRecordingOutputConfiguration,
SCRecordingOutputCodec, SCRecordingOutputFileType
};
use std::path::PathBuf;
let content = SCShareableContent::get()?;
let display = &content.displays()[0];
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let stream_config = SCStreamConfiguration::new()
.with_width(1920)
.with_height(1080);
// Configure recording output
let output_path = PathBuf::from("/tmp/screen_recording.mp4");
let recording_config = SCRecordingOutputConfiguration::new()
.with_output_url(&output_path)
.with_video_codec(SCRecordingOutputCodec::H264)
.with_output_file_type(SCRecordingOutputFileType::MP4);
let recording_output = SCRecordingOutput::new(&recording_config)
.ok_or("Failed to create recording output")?;
// Start stream and add recording
let stream = SCStream::new(&filter, &stream_config);
stream.add_recording_output(&recording_output)?;
stream.start_capture()?;
// Record for 10 seconds
std::thread::sleep(std::time::Duration::from_secs(10));
// Check recording stats
let duration = recording_output.recorded_duration();
let file_size = recording_output.recorded_file_size();
println!("Recorded {}/{} seconds, {} bytes", duration.value, duration.timescale, file_size);
stream.remove_recording_output(&recording_output)?;
stream.stop_capture()?;§Module Organization
| Module | Description |
|---|---|
stream | Stream configuration and management (SCStream, SCContentFilter) |
shareable_content | Display, window, and application enumeration |
cm | Core Media types (CMSampleBuffer, CMTime, IOSurface) |
cv | Core Video types (CVPixelBuffer, lock guards) |
cg | Core Graphics types (CGRect, CGSize) |
metal | Metal texture helpers for zero-copy GPU rendering |
dispatch_queue | Custom dispatch queues for callbacks |
error | Error types and result aliases |
async_api | Async wrappers (requires async feature) |
screenshot_manager | Single-frame capture (macOS 14.0+) |
recording_output | Direct file recording (macOS 15.0+) |
§Feature Flags
| Feature | Description |
|---|---|
async | Runtime-agnostic async API |
macos_13_0 | macOS 13.0+ APIs (audio capture, synchronization clock) |
macos_14_0 | macOS 14.0+ APIs (screenshots, content picker) |
macos_14_2 | macOS 14.2+ APIs (menu bar, child windows, presenter overlay) |
macos_14_4 | macOS 14.4+ APIs (current process shareable content) |
macos_15_0 | macOS 15.0+ APIs (recording output, HDR, microphone) |
macos_15_2 | macOS 15.2+ APIs (screenshot in rect, stream delegates) |
macos_26_0 | macOS 26.0+ APIs (advanced screenshot config, HDR output) |
Features are cumulative: enabling macos_15_0 also enables all earlier versions.
§Platform Requirements
- macOS 12.3+ (Monterey) - Base
ScreenCaptureKitsupport - Screen Recording Permission - Must be granted by user in System Preferences
- Hardened Runtime - Required for notarized apps
§Examples
See the examples directory:
| Example | Description |
|---|---|
01_basic_capture | Simplest screen capture |
02_window_capture | Capture specific windows |
03_audio_capture | Audio + video capture |
04_pixel_access | Read pixel data with cursor API |
05_screenshot | Single screenshot (macOS 14.0+) |
06_iosurface | Zero-copy GPU buffer access |
07_list_content | List available displays, windows, apps |
08_async | Async/await API with any runtime |
09_closure_handlers | Closure-based handlers |
10_recording_output | Direct video recording (macOS 15.0+) |
11_content_picker | System content picker UI (macOS 14.0+) |
12_stream_updates | Dynamic config/filter updates |
13_advanced_config | HDR, presets, microphone (macOS 15.0+) |
14_app_capture | Application-based filtering |
15_memory_leak_check | Memory leak detection |
16_full_metal_app | Full Metal GUI application |
17_metal_textures | Metal texture creation from IOSurface |
§Common Patterns
§Capture Window by Title
use screencapturekit::prelude::*;
let content = SCShareableContent::get()?;
let windows = content.windows();
let window = windows
.iter()
.find(|w| w.title().is_some_and(|t| t.contains("Safari")))
.ok_or("Window not found")?;
let filter = SCContentFilter::create()
.with_window(window)
.build();§Capture Specific Application
use screencapturekit::prelude::*;
let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;
// Find app by bundle ID
let apps = content.applications();
let safari = apps
.iter()
.find(|app| app.bundle_identifier() == "com.apple.Safari")
.ok_or("Safari not found")?;
// Capture only windows from this app
let filter = SCContentFilter::create()
.with_display(&display)
.with_including_applications(&[safari], &[]) // Include Safari, no excepted windows
.build();§Exclude Your Own App’s Windows
use screencapturekit::prelude::*;
let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;
// Find our app's windows
let windows = content.windows();
let my_windows: Vec<&SCWindow> = windows
.iter()
.filter(|w| w.owning_application()
.map(|app| app.bundle_identifier() == "com.mycompany.myapp")
.unwrap_or(false))
.collect();
// Capture everything except our windows
let filter = SCContentFilter::create()
.with_display(&display)
.with_excluding_windows(&my_windows)
.build();§List All Available Content
use screencapturekit::prelude::*;
let content = SCShareableContent::get()?;
println!("=== Displays ===");
for display in content.displays() {
println!(" Display {}: {}x{}", display.display_id(), display.width(), display.height());
}
println!("\n=== Windows ===");
for window in content.windows().iter().filter(|w| w.is_on_screen()) {
println!(" [{}] {} - {}",
window.window_id(),
window.owning_application()
.map(|app| app.application_name())
.unwrap_or_default(),
window.title().unwrap_or_default()
);
}
println!("\n=== Applications ===");
for app in content.applications() {
println!(" {} ({})", app.application_name(), app.bundle_identifier());
}Re-exports§
pub use cm::AudioBuffer;pub use cm::AudioBufferList;pub use cm::CMSampleTimingInfo;pub use cm::CMTime;pub use cm::SCFrameStatus;pub use apple_metal;
Modules§
- async_
api async - Async API for
ScreenCaptureKit - audio_
devices - Audio input device enumeration using
AVFoundation. - cg
- CoreGraphics value types — re-exported from
apple-cfso that downstream crates and other doom-fish bindings see the sameCGRect/CGPoint/CGSizetypes. - cm
- Core Media types and wrappers
- codec_
types - Common codec type constants
- content_
sharing_ picker macos_14_0 SCContentSharingPicker- UI for selecting content to share- cv
CoreVideotypes — re-exported fromapple-cf.- dispatch_
queue - GCD dispatch queue wrapper — re-exported from
apple-cfto share the sameDispatchQueuetype with the rest of the doom-fish suite. - error
- Error types for
ScreenCaptureKitoperations - ffi
- Swift FFI bridge to
ScreenCaptureKit - media_
types - Common media type constants
- metal
- Metal texture helpers for
IOSurface - prelude
- Prelude module for convenient imports
- recording_
output macos_15_0 SCRecordingOutput- Direct video file recording- screenshot_
manager macos_14_0 SCScreenshotManager- Single-shot screenshot capture- shareable_
content - Shareable content types - displays, windows, and applications
- stream
- Screen capture stream functionality
- utils
- Shared utilities.
Structs§
- CMFormat
Description - CMSample
Buffer - Re-exported
CMSampleBuffer— same opaque-pointer wrapper used across the doom-fish suite. Owned reference to aCoreMediaCMSampleBufferRef. - CVPixel
Buffer - CVPixel
Buffer Pool - Opaque handle to
CVPixelBufferPool - Four
Char Code FourCharCoderepresents a 4-character code (used in Core Video/Media)- IOSurface
- Hardware-accelerated surface for efficient frame delivery