Skip to content

doom-fish/screencapturekit-rs

Repository files navigation

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.

Crates.io Crates.io Downloads docs.rs License Build Status Stars

💼 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.

Screen.Recording.2025-12-11.at.11.56.46.mov

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

[dependencies]
screencapturekit = "3"

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 = "3", features = ["async", "macos_15_0"] }

Upgrading from 1.x? See docs/MIGRATION.md — the headline 2.0 changes are a Send + Sync bound on output / delegate traits, #[non_exhaustive] on PixelFormat and SCStreamErrorCode, and a new PixelFormat::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::*;
# fn main() -> Result<(), Box<dyn std::error::Error>> {
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()?;
# Ok(()) }
Closure-based handler (no trait impl needed)
# use screencapturekit::prelude::*;
# fn example(stream: &mut SCStream) {
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
        # break;
    }

    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+)
# #[cfg(feature = "macos_14_0")]
# fn example(
#     filter: &screencapturekit::stream::content_filter::SCContentFilter,
#     config: &screencapturekit::stream::configuration::SCStreamConfiguration,
# ) -> Result<(), Box<dyn std::error::Error>> {
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)?;
# Ok(()) }
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};
# fn example(stream: &mut SCStream) {
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:

  1. System Settings → Privacy & Security → Screen Recording
  2. Enable your binary (during development this is usually your terminal or IDE)
  3. Restart the app

For distribution, add a purpose string to Info.plist — the user-facing TCC prompt requires it and the app will be terminated without one:

<key>NSScreenCaptureUsageDescription</key>
<string>Capture your screen so the app can …</string>

ScreenCaptureKit is purely TCC-gated: there is no code-signing entitlement that grants screen capture access. Capture is allowed solely when the user enables your binary under System Settings → Privacy & Security → Screen & System Audio Recording.

App type What you need
Any signed macOS app (sandboxed or not) NSScreenCaptureUsageDescription in Info.plist + user TCC grant
Sandboxed app Additionally com.apple.security.app-sandbox = true in Entitlements.plist — this only turns the sandbox on; it does not grant capture
Sandboxed app capturing system audio (macOS 13+) Optionally com.apple.security.device.audio-input = true

There is no com.apple.security.screen-capture entitlement. That key isn't part of Apple's security-entitlements reference; the only com.apple.security.device.* keys are camera, microphone, audio-input, usb, and bluetooth. The two real screen-capture entitlements (com.apple.developer.screen-capture.include-passthrough and com.apple.developer.protected-content) are Enterprise / visionOS managed entitlements and don't apply to ScreenCaptureKit on macOS.

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 BGRA to skip the per-pixel R↔B swap when uploading to Metal / wgpu / ffmpeg (SCScreenshotManager::bgra_data is ~5% faster than rgba_data).
  • Reuse a Vec<u8> across screenshots with the *_data_into variants (saves a ~33 MB allocation per 4K frame — new in 2.1).
  • When iterating many windows / displays / apps, use the batched SCShareableContent::snapshot() API — collapses 1 + N + 6N FFI calls into one round-trip per category (~2× faster on a typical desktop).
  • Read every SCStreamFrameInfo attachment in one cast via CMSampleBuffer::frame_info().
use screencapturekit::prelude::*;
use screencapturekit::shareable_content::ContentSnapshot;
# fn example() -> Result<(), Box<dyn std::error::Error>> {
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(""));
}
# let _ = displays;
# Ok(()) }

Run benchmarks on your hardware:

cargo bench
cargo bench --bench hotspots --features macos_14_0

See 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 Missing NSScreenCaptureUsageDescription in Info.plist — the system terminates apps that trigger the Screen Recording TCC prompt without one (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 require Send + Sync
  • PixelFormat is #[non_exhaustive] and gained Unknown(FourCharCode) for forward-compat with future Apple pixel formats
  • SCStreamErrorCode is #[non_exhaustive]
  • PixelFormat's PartialEq / Hash are normalised through FourCharCode
  • 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:

  1. Follow existing patterns — builder pattern with ::new() and .with_*()
  2. Add tests for new functionality
  3. cargo fmt && cargo clippy --all-features -- -D warnings && cargo test
  4. 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:

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.

About

Safe, idiomatic Rust bindings for Apple ScreenCaptureKit — high-performance screen, window, and audio capture on macOS.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors