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 defaultSCContentSharingPickerConfigurationso user-facing pickers match macOS defaults.CMSampleBuffer::presenter_overlay_content_rect()(and the matching field onframe_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])andbgra_data_into(&mut [u8])— render into a caller-supplied buffer to amortise the per-callwidth*height*4byte allocation across many screenshots.- Native-BGRA fast path in
SCScreenshotManagerskips 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_imagenow returnsapple_cf::cg::CGImage.screencapturekit::cm::CMTimeis now a re-export ofapple_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 usedscreencapturekit's own types (via the prelude orscreencapturekit::{cg, cm}) the upgrade is typically just theCGRectfield-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'aof the wrapped audio buffer rather than the&selfborrow. This relaxes the borrow (the slice may now outlive the&selfreference), so existing call sites keep compiling unchanged.- Strided pixel render + locked
IOSurfaceCPU view. New additive helpersCGImageExt::rgba_data_into_strided/bgra_data_into_stridedrender 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 existingrgba_data_into/bgra_data_intopaths 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 Path | 1.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.x | 1.0 |
|---|---|
| N/A | async - Async API support |
| N/A | macos_13_0 - Audio capture |
| N/A | macos_14_0 - Screenshots, content picker |
| N/A | macos_14_2 - Menu bar, child windows |
| N/A | macos_15_0 - Recording, HDR, microphone |
| N/A | macos_15_2 - Screenshot in rect |
| N/A | macos_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:
| Deprecated | Replacement |
|---|---|
SCStreamConfiguration::builder() | SCStreamConfiguration::new() |
config.get_*() methods | config.*() (without get_ prefix) |
Quick Migration Checklist
- Update
Cargo.tomlto 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 returnResult) - Add feature flags for version-specific APIs
- Remove
get_prefix from getter calls - Update
SCContentFilter::build()to::create()