Safe, idiomatic Rust bindings for Apple's ScreenCaptureKit framework.
Capture screens, windows, and applications on macOS 12.3+ with high performance and low overhead.
<a href="https://crates.io/crates/screencapturekit"><img alt="Crates.io" src="https://img.shields.io/crates/v/screencapturekit?style=for-the-badge&logo=rust&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41" /></a>
<a href="https://crates.io/crates/screencapturekit"><img alt="Crates.io Downloads" src="https://img.shields.io/crates/d/screencapturekit?style=for-the-badge&logo=rust&color=A6E3A1&logoColor=D9E0EE&labelColor=302D41" /></a>
<a href="https://docs.rs/screencapturekit"><img alt="docs.rs" src="https://img.shields.io/docsrs/screencapturekit?style=for-the-badge&logo=docs.rs&color=8bd5ca&logoColor=D9E0EE&labelColor=302D41" /></a>
<a href="https://github.com/doom-fish/screencapturekit-rs#license"><img alt="License" src="https://img.shields.io/crates/l/screencapturekit?style=for-the-badge&logo=apache&color=ee999f&logoColor=D9E0EE&labelColor=302D41" /></a>
<a href="https://github.com/doom-fish/screencapturekit-rs/actions"><img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/doom-fish/screencapturekit-rs/ci.yml?branch=main&style=for-the-badge&logo=github&color=c69ff5&logoColor=D9E0EE&labelColor=302D41" /></a>
<a href="https://github.com/doom-fish/screencapturekit-rs/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/doom-fish/screencapturekit-rs?style=for-the-badge&logo=starship&color=F5E0DC&logoColor=D9E0EE&labelColor=302D41" /></a>
💼 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
IOSurface / Metalapple-cf / apple-metal binding crates (plus trait-only futures-core when the async feature is on; no heavy third-party runtime deps)[dependencies]
screencapturekit = "8"
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 = "8", features = ["async", "macos_15_0"] }
Upgrading a major version? See
docs/MIGRATION.mdfor a per-version guide. Releases 3.0–6.0 consolidated the Core Graphics / Core Media foundation types onto the sharedapple-cfcrate and 7.0 hardens the FFI boundary; the only likely source change across that line is 5.0's nestedCGRectlayout (rect.origin.x/rect.size.width). 8.0 makes theasyncstream lifecycle methods real futures — add.awaittoAsyncSCStream::{start,stop}_captureandupdate_*(see below).
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.
```rust,no_run 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> { 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](#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/`](examples/) — see the [Examples](#examples) table.
<strong>Window capture with audio</strong>
```rust,no_run
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)
```rust,no_run
stream.add_output_handler( |sample: CMSampleBuffer, _of_type: SCStreamOutputType| { println!("📹 frame @ {:?}", sample.presentation_timestamp()); }, SCStreamOutputType::Screen, );
Closures must be `Fn + Send + Sync + 'static`.
<strong>Async capture (any executor)</strong>
```rust,ignore
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);
// start/stop/update are real futures — awaiting parks the task via its
// Waker and never blocks the executor thread.
stream.start_capture().await?;
while let Some(_frame) = stream.next().await {
// process frame
# break;
}
stream.stop_capture().await?;
// Distinguish a normal end from an error stop (display gone, perms revoked, …):
if let Some(err) = stream.take_error() {
eprintln!("stream stopped with error: {err}");
}
Ok(())
}
Requires the async feature. Works with Tokio, async-std, smol, or any
custom executor — the binding does not spawn its own runtime, and the
lifecycle methods are waker-based so they never block the executor. AsyncSCStream
also exposes frames() / frames_typed() as futures::Streams, so you can use
the StreamExt combinators (take, map, filter, collect, …):
```rust,ignore use futures_util::StreamExt; let first_30: Vec<_> = stream.frames().take(30).collect().await;
<strong>Async audio + video from one stream</strong>
```rust,ignore
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();
// Enable audio in the configuration …
let config = SCStreamConfiguration::new()
.with_width(1920).with_height(1080)
.with_captures_audio(true);
let mut stream = AsyncSCStream::new(&filter, &config, 32, SCStreamOutputType::Screen);
// … then register audio as a second output type on the SAME stream.
stream.add_output_type(SCStreamOutputType::Audio);
stream.start_capture().await?;
// `next_typed()` yields each sample tagged with its output type.
while let Some((_sample, kind)) = stream.next_typed().await {
match kind {
SCStreamOutputType::Screen => { /* video frame */ }
SCStreamOutputType::Audio => { /* system audio */ }
_ => {}
}
# break;
}
stream.stop_capture().await?;
Ok(())
}
Screenshot (macOS 14.0+)
```rust,no_run
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)?;
<strong>System content picker (macOS 14.0+)</strong>
```rust,ignore
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
```rust,no_run 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).
<strong>Zero-copy GPU access (IOSurface → Metal / wgpu)</strong>
```rust,no_run
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.
23 runnable examples cover every API surface. The full table with feature
requirements lives in [examples/README.md](exa
$ claude mcp add screencapturekit-rs \
-- python -m otcore.mcp_server <graph>