screen_capture_kit 1.0.0
screen_capture_kit: ^1.0.0 copied to clipboard
Native Dart bindings for macOS ScreenCaptureKit using Dart Build Hooks.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased #
1.0.0 - 2026-03-21 #
First stable release under Semantic Versioning. Prior 0.0.x releases were pre-release; breaking API changes since 0.0.5 are summarized under Changed below.
Added #
CaptureResolution(automatic,best,nominal) andcaptureScreenshot(..., captureResolution:)— maps toSCStreamConfiguration.captureResolutionforSCScreenshotManager.captureImageon macOS 14+.- Live
captureResolution—startCaptureStream/startCaptureStreamWithUpdatercaptureResolution:andStreamConfiguration.captureResolutionset the same property onSCStreamConfigurationfor running streams (macOS 14+). SCStreamDelegate→ Dart: OptionalemitDelegateEvents: trueonstartCaptureStream/startCaptureStreamWithUpdater;CaptureStream.delegateEventsdeliversCaptureStreamDelegateEvent(didStopWithError,outputVideoEffectDidStart,outputVideoEffectDidStop) from the macOS native bridge.
Changed #
- Example record CLIs (
record_display,record_display_with_audio,record_picker_with_audio):--quality/-q(automatic|best|nominal) for livecaptureResolution, aligned withscreenshot_display(defaultautomatic; macOS 14+). - macOS capture streams (internal): Video, audio, microphone, and delegate
StreamControllerinstances are owned by small wrappers with explicitclose(), so lifecycle matches subscription teardown without relying on// ignore: close_sinksfor the main capture setup path. - Example CLIs (
screenshot_display,record_picker_with_audio): When--width/--heightare omitted, use the selected or reference display pixel size (captureScreenshotnow passesFrameSizefor the chosen display;record_picker_with_audiodefaults to a reference display: highest known refresh rate, then largest area).record_display/record_display_with_audioalready defaulted to the chosen display’s size when both are omitted. - Breaking: Identifier extension types
DisplayId,FilterId,ProcessId, andWindowIdno longer define getters that duplicated the primary field (e.g.filterId,displayId). Usevaluefor the underlyingint, consistent withBundleIdusingvaluefor theString. - Breaking:
DisplayId,FilterId, andWindowIdfactories requirevalue > 0;ProcessIdrequiresvalue >= 0. Invalid values throwArgumentError. These types are no longerconst-constructible (use e.g.DisplayId(1)). PixelRect: Factory validates finitex,y,width,heightand non-negativewidth/height; throwsArgumentErrorotherwise.
Fixed #
- macOS audio JSON (
_parseAudioJson): When native sendsformat: raw(e.g. non–Linear PCM path in the bridge), the Dart layer now maps it tof32ors16usingnumSamples,channelCount, and PCM byte length soCapturedAudio.formatis always a concrete label for interleaved PCM. - Example
pcm_wav_writer: Float32 samples written to WAV are sanitized (non-finite values →0; amplitudes clamped to [-1, 1]) sorecord_display_with_audiocan mux to AAC/MP4 with ffmpeg when float magnitudes exceed unity. - Microphone 24-bit PCM (macOS): ScreenCaptureKit microphone on macOS delivers 24-bit signed integer packed PCM (
s24le), not float32. The native bridge (BuildInterleavedPCM) now detects this format and converts to float32 (normalized to [-1, 1]) before sending to Dart. Previously the raw 3-byte-per-sample data was misinterpreted as float32, producing noise/garbage audio in the WAV output.
0.0.5 - 2026-03-21 #
Added #
- Example CLIs: Added
screenshot_display(single-display PNG viacaptureScreenshot),record_display(uncompressed BGRA AVI, Dart-only), andrecord_display_with_audio(AVI + PCM WAV → MP4 via ffmpeg) with shared helpersavi_isolate_recorderandpcm_wav_writer. - Example
record_picker_with_audio: CLI usingpresentContentSharingPickerwith FPS capped to display refresh (default 120),--audio none|system|mic|both, and ffmpeg mux to MP4. Optional fixed--width/--height; otherwise AVI dimensions follow the first captured frame (deferDimensionsFromFirstFrameon the shared isolate AVI writer). CaptureStream.flushPendingAudio: Drains native audio queues (system and/or microphone) after the FFI poll loop has stopped. Call after cancelingaudioStream/microphoneStreamsubscriptions and before finalizing WAV files so tail PCM buffers are not discarded.- Audio sample timestamps (macOS): Native JSON now includes optional
presentationTimeSecondsanddurationSecondsfrom each audioCMSampleBuffer(system + microphone).CapturedAudioexposes these for timeline-aligned consumers. CapturedAudio.frameCount: Optional frame count from native JSONnumSamples(CMSampleBufferGetNumSamples) when the macOS bridge provides it.BundleId:extension typewrapping an application bundle identifier (String); used byRunningApplication.bundleIdentifierandContentSharingPickerConfiguration.excludedBundleIds.DisplayRefreshRate:extension typefor a display refresh rate in whole Hz (unknownsentinel or validated1..480), populated from shareable content (CGDisplayModeGetRefreshRate). Reflected inDisplayequality,hashCode, andtoString.
Changed #
- Breaking:
RunningApplication.bundleIdentifierisBundleId(notString).ContentSharingPickerConfiguration.excludedBundleIdsisList<BundleId>?(notList<String>?). Display.refreshRatetype isDisplayRefreshRateinstead ofdouble; useDisplayRefreshRate.fromNumwhen building from raw values.- Stream video (macOS):
stream_get_next_framereturns a malloc'd buffer with a small binary header (width, height,bytesPerRow, data size) plus raw BGRA pixels, instead of a JSON string with base64 payload. NativeStreamFrameHandlerkeeps a bounded queue of pending video frames. - Dart macOS bridge:
startCaptureStream/startCaptureStreamWithUpdatervideo delivery uses the raw frame path and drains multiple frames per event-loop turn (capped batch + yields) so capture throughput stays high while the isolate/event loop can still make progress. - Example
record_display_with_audio: System and microphone WAVs are written sequentially in capture order again. PTS-based placement had been able to skip real microphone PCM when overlap trimming thought the cursor was ahead of the buffer's presentation time—sequential append keeps every native chunk. OptionalCapturedAudiotimestamps remain available for custom alignment in other apps. - Audio FFI polling (macOS): Increased per-tick audio JSON batch drain limit (
_kMaxAudioChunksPerPollBatch24 → 96) so the isolate is less likely to lag the microphone queue.
Removed #
- Example
example.dart: Removed the interactive kitchen-sink demo. Display/window/region capture and screenshots are covered by the dedicated CLI examples (screenshot_display,record_display, etc.).
Fixed #
presentContentSharingPickerdeadlock (macOS): The API was wrapped inIsolate.run, so nativepicker_presentran on a worker thread while the bridge useddispatch_sync(main_queue, …)— the main isolate waited for the worker and the worker waited for the main queue. The picker now runspresentContentSharingPickerImplon the calling isolate (same pattern as UI requires for AppKit).- Picker
nextEventMatchingMaskcrash (macOS):picker_startcalled-[NSApplication nextEventMatchingMask:…]from a Dart worker thread, which is restricted to the main thread. Replaced the AppKit event loop with sleep-polling; observer callbacks are delivered by the system via GCD and do not require explicit event pumping. - Content filter registry (macOS):
ensureFilterRegistryused[NSMutableDictionary dictionary](autoreleased); the GCD thread's autorelease pool could drain the dictionary before the Dart thread read it. Switched to[[NSMutableDictionary alloc] init]for direct ownership. Also added@synchronizedtoget_content_filterfor thread-safe reads. - Native content-sharing picker API (macOS): Used Objective-C API
+[SCContentSharingPicker sharedPicker]anddefaultConfiguration.allowedPickerModes+presentinstead of the nonexistent+[SCContentSharingPicker shared]andpresentUsing:(Swift-only names), which causedNSInvalidArgumentExceptionat runtime. - SCContentSharingPicker UI (macOS): Set
picker.active = YESbeforepresent(required by Apple's header: the picker UI does not appear otherwise). For CLI tools, setNSApplicationactivation policy to Accessory and callactivateIgnoringOtherApps:so Control Center can show the picker. - Audio FFI
timeout_ms == 0(macOS):stream_get_next_audioandstream_get_next_microphonetreated0as a 5 second wait instead of a non-blocking poll (unlikestream_get_next_frame). That stalled the event loop and could starve microphone delivery when queues ran dry between chunks. - Audio shutdown drain: Canceling
audioStream/microphoneStreamsubscriptions stopped the Dart poll loop while native queues could still hold PCM data. CallCaptureStream.flushPendingAudioafter canceling audio subscriptions and before finalizing WAVs so tail buffers are not discarded. The flusher uses capped bursts and yields so it cannot starve timers or break--durationwhile capture is still live. - Example
record_display_with_audio: After capture, pad the microphone WAV with trailing silence when system audio is stereo Float32 and the mic is mono Float32, so*_mic.wavduration matches*_system.wav. ScreenCaptureKit often emits fewer samples per microphoneCMSampleBufferthan per system-audio buffer while callback counts stay paired, which previously produced about half the mic wall-clock in raw PCM. - Audio capture (macOS): Non-interleaved (planar) PCM from
SCStreamOutputTypeAudio/Microphoneno longer uses only the firstAudioBuffer; channels are interleaved so stereo WAV/ffmpeg mux matches real duration (fixes playback sounding 2× fast). Planar layout is also detected whenkAudioFormatFlagIsNonInterleavedis not set butmNumberBuffers == mChannelsPerFrameand each buffer is single-channel (fixes mic-only half duration after mux with system audio). - Audio + microphone Dart polling: Replaced 100 ms blocking FFI reads with short timeouts and per-tick batch draining (same idea as video frames), so system-audio polling no longer starves the microphone on the same isolate—avoids mic dropping out mid-recording.
- Dual audio poll loop: When both system and microphone capture are enabled, a single fair round-robin scheduler drains both streams (shared batch cap lowered to 24 chunks/tick) so independent timers no longer monopolize the isolate and starve video or the other audio stream.
- Microphone PCM (macOS): Mono samples split across multiple 1-channel
AudioBuffers are now concatenated (previously onlymBuffers[0]was copied, roughly halving mic WAV duration vs system audio). Edge cases whereCMSampleBufferGetNumSamplesimplies more bytes than written are handled by reading fromCMSampleBufferGetDataBufferwhen present.
0.0.4 - 2026-03-20 #
Changed #
- Breaking type updates: stream configuration and stream APIs now use
FrameRate(1..120) andQueueDepth(1..8) value objects instead of rawintforframeRate/queueDepth; invalid values throwArgumentError. - Breaking API naming updates: stream output dimension parameter names were renamed from
outputSizetoframeSizeacrosscaptureScreenshot,startCaptureStream,startCaptureStreamWithUpdater, andStreamConfiguration.
0.0.3 - 2026-03-19 #
Removed #
ScreenCaptureKitPortandScreenCaptureKitImpl: the public entry type isScreenCaptureKitonly. Tests may useimplements ScreenCaptureKitor a mocking package (see class documentation).ContentFilterHandle: useFilterIddirectly forcaptureScreenshot, stream APIs, andreleaseFilter.
Changed #
- Domain layout:
entities/,value_objects/(geometry/,identifiers/,capture/), anderrors/; shareable-content types and capture configuration types (ContentFilter,StreamConfiguration, picker types, etc.) are grouped under domain value objects where appropriate. - Breaking type updates: entities use
DisplayId,WindowId,ProcessId,FrameSize, andPixelRectinstead of raw primitives;CapturedFrameandCapturedImageexposeFrameSizefor dimensions. - Capture output sizing API updates:
captureScreenshot,startCaptureStream, andstartCaptureStreamWithUpdatertakeoutputSize: FrameSizeinstead of separatewidth/heightintegers;StreamConfigurationusesoutputSizefor stream output dimensions. - FrameSize validation for capture output: for capture output requests, valid sizes are
FrameSize.zero(0×0, native default) or both dimensions strictly positive; mixed0/positive pairs throwArgumentError.
Fixed #
- Native bridge: safer cross-thread transfer for frame metadata (C strings) and corrected
@availablebranching in the stream path.
0.0.2 - 2026-03-18 #
Fixed #
- Stream SEGV on cancel: call
removeStreamOutputbeforestopCaptureso no callbacks run during teardown, preventing crash when subscription is cancelled on macOS.
Changed #
- Layered architecture: domain, application, infrastructure, and presentation layers with ports and adapters.
- Domain: value objects for IDs and geometry (
DisplayId,WindowId,FilterId,PixelRect,FrameSize), entities with@immutable. - Application:
ScreenCaptureKitPortinterface andScreenCaptureKitImplimplementation. - Infrastructure: native bridge (FFI) and stub implementations moved into infrastructure layer.
- Presentation: configuration and DTO-like types (
ContentFilter,StreamConfiguration, etc.) grouped in presentation layer.
0.0.1 - 2026-03-14 #
Added #
- Shareable content API:
getShareableContent()returning displays, applications, and windows (Display,RunningApplication,Window,ShareableContent). - Content filters:
createWindowFilter(Window),createDisplayFilter(Display, {excludingWindows}),releaseFilter(FilterId). - Display and window capture:
startCaptureStream()andstartCaptureStreamWithUpdater()with configurable width, height, frame rate, source rect (region capture), cursor visibility, queue depth, and optional system audio (macOS 13+) and microphone (macOS 15+). - Runtime updates:
CaptureStream.updateConfiguration(),CaptureStream.updateContentFilter()for changing stream config or filter without stopping. - Screenshot:
captureScreenshot(FilterId, {width, height})(macOS 14+). - System content-sharing picker (macOS 14+):
presentContentSharingPicker({allowedModes}),ContentSharingPickerModeenum,ContentSharingPickerConfiguration, andCaptureStream.setContentSharingPickerConfiguration()for per-stream picker config. - Exception type:
ScreenCaptureKitExceptionwith optional domain and code from native errors. - Stub implementation on non-macOS platforms (throws
UnsupportedError). - Dart SDK constraint
^3.10.0and dependency set (code_assets, ffi, hooks, meta, native_toolchain_c; mocktail, test for dev).