screencapturekit 3.1.2

Safe Rust bindings for Apple's ScreenCaptureKit framework - screen and audio capture on macOS
Documentation

๐Ÿ’ผ 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

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

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(()) }
# 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.

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.

# #[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(()) }
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.

See examples/10_recording_output.rs โ€” it covers SCRecordingOutput, SCRecordingOutputConfiguration, and the delegate callbacks for start / finish / error.

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

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:

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.