Skip to main content

Crate screencapturekit

Crate screencapturekit 

Source
Expand description

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.

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

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 = "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.

Window capture with audio
use screencapturekit::prelude::*;
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()?;
Closure-based handler (no trait impl needed)
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
    }

    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+)
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)?;
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};
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 NSScreenCaptureUsageDescription to Info.plist and the appropriate entitlements:

<key>com.apple.security.app-sandbox</key>      <true/>
<key>com.apple.security.screen-capture</key>   <true/>

§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 (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;
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(""));
}

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 notarizationAdd the screen-capture entitlement (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. 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.


§API Documentation

Safe, idiomatic Rust bindings for Apple’s ScreenCaptureKit framework.

Capture screen content, windows, and applications with high performance on macOS 12.3+.

§Features

  • Screen and window capture - Capture displays, windows, or specific applications
  • Audio capture - System audio and microphone input (macOS 13.0+)
  • Real-time frame processing - High-performance callbacks with custom dispatch queues
  • Async support - Runtime-agnostic async API (Tokio, async-std, smol, etc.)
  • Zero-copy GPU access - Direct IOSurface access for Metal/OpenGL integration
  • Screenshots - Single-frame capture without streaming (macOS 14.0+)
  • Recording - Direct-to-file video recording (macOS 15.0+)
  • Content Picker - System UI for user content selection (macOS 14.0+)

§Installation

Add to your Cargo.toml:

[dependencies]
screencapturekit = "1"

For async support:

[dependencies]
screencapturekit = { version = "1", features = ["async"] }

§Quick Start

§1. Request Permission

Screen recording requires user permission. Add to your Info.plist:

<key>NSScreenCaptureUsageDescription</key>
<string>This app needs screen recording permission.</string>

§2. Implement a Frame Handler

You can use either a struct or a closure:

Struct-based handler:

use screencapturekit::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

struct FrameHandler {
    count: Arc<AtomicUsize>,
}

impl SCStreamOutputTrait for FrameHandler {
    fn did_output_sample_buffer(&self, sample: CMSampleBuffer, of_type: SCStreamOutputType) {
        match of_type {
            SCStreamOutputType::Screen => {
                let n = self.count.fetch_add(1, Ordering::Relaxed);
                if n % 60 == 0 {
                    println!("Frame {n}");
                }
            }
            SCStreamOutputType::Audio => {
                println!("Got audio samples!");
            }
            _ => {}
        }
    }
}

Closure-based handler:

use screencapturekit::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

let frame_count = Arc::new(AtomicUsize::new(0));
let count_clone = frame_count.clone();

let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(
    move |_sample: CMSampleBuffer, _of_type: SCStreamOutputType| {
        count_clone.fetch_add(1, Ordering::Relaxed);
    },
    SCStreamOutputType::Screen
);

§3. Start Capturing

use screencapturekit::prelude::*;

// Get available displays
let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;

// Configure what to capture
let filter = SCContentFilter::create()
    .with_display(&display)
    .with_excluding_windows(&[])
    .build();

// Configure how to capture
let config = SCStreamConfiguration::new()
    .with_width(1920)
    .with_height(1080)
    .with_pixel_format(PixelFormat::BGRA)
    .with_shows_cursor(true);

// Create stream and add handler
let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);

// Start capturing
stream.start_capture()?;

// ... capture runs in background ...
std::thread::sleep(std::time::Duration::from_secs(5));

stream.stop_capture()?;

§Configuration Options

Use the builder pattern for fluent configuration:

use screencapturekit::prelude::*;

// For 60 FPS, use CMTime to specify frame interval
let frame_interval = CMTime::new(1, 60); // 1/60th of a second

let config = SCStreamConfiguration::new()
    // Video settings
    .with_width(1920)
    .with_height(1080)
    .with_pixel_format(PixelFormat::BGRA)
    .with_shows_cursor(true)
    .with_minimum_frame_interval(&frame_interval)
     
    // Audio settings
    .with_captures_audio(true)
    .with_sample_rate(48000)
    .with_channel_count(2);

§Available Pixel Formats

FormatDescriptionUse Case
PixelFormat::BGRA32-bit BGRAGeneral purpose, easy to use
PixelFormat::l10r10-bit RGBHDR content
PixelFormat::YCbCr_420vYCbCr 4:2:0Video encoding (H.264/HEVC)
PixelFormat::YCbCr_420fYCbCr 4:2:0 full rangeVideo encoding

§Accessing Frame Data

§Pixel Data (CPU)

Lock the pixel buffer for direct CPU access:

use screencapturekit::prelude::*;
use screencapturekit::cv::{CVPixelBuffer, CVPixelBufferLockFlags, PixelBufferCursorExt};
use std::io::{Read, Seek, SeekFrom};

if let Some(buffer) = sample.image_buffer() {
    if let Ok(guard) = buffer.lock(CVPixelBufferLockFlags::READ_ONLY) {
        // Method 1: Direct slice access (fast)
        let pixels = guard.as_slice();
        let width = guard.width();
        let height = guard.height();

        // Method 2: Use cursor for reading specific pixels
        let mut cursor = guard.cursor();
         
        // Read first pixel (BGRA)
        if let Ok(pixel) = cursor.read_pixel() {
            println!("First pixel: {:?}", pixel);
        }

        // Seek to center pixel
        let center_x = width / 2;
        let center_y = height / 2;
        if cursor.seek_to_pixel(center_x, center_y, guard.bytes_per_row()).is_ok() {
            if let Ok(pixel) = cursor.read_pixel() {
                println!("Center pixel: {:?}", pixel);
            }
        }
    }
}

§IOSurface (GPU)

For Metal/OpenGL integration, access the underlying IOSurface:

use screencapturekit::prelude::*;
use screencapturekit::cm::IOSurfaceLockOptions;
use screencapturekit::cv::PixelBufferCursorExt;

if let Some(buffer) = sample.image_buffer() {
    // Check if IOSurface-backed (usually true for ScreenCaptureKit)
    if buffer.is_backed_by_io_surface() {
        if let Some(surface) = buffer.io_surface() {
            println!("Dimensions: {}x{}", surface.width(), surface.height());
            println!("Pixel format: 0x{:08X}", surface.pixel_format());
            println!("Bytes per row: {}", surface.bytes_per_row());
            println!("In use: {}", surface.is_in_use());

            // Lock for CPU access to IOSurface data
            if let Ok(guard) = surface.lock(IOSurfaceLockOptions::READ_ONLY) {
                let mut cursor = guard.cursor();
                if let Ok(pixel) = cursor.read_pixel() {
                    println!("First pixel: {:?}", pixel);
                }
            }
        }
    }
}

§Audio + Video Capture

Capture system audio alongside video:

use screencapturekit::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

struct AVHandler {
    video_count: Arc<AtomicUsize>,
    audio_count: Arc<AtomicUsize>,
}

impl SCStreamOutputTrait for AVHandler {
    fn did_output_sample_buffer(&self, _sample: CMSampleBuffer, of_type: SCStreamOutputType) {
        match of_type {
            SCStreamOutputType::Screen => {
                self.video_count.fetch_add(1, Ordering::Relaxed);
            }
            SCStreamOutputType::Audio => {
                self.audio_count.fetch_add(1, Ordering::Relaxed);
            }
            SCStreamOutputType::Microphone => {
                // Requires macOS 15.0+ and .with_captures_microphone(true)
            }
        }
    }
}

let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;

let filter = SCContentFilter::create()
    .with_display(&display)
    .with_excluding_windows(&[])
    .build();

let config = SCStreamConfiguration::new()
    .with_width(1920)
    .with_height(1080)
    .with_captures_audio(true)  // Enable system audio
    .with_sample_rate(48000)    // 48kHz
    .with_channel_count(2);     // Stereo

let handler = AVHandler {
    video_count: Arc::new(AtomicUsize::new(0)),
    audio_count: Arc::new(AtomicUsize::new(0)),
};

let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(handler, SCStreamOutputType::Screen);
stream.start_capture()?;

§Dynamic Stream Updates

Update configuration or content filter while streaming:

use screencapturekit::prelude::*;

let mut stream = SCStream::new(&filter, &config);
stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);
stream.start_capture()?;

// Capture at initial resolution...
std::thread::sleep(std::time::Duration::from_secs(2));

// Update to higher resolution while streaming
let new_config = SCStreamConfiguration::new()
    .with_width(1920)
    .with_height(1080);
stream.update_configuration(&new_config)?;

// Switch to a different window
let windows = content.windows();
if let Some(window) = windows.iter().find(|w| w.is_on_screen()) {
    let window_filter = SCContentFilter::create().with_window(window).build();
    stream.update_content_filter(&window_filter)?;
}

stream.stop_capture()?;

§Error Handling with Delegates

Handle stream errors gracefully using delegates:

use screencapturekit::prelude::*;
use screencapturekit::stream::ErrorHandler;

// Create an error handler using a closure
let error_handler = ErrorHandler::new(|error| {
    eprintln!("Stream error: {error}");
});

// Create stream with delegate
let mut stream = SCStream::new_with_delegate(&filter, &config, error_handler);
stream.add_output_handler(
    |_sample, _type| { /* process frames */ },
    SCStreamOutputType::Screen
);
stream.start_capture()?;

§Custom Dispatch Queues

Control which thread/queue handles frame callbacks:

use screencapturekit::prelude::*;
use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};

let mut stream = SCStream::new(&filter, &config);

// Create a high-priority queue for frame processing
let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);

stream.add_output_handler_with_queue(
    |_sample, _type| { /* called on custom queue */ },
    SCStreamOutputType::Screen,
    Some(&queue)
);

§Async API

Enable the async feature for async/await support. The async API is executor-agnostic and works with Tokio, async-std, smol, or any runtime:

use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
use screencapturekit::prelude::*;

async fn capture() -> Result<(), Box<dyn std::error::Error>> {
    // Get content asynchronously (true async - no blocking)
    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);
     
    // Create async stream with 30-frame buffer
    let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
    stream.start_capture()?;
     
    // Async iteration over frames
    let mut count = 0;
    while count < 100 {
        if let Some(_frame) = stream.next().await {
            count += 1;
        }
    }
     
    stream.stop_capture()?;
    Ok(())
}

// Concurrent async operations
async fn concurrent_queries() -> Result<(), Box<dyn std::error::Error>> {
    let (result1, result2) = tokio::join!(
        AsyncSCShareableContent::get(),
        AsyncSCShareableContent::with_options()
            .on_screen_windows_only(true)
            .get(),
    );
    Ok(())
}

§Screenshots (macOS 14.0+)

Take single screenshots without setting up a stream:

use screencapturekit::prelude::*;
use screencapturekit::screenshot_manager::SCScreenshotManager;

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

// Capture screenshot as CGImage
let image = SCScreenshotManager::capture_image(&filter, &config)?;
println!("Screenshot: {}x{}", image.width(), image.height());

// Or capture as CMSampleBuffer for more control
let sample_buffer = SCScreenshotManager::capture_sample_buffer(&filter, &config)?;

§Recording (macOS 15.0+)

Record directly to a video file:

use screencapturekit::prelude::*;
use screencapturekit::recording_output::{
    SCRecordingOutput, SCRecordingOutputConfiguration,
    SCRecordingOutputCodec, SCRecordingOutputFileType
};
use std::path::PathBuf;

let content = SCShareableContent::get()?;
let display = &content.displays()[0];

let filter = SCContentFilter::create()
    .with_display(display)
    .with_excluding_windows(&[])
    .build();

let stream_config = SCStreamConfiguration::new()
    .with_width(1920)
    .with_height(1080);

// Configure recording output
let output_path = PathBuf::from("/tmp/screen_recording.mp4");
let recording_config = SCRecordingOutputConfiguration::new()
    .with_output_url(&output_path)
    .with_video_codec(SCRecordingOutputCodec::H264)
    .with_output_file_type(SCRecordingOutputFileType::MP4);

let recording_output = SCRecordingOutput::new(&recording_config)
    .ok_or("Failed to create recording output")?;

// Start stream and add recording
let stream = SCStream::new(&filter, &stream_config);
stream.add_recording_output(&recording_output)?;
stream.start_capture()?;

// Record for 10 seconds
std::thread::sleep(std::time::Duration::from_secs(10));

// Check recording stats
let duration = recording_output.recorded_duration();
let file_size = recording_output.recorded_file_size();
println!("Recorded {}/{} seconds, {} bytes", duration.value, duration.timescale, file_size);

stream.remove_recording_output(&recording_output)?;
stream.stop_capture()?;

§Module Organization

ModuleDescription
streamStream configuration and management (SCStream, SCContentFilter)
shareable_contentDisplay, window, and application enumeration
cmCore Media types (CMSampleBuffer, CMTime, IOSurface)
cvCore Video types (CVPixelBuffer, lock guards)
cgCore Graphics types (CGRect, CGSize)
metalMetal texture helpers for zero-copy GPU rendering
dispatch_queueCustom dispatch queues for callbacks
errorError types and result aliases
async_apiAsync wrappers (requires async feature)
screenshot_managerSingle-frame capture (macOS 14.0+)
recording_outputDirect file recording (macOS 15.0+)

§Feature Flags

FeatureDescription
asyncRuntime-agnostic async API
macos_13_0macOS 13.0+ APIs (audio capture, synchronization clock)
macos_14_0macOS 14.0+ APIs (screenshots, content picker)
macos_14_2macOS 14.2+ APIs (menu bar, child windows, presenter overlay)
macos_14_4macOS 14.4+ APIs (current process shareable content)
macos_15_0macOS 15.0+ APIs (recording output, HDR, microphone)
macos_15_2macOS 15.2+ APIs (screenshot in rect, stream delegates)
macos_26_0macOS 26.0+ APIs (advanced screenshot config, HDR output)

Features are cumulative: enabling macos_15_0 also enables all earlier versions.

§Platform Requirements

  • macOS 12.3+ (Monterey) - Base ScreenCaptureKit support
  • Screen Recording Permission - Must be granted by user in System Preferences
  • Hardened Runtime - Required for notarized apps

§Examples

See the examples directory:

ExampleDescription
01_basic_captureSimplest screen capture
02_window_captureCapture specific windows
03_audio_captureAudio + video capture
04_pixel_accessRead pixel data with cursor API
05_screenshotSingle screenshot (macOS 14.0+)
06_iosurfaceZero-copy GPU buffer access
07_list_contentList available displays, windows, apps
08_asyncAsync/await API with any runtime
09_closure_handlersClosure-based handlers
10_recording_outputDirect video recording (macOS 15.0+)
11_content_pickerSystem content picker UI (macOS 14.0+)
12_stream_updatesDynamic config/filter updates
13_advanced_configHDR, presets, microphone (macOS 15.0+)
14_app_captureApplication-based filtering
15_memory_leak_checkMemory leak detection
16_full_metal_appFull Metal GUI application
17_metal_texturesMetal texture creation from IOSurface

§Common Patterns

§Capture Window by Title

use screencapturekit::prelude::*;

let content = SCShareableContent::get()?;
let windows = content.windows();
let window = windows
    .iter()
    .find(|w| w.title().is_some_and(|t| t.contains("Safari")))
    .ok_or("Window not found")?;

let filter = SCContentFilter::create()
    .with_window(window)
    .build();

§Capture Specific Application

use screencapturekit::prelude::*;

let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;

// Find app by bundle ID
let apps = content.applications();
let safari = apps
    .iter()
    .find(|app| app.bundle_identifier() == "com.apple.Safari")
    .ok_or("Safari not found")?;

// Capture only windows from this app
let filter = SCContentFilter::create()
    .with_display(&display)
    .with_including_applications(&[safari], &[])  // Include Safari, no excepted windows
    .build();

§Exclude Your Own App’s Windows

use screencapturekit::prelude::*;

let content = SCShareableContent::get()?;
let display = content.displays().into_iter().next().ok_or("No display")?;

// Find our app's windows
let windows = content.windows();
let my_windows: Vec<&SCWindow> = windows
    .iter()
    .filter(|w| w.owning_application()
        .map(|app| app.bundle_identifier() == "com.mycompany.myapp")
        .unwrap_or(false))
    .collect();

// Capture everything except our windows
let filter = SCContentFilter::create()
    .with_display(&display)
    .with_excluding_windows(&my_windows)
    .build();

§List All Available Content

use screencapturekit::prelude::*;

let content = SCShareableContent::get()?;

println!("=== Displays ===");
for display in content.displays() {
    println!("  Display {}: {}x{}", display.display_id(), display.width(), display.height());
}

println!("\n=== Windows ===");
for window in content.windows().iter().filter(|w| w.is_on_screen()) {
    println!("  [{}] {} - {}",
        window.window_id(),
        window.owning_application()
            .map(|app| app.application_name())
            .unwrap_or_default(),
        window.title().unwrap_or_default()
    );
}

println!("\n=== Applications ===");
for app in content.applications() {
    println!("  {} ({})", app.application_name(), app.bundle_identifier());
}

Re-exports§

pub use cm::AudioBuffer;
pub use cm::AudioBufferList;
pub use cm::CMSampleTimingInfo;
pub use cm::CMTime;
pub use cm::SCFrameStatus;
pub use apple_metal;

Modules§

async_apiasync
Async API for ScreenCaptureKit
audio_devices
Audio input device enumeration using AVFoundation.
cg
CoreGraphics value types — re-exported from apple-cf so that downstream crates and other doom-fish bindings see the same CGRect / CGPoint / CGSize types.
cm
Core Media types and wrappers
codec_types
Common codec type constants
content_sharing_pickermacos_14_0
SCContentSharingPicker - UI for selecting content to share
cv
CoreVideo types — re-exported from apple-cf.
dispatch_queue
GCD dispatch queue wrapper — re-exported from apple-cf to share the same DispatchQueue type with the rest of the doom-fish suite.
error
Error types for ScreenCaptureKit operations
ffi
Swift FFI bridge to ScreenCaptureKit
media_types
Common media type constants
metal
Metal texture helpers for IOSurface
prelude
Prelude module for convenient imports
recording_outputmacos_15_0
SCRecordingOutput - Direct video file recording
screenshot_managermacos_14_0
SCScreenshotManager - Single-shot screenshot capture
shareable_content
Shareable content types - displays, windows, and applications
stream
Screen capture stream functionality
utils
Shared utilities.

Structs§

CMFormatDescription
CMSampleBuffer
Re-exported CMSampleBuffer — same opaque-pointer wrapper used across the doom-fish suite. Owned reference to a CoreMedia CMSampleBufferRef.
CVPixelBuffer
CVPixelBufferPool
Opaque handle to CVPixelBufferPool
FourCharCode
FourCharCode represents a 4-character code (used in Core Video/Media)
IOSurface
Hardware-accelerated surface for efficient frame delivery