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.
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-metalbinding crates (no heavy third-party runtime deps)
Table of Contents
- Install Β· Quick Start Β· Recipes
- Feature Flags Β· Examples Β· Documentation
- Requirements & Permissions Β· Performance
- Troubleshooting Β· Migration Β· Contributing
Install
[dependencies]
screencapturekit = "6"
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 = "6", features = ["async", "macos_15_0"] }
Upgrading a major version? See
docs/MIGRATION.mdfor a per-version guide. The 3.0β6.0 line consolidated the Core Graphics / Core Media foundation types onto the sharedapple-cfcrate; the most likely source change is 5.0's nestedCGRectlayout (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:
| 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 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-captureentitlement. That key isn't part of Apple's security-entitlements reference; the onlycom.apple.security.device.*keys arecamera,microphone,audio-input,usb, andbluetooth. The two real screen-capture entitlements (com.apple.developer.screen-capture.include-passthroughandcom.apple.developer.protected-content) are Enterprise / visionOS managed entitlements and don't apply toScreenCaptureKiton 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
BGRAto skip the per-pixel RβB swap when uploading to Metal / wgpu / ffmpeg (CGImageExt::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;
# 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,
including a section for every major version bump.
Highlights by major version:
- 2.0 β output / delegate traits (and closure overloads) now require
Send + Sync;PixelFormatandSCStreamErrorCodebecame#[non_exhaustive];PixelFormatgainedUnknown(FourCharCode); everymacos_*Cargo feature now propagates to the Swift bridge build. - 3.0 β foundation types (Core Graphics / Core Media /
IOSurface/ Core Video) moved onto the sharedapple-cf/apple-metalcrates as re-exports; ScreenCaptureKit-specificCMSampleBufferaccessors moved to theCMSampleBufferExt/CMSampleBufferSCExtextension traits (both in the prelude). - 4.0 β
ScreenshotManager::capture_imagereturnsapple_cf::cg::CGImageandscreencapturekit::cm::CMTimeis anapple-cfre-export (drop any cross-crate conversions). - 5.0 β adopts
apple-cf0.8's nestedCGRectlayout: userect.origin.x/rect.size.widthinstead of flatrect.x/rect.width. - 6.0 β
CMSampleTimingInfoandCMClockare now re-exported fromapple-cfas 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:
- 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.