Migration Guide

June 18, 2026 · View on GitHub

This guide helps you migrate between major versions of screencapturekit-rs.

Note: The current release line is 7.x. The sections below document historical major-version migrations (the FFI-hardening work that started in 2.0). For changes in recent releases, see CHANGELOG.md.

Migrating from 1.x to 2.0

Version 2.0 hardens the FFI boundary. Most projects can upgrade by bumping the dependency and addressing a handful of compile errors — no design-level rework is required.

Cargo.toml

 [dependencies]
-screencapturekit = "1"
+screencapturekit = "2"

Send + Sync bound on output / delegate traits

SCStreamOutputTrait and SCStreamDelegateTrait (and the Fn(...) closure overloads) now require Send + Sync. Apple's dispatch queues may invoke the handler concurrently from arbitrary threads, so any state owned by the handler must be thread-safe.

Before (1.x):

struct Handler { count: std::cell::Cell<usize> }   // !Sync — compiles in 1.x
impl SCStreamOutputTrait for Handler { /* ... */ }

After (2.0):

use std::sync::atomic::{AtomicUsize, Ordering};

struct Handler { count: AtomicUsize }              // Send + Sync
impl SCStreamOutputTrait for Handler {
    fn did_output_sample_buffer(&self, _: CMSampleBuffer, _: SCStreamOutputType) {
        self.count.fetch_add(1, Ordering::Relaxed);
    }
}

For closures: replace Cell / Rc with Arc<Atomic*> / Arc<Mutex<...>> / Arc<RwLock<...>>.

PixelFormat::Unknown(FourCharCode)

PixelFormat is now #[non_exhaustive] and surfaces unrecognised codes via a new Unknown(FourCharCode) variant instead of mapping them to BGRA. This means every match over PixelFormat must include a wildcard arm:

Before (1.x):

match config.pixel_format() {
    PixelFormat::BGRA => { /* ... */ }
    PixelFormat::YCbCr420v => { /* ... */ }
    PixelFormat::YCbCr420f => { /* ... */ }
    PixelFormat::L10R => { /* ... */ }
}

After (2.0):

match config.pixel_format() {
    PixelFormat::BGRA => { /* ... */ }
    PixelFormat::YCbCr420v => { /* ... */ }
    PixelFormat::YCbCr420f => { /* ... */ }
    PixelFormat::L10R => { /* ... */ }
    PixelFormat::Unknown(code) => eprintln!("unrecognised pixel format: {code}"),
    _ => { /* future variants */ }
}

PartialEq / Hash are now normalised through FourCharCode, so two representations of the same OSType (e.g. PixelFormat::BGRA vs PixelFormat::Unknown(FourCharCode::from_bytes(*b"BGRA"))) compare equal.

SCStreamErrorCode is #[non_exhaustive]

match arms over SCStreamErrorCode (typically inside an SCError::StreamError { code, .. } arm) now require a wildcard:

match err_code {
    SCStreamErrorCode::UserStopped => { /* graceful */ }
    SCStreamErrorCode::UserDeclined => { /* permission */ }
    _ => { /* anything Apple adds in a future macOS */ }
}

Build-time SDK enforcement

The build script no longer silently degrades when xcrun / SDK detection fails — it bails with a clear error pointing at xcode-select. Make sure Xcode Command Line Tools are installed and selected:

xcode-select --install
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer

Every macos_* Cargo feature is also forwarded to the Swift compile, so a feature only enabled on the Rust side (which used to silently miss the matching Swift symbols) will now produce a coherent linker error.

New APIs you can opt into

  • SCContentSharingPicker::is_active() / set_is_active() — query and toggle the picker's idle state without recreating it.
  • SCContentSharingPicker::default_configuration() — read the system default SCContentSharingPickerConfiguration so user-facing pickers match macOS defaults.
  • CMSampleBuffer::presenter_overlay_content_rect() (and the matching field on frame_info()) — the new Presenter Overlay layout rect.

Migrating from 2.0 to 2.1

2.1 is fully backwards-compatible with 2.0 — no source changes required. New optional APIs:

  • CGImage::rgba_data_into(&mut [u8]) and bgra_data_into(&mut [u8]) — render into a caller-supplied buffer to amortise the per-call width*height*4 byte allocation across many screenshots.
  • Native-BGRA fast path in SCScreenshotManager skips the channel swap for downstreams that accept BGRA directly (Metal / wgpu / ffmpeg).

Migrating from 2.1 to 3.0

3.0 migrates the Core Graphics / Core Media / IOSurface / Core Video foundation types onto the shared apple-cf and apple-metal crates, eliminating screencapturekit's private nominal duplicates. Most affected types are now re-exports, so use screencapturekit::cg::CGRect; (or the prelude) keeps working unchanged.

The one source-level change: the ScreenCaptureKit-specific accessors on CMSampleBuffer moved to extension traits. Bring them into scope to call them:

use screencapturekit::cm::{CMSampleBufferExt, CMSampleBufferSCExt};
// now `sample.image_buffer()`, `sample.frame_status()`, … resolve

The prelude already re-exports both traits, so use screencapturekit::prelude::*; is enough.

Migrating from 3.x to 4.0

4.0 removes duplicated Core Media / Core Graphics value types from the public API in favour of the canonical apple-cf ones:

  • ScreenshotManager::capture_image now returns apple_cf::cg::CGImage.
  • screencapturekit::cm::CMTime is now a re-export of apple_cf::cm::CMTime.

If you previously converted between screencapturekit's types and apple-cf's when chaining into ImageIO / VideoToolbox, delete those conversions — the types are now identical.

Migrating from 4.0 to 5.0

5.0 adopts apple-cf 0.8's nested CGRect layout. Flat field access becomes nested through origin / size:

-let x = rect.x;
-let w = rect.width;
+let x = rect.origin.x;
+let w = rect.size.width;

The convenience constructor is unchanged — CGRect::new(x, y, w, h) still takes four flat coordinates.

Migrating from 5.0 to 6.0

6.0 re-exports the final Core Media timing types from apple-cf: screencapturekit::cm::{CMSampleTimingInfo, CMClock} are now re-exports of apple_cf::cm::{CMSampleTimingInfo, CMClock}. As with 4.0, drop any manual conversions between the previously-distinct types. No other source changes are required.

The 4.0 → 6.0 bumps are all driven by consolidating onto apple-cf; if your code only used screencapturekit's own types (via the prelude or screencapturekit::{cg, cm}) the upgrade is typically just the CGRect field-access change from 5.0.

Migrating from 6.0 to 7.0

7.0 is an FFI-hardening release with no required source changes for typical users. It is a major version only because of conservative semver around two low-level changes:

  • AudioBufferRef::data() lifetime. The returned slice is now tied to the lifetime 'a of the wrapped audio buffer rather than the &self borrow. This relaxes the borrow (the slice may now outlive the &self reference), so existing call sites keep compiling unchanged.
  • Strided pixel render + locked IOSurface CPU view. New additive helpers CGImageExt::rgba_data_into_strided / bgra_data_into_strided render into a caller-supplied buffer using an explicit row stride, so consumers with padded/row-aligned buffers (GPU upload, wgpu) aren't forced into tight packing. The existing rgba_data_into / bgra_data_into paths are unchanged.

Everything else is internal: MaybeUninit scratch buffers for batched FFI calls, null-checked constructors, and consolidated retain/release wrappers.

Migrating to the next major version (unreleased)

The async stream lifecycle methods are now genuinely asynchronous. Previously AsyncSCStream::start_capture / stop_capture / update_configuration / update_content_filter returned Result<(), SCError> and blocked the calling thread on a condition variable until ScreenCaptureKit acknowledged the operation — which stalls single-threaded / current-thread executors. They now return a StreamControlFuture you .await:

- stream.start_capture()?;
- stream.stop_capture()?;
+ stream.start_capture().await?;
+ stream.stop_capture().await?;
- stream.update_configuration(&config)?;
- stream.update_content_filter(&filter)?;
+ stream.update_configuration(&config).await?;
+ stream.update_content_filter(&filter).await?;

Awaiting now parks the task via its Waker and resumes from the Swift completion callback, so it never blocks the executor — matching the rest of the async_api (content queries, screenshots, picker, frame iteration) and the underlying Swift Task { try await … } entry points. The returned StreamControlFuture is Send, so it can be moved across tokio::spawn.

If you specifically want a blocking call (e.g. from synchronous code), reach through to the synchronous stream with stream.inner().start_capture() — the SCStream methods are unchanged.

Migrating from 0.x to 1.0

Version 1.0 introduced a complete API redesign with builder patterns, async support, and new macOS features.

Configuration API Changes

Before (0.x):

use screencapturekit::sc_stream_configuration::UnsafeSCStreamConfiguration;

let mut config = UnsafeSCStreamConfiguration::default();
config.set_width(1920);
config.set_height(1080);
config.set_shows_cursor(true);

After (1.0):

use screencapturekit::prelude::*;

let config = SCStreamConfiguration::new()
    .with_width(1920)
    .with_height(1080)
    .with_shows_cursor(true);

Content Filter API Changes

Before (0.x):

use screencapturekit::sc_content_filter::UnsafeSCContentFilter;

let filter = UnsafeSCContentFilter::new(display);

After (1.0):

use screencapturekit::prelude::*;

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

Stream Creation Changes

Before (0.x):

use screencapturekit::sc_stream::UnsafeSCStream;

let stream = UnsafeSCStream::new(filter, config, handler);
stream.start_capture();

After (1.0):

use screencapturekit::prelude::*;

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

Handler Trait Changes

Before (0.x):

impl StreamOutput for MyHandler {
    fn stream_output(&self, sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
        // process sample
    }
}

After (1.0):

impl SCStreamOutputTrait for MyHandler {
    fn did_output_sample_buffer(&self, sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
        // process sample
    }
}

Closure Handlers (New in 1.0)

You can now use closures instead of implementing traits:

stream.add_output_handler(
    |sample: CMSampleBuffer, output_type: SCStreamOutputType| {
        println!("Got frame!");
    },
    SCStreamOutputType::Screen
);

Error Handling

Before (0.x):

// Errors were often panics or Option<T>
let content = SCShareableContent::get().unwrap();

After (1.0):

// Proper Result<T, SCError> types
let content = SCShareableContent::get()?;

Module Path Changes

0.x Path1.0 Path
screencapturekit::sc_stream::*screencapturekit::stream::*
screencapturekit::sc_stream_configuration::*screencapturekit::stream::configuration::*
screencapturekit::sc_content_filter::*screencapturekit::stream::content_filter::*
screencapturekit::sc_shareable_content::*screencapturekit::shareable_content::*

Recommended: Use the prelude for common types:

use screencapturekit::prelude::*;

Feature Flag Changes

0.x1.0
N/Aasync - Async API support
N/Amacos_13_0 - Audio capture
N/Amacos_14_0 - Screenshots, content picker
N/Amacos_14_2 - Menu bar, child windows
N/Amacos_15_0 - Recording, HDR, microphone
N/Amacos_15_2 - Screenshot in rect
N/Amacos_26_0 - Advanced screenshot config

Migrating from 1.0 to 1.1

Builder Method Rename

Before (1.0.0):

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

After (1.1+):

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

Configuration Setters

Before (1.0.0):

let mut config = SCStreamConfiguration::new();
config.set_width(1920);  // Returns &mut Self
config.set_height(1080);

After (1.1+):

let config = SCStreamConfiguration::new()
    .with_width(1920)   // Chainable
    .with_height(1080);

Migrating from 1.1/1.2 to 1.3+

Content Picker API

Before (1.2):

// Blocking API
let result = SCContentSharingPicker::pick(&config)?;

After (1.3+):

// Callback-based API
SCContentSharingPicker::show(&config, |outcome| {
    match outcome {
        SCPickerOutcome::Picked(result) => { /* use result */ }
        SCPickerOutcome::Cancelled => { /* handle cancel */ }
        SCPickerOutcome::Error(e) => { /* handle error */ }
    }
});

// Or async (with async feature)
let outcome = AsyncSCContentSharingPicker::show(&config).await;

Getter Method Naming

The get_ prefix was removed from getters:

Before:

let width = config.get_width();
let rect = filter.get_content_rect();
let time = sample.get_presentation_timestamp();

After:

let width = config.width();
let rect = filter.content_rect();
let time = sample.presentation_timestamp();

CMSampleBuffer Methods

Before:

sample.get_image_buffer()
sample.get_format_description()

After:

sample.image_buffer()
sample.format_description()

Deprecated APIs

The following APIs are deprecated and will be removed in future versions:

DeprecatedReplacement
SCStreamConfiguration::builder()SCStreamConfiguration::new()
config.get_*() methodsconfig.*() (without get_ prefix)

Quick Migration Checklist

  • Update Cargo.toml to new version
  • Replace Unsafe* types with safe equivalents
  • Use prelude::* for common imports
  • Replace trait implementations with closures (optional)
  • Update handler trait name to SCStreamOutputTrait
  • Add ? for error handling (functions now return Result)
  • Add feature flags for version-specific APIs
  • Remove get_ prefix from getter calls
  • Update SCContentFilter::build() to ::create()