README.md

May 29, 2026 Β· View on GitHub

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

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
  • πŸ“¦ Minimal dependencies β€” only the thin apple-cf / apple-metal binding crates (no heavy third-party runtime deps)

Table of Contents


Install

[dependencies]
screencapturekit = "6"

Opt-in features (additive):

FeatureEnables
asyncRuntime-agnostic async API (Tokio / async-std / smol / …)
macos_13_0Audio capture, sync clock
macos_14_0Screenshots, content picker, content info
macos_14_2Menu bar capture, child windows, presenter overlay
macos_14_4Current-process shareable content
macos_15_0Recording output, HDR capture, microphone
macos_15_2Screenshot in rect, stream active/inactive delegates
macos_26_0Advanced 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 = "6", features = ["async", "macos_15_0"] }

Upgrading a major version? See docs/MIGRATION.md for a per-version guide. The 3.0–6.0 line consolidated the Core Graphics / Core Media foundation types onto the shared apple-cf crate; the most likely source change is 5.0's nested CGRect layout (rect.origin.x / rect.size.width).

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::{CGImageExt, 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:

ExampleWhat it shows
01_basic_captureMinimal screen capture β€” start here
08_asyncAsync API, picker, runtime-agnostic patterns
09_closure_handlersClosures + delegate callbacks
10_recording_outputDirect-to-file recording (macOS 15.0+)
11_content_pickerSystem picker UI (macOS 14.0+)
16_full_metal_app/Full Metal viewer app (macOS 14.0+)
18_wgpu_integrationZero-copy wgpu integration
19_ffmpeg_encodingReal-time H.264 via ffmpeg
24_batched_apis_showcaseBatched 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

WhereWhat
docs.rsFull API reference
docs/MIGRATION.mdUpgrading between major versions
docs/BENCHMARKS.mdBenchmark methodology + results
examples/README.mdAll 23 examples + feature requirements
CHANGELOG.mdRelease 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 typeWhat you need
Any signed macOS app (sandboxed or not)NSScreenCaptureUsageDescription in Info.plist + user TCC grant
Sandboxed appAdditionally 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.

ResolutionExpected FPSFirst-frame latency
1080p30–6030–100 ms
4K15–3050–150 ms

Hot-path tips:

  • Prefer BGRA to skip the per-pixel R↔B swap when uploading to Metal / wgpu / ffmpeg (CGImageExt::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

SymptomLikely cause / fix
SCShareableContent::get() returns empty / errorsMissing Screen Recording permission β€” grant it in System Settings, then restart
Black / empty framesCaptured window minimized; pixel format mismatch; filter doesn't include the right display/window
No audio samplesDid you set .with_captures_audio(true) and add a handler for SCStreamOutputType::Audio?
Build fails with Swift bridge errorsxcode-select --install; then cargo clean && cargo build
App crashes after notarizationMissing 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 compilesBoth are #[non_exhaustive] in 2.0 β€” add a wildcard _ => … arm

Migration

Upgrading? See docs/MIGRATION.md for the full guide, including a section for every major version bump.

Highlights by major version:

  • 2.0 β€” output / delegate traits (and closure overloads) now require Send + Sync; PixelFormat and SCStreamErrorCode became #[non_exhaustive]; PixelFormat gained Unknown(FourCharCode); every macos_* Cargo feature now propagates to the Swift bridge build.
  • 3.0 β€” foundation types (Core Graphics / Core Media / IOSurface / Core Video) moved onto the shared apple-cf / apple-metal crates as re-exports; ScreenCaptureKit-specific CMSampleBuffer accessors moved to the CMSampleBufferExt / CMSampleBufferSCExt extension traits (both in the prelude).
  • 4.0 β€” ScreenshotManager::capture_image returns apple_cf::cg::CGImage and screencapturekit::cm::CMTime is an apple-cf re-export (drop any cross-crate conversions).
  • 5.0 β€” adopts apple-cf 0.8's nested CGRect layout: use rect.origin.x / rect.size.width instead of flat rect.x / rect.width.
  • 6.0 β€” CMSampleTimingInfo and CMClock are now re-exported from apple-cf as well.

If you only use the prelude / screencapturekit::{cg, cm} types, the 4.0–6.0 upgrades are typically just the 5.0 CGRect field-access change.

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.