Skip to main content

screencapturekit/
async_api.rs

1//! Async API for `ScreenCaptureKit`
2//!
3//! This module provides async versions of operations when the `async` feature is enabled.
4//! The async API is **executor-agnostic** and works with any async runtime (Tokio, async-std, smol, etc.).
5//!
6//! ## Available Types
7//!
8//! | Type | Description |
9//! |------|-------------|
10//! | [`AsyncSCShareableContent`] | Async content queries |
11//! | [`AsyncSCStream`] | Async stream with frame iteration |
12//! | [`AsyncSCScreenshotManager`] | Async screenshot capture (macOS 14.0+) |
13//! | [`AsyncSCContentSharingPicker`] | Async content picker UI (macOS 14.0+) |
14//! | [`AsyncSCRecordingOutput`] | Async recording with events (macOS 15.0+) |
15//!
16//! ## Runtime Agnostic Design
17//!
18//! This async API uses only `std` types and works with **any** async runtime:
19//! - Uses callback-based Swift FFI for true async operations
20//! - Uses `std::sync::{Arc, Mutex}` for synchronization
21//! - Uses `std::task::{Poll, Waker}` for async primitives
22//! - Uses `std::future::Future` trait
23//!
24//! ## Examples
25//!
26//! ### Basic Async Content Query
27//!
28//! ```rust,no_run
29//! # #[tokio::main]
30//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! use screencapturekit::async_api::AsyncSCShareableContent;
32//!
33//! let content = AsyncSCShareableContent::get().await?;
34//! println!("Found {} displays", content.displays().len());
35//! println!("Found {} windows", content.windows().len());
36//! # Ok(())
37//! # }
38//! ```
39//!
40//! ### Async Stream with Frame Iteration
41//!
42//! ```rust,no_run
43//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
44//! use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
45//! use screencapturekit::stream::configuration::SCStreamConfiguration;
46//! use screencapturekit::stream::content_filter::SCContentFilter;
47//! use screencapturekit::stream::output_type::SCStreamOutputType;
48//!
49//! let content = AsyncSCShareableContent::get().await?;
50//! let display = &content.displays()[0];
51//! let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
52//! let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
53//!
54//! let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
55//! stream.start_capture().await?;
56//!
57//! // Process frames asynchronously
58//! for _ in 0..100 {
59//!     if let Some(frame) = stream.next().await {
60//!         println!("Got frame at {:?}", frame.presentation_timestamp());
61//!     }
62//! }
63//!
64//! stream.stop_capture().await?;
65//! # Ok(())
66//! # }
67//! ```
68
69use crate::error::SCError;
70use crate::shareable_content::SCShareableContent;
71use crate::stream::configuration::SCStreamConfiguration;
72use crate::stream::content_filter::SCContentFilter;
73use crate::stream::output_type::SCStreamOutputType;
74use crate::utils::completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
75use std::ffi::c_void;
76use std::future::Future;
77use std::pin::Pin;
78use std::sync::{Arc, Mutex};
79use std::task::{Context, Poll, Waker};
80
81// ============================================================================
82// AsyncSCShareableContent - True async with callback-based FFI
83// ============================================================================
84
85/// Callback from Swift FFI for shareable content
86extern "C" fn shareable_content_callback(
87    content: *const c_void,
88    error: *const i8,
89    user_data: *mut c_void,
90) {
91    crate::utils::panic_safe::catch_user_panic("shareable_content_callback", move || {
92        if !error.is_null() {
93            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
94            let error_msg = unsafe { error_from_cstr(error) };
95            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
96            unsafe { AsyncCompletion::<SCShareableContent>::complete_err(user_data, error_msg) };
97        } else if !content.is_null() {
98            // SAFETY: `content` is non-null (checked above) and is a valid `SCShareableContent` pointer retained for us by the Swift completion handler.
99            let sc = unsafe { SCShareableContent::from_ptr(content) };
100            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
101            unsafe { AsyncCompletion::complete_ok(user_data, sc) };
102        } else {
103            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
104            unsafe {
105                AsyncCompletion::<SCShareableContent>::complete_err(
106                    user_data,
107                    "Unknown error".to_string(),
108                );
109            };
110        }
111    });
112}
113
114/// Future for async shareable content retrieval
115pub struct AsyncShareableContentFuture {
116    inner: AsyncCompletionFuture<SCShareableContent>,
117}
118
119impl std::fmt::Debug for AsyncShareableContentFuture {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.debug_struct("AsyncShareableContentFuture")
122            .finish_non_exhaustive()
123    }
124}
125
126impl Future for AsyncShareableContentFuture {
127    type Output = Result<SCShareableContent, SCError>;
128
129    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
130        Pin::new(&mut self.inner)
131            .poll(cx)
132            .map(|r| r.map_err(SCError::NoShareableContent))
133    }
134}
135
136/// Async wrapper for `SCShareableContent`
137///
138/// Provides async methods to retrieve displays, windows, and applications
139/// without blocking. **Executor-agnostic** - works with any async runtime.
140#[derive(Debug, Clone, Copy)]
141pub struct AsyncSCShareableContent;
142
143impl AsyncSCShareableContent {
144    /// Asynchronously get the shareable content (displays, windows, applications)
145    ///
146    /// Uses callback-based Swift FFI for true async operation.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if:
151    /// - Screen recording permission is not granted
152    /// - The system fails to retrieve shareable content
153    pub fn get() -> AsyncShareableContentFuture {
154        Self::create().get()
155    }
156
157    /// Create options builder for customizing shareable content retrieval
158    #[must_use]
159    pub fn create() -> AsyncSCShareableContentOptions {
160        AsyncSCShareableContentOptions::default()
161    }
162}
163
164/// Options for async shareable content retrieval
165#[derive(Default, Debug, Clone, PartialEq, Eq)]
166pub struct AsyncSCShareableContentOptions {
167    exclude_desktop_windows: bool,
168    on_screen_windows_only: bool,
169}
170
171impl AsyncSCShareableContentOptions {
172    /// Exclude desktop windows from the shareable content
173    #[must_use]
174    pub fn with_exclude_desktop_windows(mut self, exclude: bool) -> Self {
175        self.exclude_desktop_windows = exclude;
176        self
177    }
178
179    /// Include only on-screen windows in the shareable content
180    #[must_use]
181    pub fn with_on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
182        self.on_screen_windows_only = on_screen_only;
183        self
184    }
185
186    /// Asynchronously get the shareable content with these options
187    pub fn get(self) -> AsyncShareableContentFuture {
188        let (future, context) = AsyncCompletion::create();
189
190        // SAFETY: `context` is a valid one-shot completion pointer created by `AsyncCompletion::create()`; the Swift layer invokes the callback exactly once, after which the pointer is consumed.
191        unsafe {
192            crate::ffi::sc_shareable_content_get_with_options(
193                self.exclude_desktop_windows,
194                self.on_screen_windows_only,
195                shareable_content_callback,
196                context,
197            );
198        }
199
200        AsyncShareableContentFuture { inner: future }
201    }
202
203    /// Asynchronously get shareable content with only windows below a reference window
204    ///
205    /// This returns windows that are stacked below the specified reference window
206    /// in the window layering order.
207    ///
208    /// # Arguments
209    ///
210    /// * `reference_window` - The window to use as the reference point
211    pub fn below_window(
212        self,
213        reference_window: &crate::shareable_content::SCWindow,
214    ) -> AsyncShareableContentFuture {
215        let (future, context) = AsyncCompletion::create();
216
217        // SAFETY: `context` is a valid one-shot completion pointer created by `AsyncCompletion::create()`; the Swift layer invokes the callback exactly once, after which the pointer is consumed.
218        unsafe {
219            crate::ffi::sc_shareable_content_get_below_window(
220                self.exclude_desktop_windows,
221                reference_window.as_ptr(),
222                shareable_content_callback,
223                context,
224            );
225        }
226
227        AsyncShareableContentFuture { inner: future }
228    }
229
230    /// Asynchronously get shareable content with only windows above a reference window
231    ///
232    /// This returns windows that are stacked above the specified reference window
233    /// in the window layering order.
234    ///
235    /// # Arguments
236    ///
237    /// * `reference_window` - The window to use as the reference point
238    pub fn above_window(
239        self,
240        reference_window: &crate::shareable_content::SCWindow,
241    ) -> AsyncShareableContentFuture {
242        let (future, context) = AsyncCompletion::create();
243
244        // SAFETY: `context` is a valid one-shot completion pointer created by `AsyncCompletion::create()`; the Swift layer invokes the callback exactly once, after which the pointer is consumed.
245        unsafe {
246            crate::ffi::sc_shareable_content_get_above_window(
247                self.exclude_desktop_windows,
248                reference_window.as_ptr(),
249                shareable_content_callback,
250                context,
251            );
252        }
253
254        AsyncShareableContentFuture { inner: future }
255    }
256}
257
258impl AsyncSCShareableContent {
259    /// Asynchronously get shareable content for the current process only (macOS 14.4+)
260    ///
261    /// This retrieves content that the current process can capture without
262    /// requiring user authorization via TCC (Transparency, Consent, and Control).
263    ///
264    /// # Examples
265    ///
266    /// ```rust,no_run
267    /// # #[tokio::main]
268    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
269    /// use screencapturekit::async_api::AsyncSCShareableContent;
270    ///
271    /// // Get content capturable without TCC authorization
272    /// let content = AsyncSCShareableContent::current_process().await?;
273    /// println!("Found {} windows for current process", content.windows().len());
274    /// # Ok(())
275    /// # }
276    /// ```
277    #[cfg(feature = "macos_14_4")]
278    pub fn current_process() -> AsyncShareableContentFuture {
279        let (future, context) = AsyncCompletion::create();
280
281        // SAFETY: `context` is a valid one-shot completion pointer created by `AsyncCompletion::create()`; the Swift layer invokes the callback exactly once, after which the pointer is consumed.
282        unsafe {
283            crate::ffi::sc_shareable_content_get_current_process_displays(
284                shareable_content_callback,
285                context,
286            );
287        }
288
289        AsyncShareableContentFuture { inner: future }
290    }
291}
292
293// ============================================================================
294// AsyncSCStream - Async stream with integrated frame iteration
295// ============================================================================
296
297/// Async iterator over sample buffers.
298///
299/// # Delivery semantics
300///
301/// This is a **lossy, bounded** buffer. When the queue is full (`capacity`
302/// reached) and a new sample arrives faster than the consumer polls it,
303/// the **oldest** buffered sample is dropped to make room for the newest
304/// (drop-oldest policy). This keeps latency low and favors fresh frames over
305/// stale ones, but means consumers that fall behind will miss intermediate
306/// frames rather than blocking the capture callback.
307struct AsyncSampleIteratorState {
308    buffer: std::collections::VecDeque<(crate::cm::CMSampleBuffer, SCStreamOutputType)>,
309    waker: Option<Waker>,
310    closed: bool,
311    capacity: usize,
312    stop_error: Option<SCError>,
313}
314
315/// Internal sender for async sample iterator
316struct AsyncSampleSender {
317    inner: Arc<Mutex<AsyncSampleIteratorState>>,
318}
319
320impl crate::stream::output_trait::SCStreamOutputTrait for AsyncSampleSender {
321    fn did_output_sample_buffer(
322        &self,
323        sample_buffer: crate::cm::CMSampleBuffer,
324        of_type: SCStreamOutputType,
325    ) {
326        let Ok(mut state) = self.inner.lock() else {
327            return;
328        };
329
330        // Drop oldest if at capacity
331        if state.buffer.len() >= state.capacity {
332            state.buffer.pop_front();
333        }
334
335        state.buffer.push_back((sample_buffer, of_type));
336
337        if let Some(waker) = state.waker.take() {
338            waker.wake();
339        }
340    }
341}
342
343impl Drop for AsyncSampleSender {
344    fn drop(&mut self) {
345        if let Ok(mut state) = self.inner.lock() {
346            state.closed = true;
347            if let Some(waker) = state.waker.take() {
348                waker.wake();
349            }
350        }
351    }
352}
353
354/// Shared poll logic for the sample futures/streams: pop the next buffered
355/// `(buffer, type)` pair, resolve to `None` when closed, or register the waker.
356fn poll_next_sample(
357    state: &Arc<Mutex<AsyncSampleIteratorState>>,
358    cx: &Context<'_>,
359) -> Poll<Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)>> {
360    let Ok(mut state) = state.lock() else {
361        return Poll::Ready(None);
362    };
363
364    if let Some(sample) = state.buffer.pop_front() {
365        return Poll::Ready(Some(sample));
366    }
367
368    if state.closed {
369        Poll::Ready(None)
370    } else {
371        // Avoid the lost-wakeup race: when the same future/stream is polled by
372        // a different task (e.g. moved between `tokio::select!` arms), the waker
373        // changes. `will_wake` skips the clone when the executor reuses the same
374        // waker; the explicit assignment guarantees the latest waker is the one
375        // a future sample arrival will wake.
376        let waker = cx.waker();
377        match state.waker {
378            Some(ref existing) if existing.will_wake(waker) => {}
379            _ => state.waker = Some(waker.clone()),
380        }
381        Poll::Pending
382    }
383}
384
385/// Future for getting the next sample buffer
386pub struct NextSample<'a> {
387    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
388}
389
390impl std::fmt::Debug for NextSample<'_> {
391    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392        f.debug_struct("NextSample").finish_non_exhaustive()
393    }
394}
395
396impl Future for NextSample<'_> {
397    type Output = Option<crate::cm::CMSampleBuffer>;
398
399    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
400        poll_next_sample(self.state, cx).map(|opt| opt.map(|(buffer, _of_type)| buffer))
401    }
402}
403
404/// Future for getting the next sample buffer together with its output type.
405///
406/// Like [`NextSample`], but yields the [`SCStreamOutputType`] alongside the
407/// buffer so consumers of a multi-output stream (e.g. screen + audio via
408/// [`AsyncSCStream::add_output_type`]) can tell frames apart. Returned by
409/// [`AsyncSCStream::next_typed`].
410pub struct NextSampleTyped<'a> {
411    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
412}
413
414impl std::fmt::Debug for NextSampleTyped<'_> {
415    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416        f.debug_struct("NextSampleTyped").finish_non_exhaustive()
417    }
418}
419
420impl Future for NextSampleTyped<'_> {
421    type Output = Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)>;
422
423    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
424        poll_next_sample(self.state, cx)
425    }
426}
427
428/// A [`Stream`](futures_core::Stream) of captured sample buffers.
429///
430/// Yields `CMSampleBuffer`s and ends (`None`) when the stream closes. Returned
431/// by [`AsyncSCStream::frames`]; it borrows the stream and is `Unpin`, so it
432/// plugs straight into the `futures::StreamExt` combinators:
433///
434/// ```no_run
435/// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
436/// use futures_util::StreamExt;
437/// let first_ten: Vec<_> = stream.frames().take(10).collect().await;
438/// # let _ = first_ten;
439/// # }
440/// ```
441pub struct SampleStream<'a> {
442    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
443}
444
445impl std::fmt::Debug for SampleStream<'_> {
446    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447        f.debug_struct("SampleStream").finish_non_exhaustive()
448    }
449}
450
451impl futures_core::Stream for SampleStream<'_> {
452    type Item = crate::cm::CMSampleBuffer;
453
454    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
455        poll_next_sample(self.state, cx).map(|opt| opt.map(|(buffer, _of_type)| buffer))
456    }
457}
458
459/// A [`Stream`](futures_core::Stream) of captured sample buffers tagged with
460/// their [`SCStreamOutputType`].
461///
462/// Like [`SampleStream`] but yields `(CMSampleBuffer, SCStreamOutputType)` so a
463/// multi-output stream's audio and video can be told apart. Returned by
464/// [`AsyncSCStream::frames_typed`].
465pub struct TypedSampleStream<'a> {
466    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
467}
468
469impl std::fmt::Debug for TypedSampleStream<'_> {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        f.debug_struct("TypedSampleStream").finish_non_exhaustive()
472    }
473}
474
475impl futures_core::Stream for TypedSampleStream<'_> {
476    type Item = (crate::cm::CMSampleBuffer, SCStreamOutputType);
477
478    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
479        poll_next_sample(self.state, cx)
480    }
481}
482
483// SAFETY: `AsyncSampleSender` holds `Arc<Mutex<AsyncSampleIteratorState>>`.
484// `AsyncSampleIteratorState` buffers `(CMSampleBuffer, SCStreamOutputType)`
485// pairs plus an `Option<Waker>` and `Option<SCError>`; `CMSampleBuffer` has its
486// own `unsafe impl Send` (it is an Apple-owned handle safe to transfer across
487// threads) and the rest are `Send + Sync`, so the whole `Arc<Mutex<...>>` is
488// safe to send and share across threads.
489unsafe impl Send for AsyncSampleSender {}
490unsafe impl Sync for AsyncSampleSender {}
491
492/// Stream delegate for [`AsyncSCStream`] that closes the sample iterator when
493/// `ScreenCaptureKit` stops the stream with an error.
494///
495/// Without this, a stream that fails mid-capture (captured display
496/// disconnected, permission revoked, …) would leave [`NextSample`] pending
497/// forever. On an error stop this records the [`SCError`], marks the iterator
498/// closed (so `next()` resolves to `None` once buffered frames drain), and
499/// wakes any parked task. The error is retrievable via
500/// [`AsyncSCStream::take_error`].
501struct AsyncStreamDelegate {
502    state: Arc<Mutex<AsyncSampleIteratorState>>,
503}
504
505impl crate::stream::delegate_trait::SCStreamDelegateTrait for AsyncStreamDelegate {
506    fn did_stop_with_error(&self, error: SCError) {
507        if let Ok(mut state) = self.state.lock() {
508            state.stop_error = Some(error);
509            state.closed = true;
510            if let Some(waker) = state.waker.take() {
511                waker.wake();
512            }
513        }
514    }
515}
516
517// SAFETY: mirrors `AsyncSampleSender` — `AsyncStreamDelegate` holds the same
518// `Arc<Mutex<AsyncSampleIteratorState>>`, whose contents (`CMSampleBuffer`,
519// `Waker`, `SCError`) are all safe to send and share across threads.
520unsafe impl Send for AsyncStreamDelegate {}
521unsafe impl Sync for AsyncStreamDelegate {}
522
523// ----------------------------------------------------------------------------
524// Stream lifecycle control futures (start / stop / update)
525// ----------------------------------------------------------------------------
526
527/// FFI completion callback for [`AsyncSCStream`] lifecycle operations.
528///
529/// Translates the Swift `(context, success, message)` completion into the
530/// waker-based [`AsyncCompletion`] machinery, so awaiting a control future
531/// resumes the task via its [`Waker`] instead of parking a thread. This is the
532/// same primitive used by the content / screenshot / picker futures.
533extern "C" fn stream_control_callback(context: *mut c_void, success: bool, msg: *const i8) {
534    crate::utils::panic_safe::catch_user_panic("stream_control_callback", move || {
535        if success {
536            // SAFETY: `context` is the one-shot completion pointer from
537            // `AsyncCompletion::<()>::create()`; Swift invokes this callback
538            // exactly once, after which the pointer is consumed.
539            unsafe { AsyncCompletion::<()>::complete_ok(context, ()) };
540        } else {
541            let error = unsafe { error_from_cstr(msg) };
542            // SAFETY: see above — one-shot completion pointer, fired once.
543            unsafe { AsyncCompletion::<()>::complete_err(context, error) };
544        }
545    });
546}
547
548/// Future for an [`AsyncSCStream`] lifecycle operation — `start_capture`,
549/// `stop_capture`, `update_configuration`, or `update_content_filter`.
550///
551/// Resolves once `ScreenCaptureKit` acknowledges the operation. Awaiting it
552/// **never blocks the executor thread**: the task is parked via its [`Waker`]
553/// and resumed from the Swift completion callback. This mirrors the underlying
554/// Swift `Task { try await … }` entry points, keeping the async surface
555/// consistent end to end.
556///
557/// The operation is kicked off eagerly when the method is called (a "hot"
558/// future), matching the rest of this module — e.g.
559/// [`AsyncSCShareableContent::get`]. Dropping the future without awaiting is
560/// safe; it simply means success/failure is not observed.
561#[must_use = "the operation starts eagerly, but you must .await the future to observe success or failure"]
562pub struct StreamControlFuture {
563    inner: AsyncCompletionFuture<()>,
564    map_err: fn(String) -> SCError,
565}
566
567impl std::fmt::Debug for StreamControlFuture {
568    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569        f.debug_struct("StreamControlFuture")
570            .finish_non_exhaustive()
571    }
572}
573
574impl Future for StreamControlFuture {
575    type Output = Result<(), SCError>;
576
577    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
578        let map_err = self.map_err;
579        Pin::new(&mut self.inner)
580            .poll(cx)
581            .map(|r| r.map_err(map_err))
582    }
583}
584
585/// Async wrapper for `SCStream` with integrated frame iteration
586///
587/// Provides async methods for stream lifecycle and frame iteration.
588/// **Executor-agnostic** - works with any async runtime.
589///
590/// # Examples
591///
592/// ```rust,no_run
593/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
594/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
595/// use screencapturekit::stream::configuration::SCStreamConfiguration;
596/// use screencapturekit::stream::content_filter::SCContentFilter;
597/// use screencapturekit::stream::output_type::SCStreamOutputType;
598///
599/// let content = AsyncSCShareableContent::get().await?;
600/// let display = &content.displays()[0];
601/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
602/// let config = SCStreamConfiguration::new()
603///     .with_width(1920)
604///     .with_height(1080);
605///
606/// let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
607/// stream.start_capture().await?;
608///
609/// // Process frames asynchronously
610/// while let Some(frame) = stream.next().await {
611///     println!("Got frame!");
612/// }
613/// # Ok(())
614/// # }
615/// ```
616/// Async wrapper for `SCStream` with integrated frame iteration.
617///
618/// # Back-pressure and frame loss
619///
620/// `AsyncSCStream` buffers samples in a **bounded** internal queue sized
621/// by the `buffer_capacity` argument to [`AsyncSCStream::new`]. When the
622/// queue is full and a new sample arrives from `ScreenCaptureKit`, the
623/// **oldest** queued sample is dropped to make room — the stream is
624/// **lossy by design**.
625///
626/// This is the right policy for real-time UI rendering, screen-share
627/// previews, and live encoding: a slow consumer would rather see the
628/// most recent frame than a stale one. It is the *wrong* policy for
629/// lossless capture (e.g. saving every frame to disk for later
630/// editing) — for that, use the synchronous [`SCStream`](crate::stream::SCStream)
631/// directly, where back-pressure is naturally enforced by Apple's
632/// `queueDepth` setting and your handler's runtime.
633///
634/// To detect when frames are being dropped, watch `buffered_count()`
635/// against `buffer_capacity` over time, or instrument your handler
636/// with a per-frame timestamp delta and compare to your expected
637/// frame interval.
638pub struct AsyncSCStream {
639    stream: crate::stream::SCStream,
640    iterator_state: Arc<Mutex<AsyncSampleIteratorState>>,
641}
642
643impl AsyncSCStream {
644    /// Create a new async stream
645    ///
646    /// # Arguments
647    ///
648    /// * `filter` - Content filter specifying what to capture
649    /// * `config` - Stream configuration
650    /// * `buffer_capacity` - Max frames to buffer (oldest dropped when full)
651    /// * `output_type` - Type of output (Screen, Audio, Microphone)
652    #[must_use]
653    pub fn new(
654        filter: &SCContentFilter,
655        config: &SCStreamConfiguration,
656        buffer_capacity: usize,
657        output_type: crate::stream::output_type::SCStreamOutputType,
658    ) -> Self {
659        let state = Arc::new(Mutex::new(AsyncSampleIteratorState {
660            buffer: std::collections::VecDeque::with_capacity(buffer_capacity),
661            waker: None,
662            closed: false,
663            capacity: buffer_capacity,
664            stop_error: None,
665        }));
666
667        let sender = AsyncSampleSender {
668            inner: Arc::clone(&state),
669        };
670
671        let delegate = AsyncStreamDelegate {
672            state: Arc::clone(&state),
673        };
674
675        let mut stream = crate::stream::SCStream::new_with_delegate(filter, config, delegate);
676        if stream.add_output_handler(sender, output_type).is_none() {
677            // Registration failed: close the iterator immediately so `next()`
678            // resolves to `None` instead of pending forever, and record why.
679            if let Ok(mut s) = state.lock() {
680                s.closed = true;
681                s.stop_error = Some(SCError::StreamError(
682                    "failed to register stream output handler".to_string(),
683                ));
684            }
685        }
686
687        Self {
688            stream,
689            iterator_state: state,
690        }
691    }
692
693    /// Get the next sample buffer asynchronously
694    ///
695    /// Returns `None` when the stream is closed. For a multi-output stream
696    /// (see [`add_output_type`](Self::add_output_type)) use
697    /// [`next_typed`](Self::next_typed) to also learn each sample's
698    /// [`SCStreamOutputType`].
699    pub fn next(&self) -> NextSample<'_> {
700        NextSample {
701            state: &self.iterator_state,
702        }
703    }
704
705    /// Get the next sample buffer together with its output type.
706    ///
707    /// Use this when the stream carries more than one output type (e.g. screen
708    /// and audio) and you need to tell the samples apart. Returns `None` when
709    /// the stream is closed.
710    pub fn next_typed(&self) -> NextSampleTyped<'_> {
711        NextSampleTyped {
712            state: &self.iterator_state,
713        }
714    }
715
716    /// Borrow the captured frames as a [`Stream`](futures_core::Stream) of
717    /// `CMSampleBuffer`s.
718    ///
719    /// This unlocks the `futures::StreamExt` combinator ecosystem
720    /// (`map`, `filter`, `take`, `for_each`, `zip`, …) for processing frames:
721    ///
722    /// ```no_run
723    /// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
724    /// use futures_util::StreamExt;
725    ///
726    /// let frames: Vec<_> = stream.frames().take(30).collect().await;
727    /// # let _ = frames;
728    /// # }
729    /// ```
730    ///
731    /// The returned stream borrows `self`; for a multi-output stream use
732    /// [`frames_typed`](Self::frames_typed) to keep each sample's output type.
733    #[must_use]
734    pub fn frames(&self) -> SampleStream<'_> {
735        SampleStream {
736            state: &self.iterator_state,
737        }
738    }
739
740    /// Borrow the captured frames as a [`Stream`](futures_core::Stream) of
741    /// `(CMSampleBuffer, SCStreamOutputType)` pairs.
742    ///
743    /// Like [`frames`](Self::frames) but keeps each sample's output type, so a
744    /// stream carrying both audio and video (see
745    /// [`add_output_type`](Self::add_output_type)) can route samples with
746    /// `StreamExt` combinators:
747    ///
748    /// ```no_run
749    /// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
750    /// use futures_util::StreamExt;
751    /// use screencapturekit::stream::output_type::SCStreamOutputType;
752    ///
753    /// let audio: Vec<_> = stream
754    ///     .frames_typed()
755    ///     .filter(|(_, kind)| std::future::ready(*kind == SCStreamOutputType::Audio))
756    ///     .take(10)
757    ///     .collect()
758    ///     .await;
759    /// # let _ = audio;
760    /// # }
761    /// ```
762    #[must_use]
763    pub fn frames_typed(&self) -> TypedSampleStream<'_> {
764        TypedSampleStream {
765            state: &self.iterator_state,
766        }
767    }
768
769    /// Also deliver samples of an additional output type.
770    ///
771    /// By default an [`AsyncSCStream`] carries the single output type passed to
772    /// [`new`](Self::new). Call this to capture more than one type from one
773    /// stream — for example add [`SCStreamOutputType::Audio`] to a stream
774    /// created for [`SCStreamOutputType::Screen`] to capture audio and video
775    /// together. Samples from every registered type share the same lossy
776    /// buffer; use [`next_typed`](Self::next_typed) /
777    /// [`try_next_typed`](Self::try_next_typed) to distinguish them.
778    ///
779    /// Returns `true` if the output type was registered. Registration can fail
780    /// if the stream configuration does not enable that type (e.g. audio
781    /// capture was not configured).
782    pub fn add_output_type(&mut self, output_type: SCStreamOutputType) -> bool {
783        let sender = AsyncSampleSender {
784            inner: Arc::clone(&self.iterator_state),
785        };
786        self.stream
787            .add_output_handler(sender, output_type)
788            .is_some()
789    }
790
791    /// Try to get a sample without waiting
792    #[must_use]
793    pub fn try_next(&self) -> Option<crate::cm::CMSampleBuffer> {
794        self.iterator_state
795            .lock()
796            .ok()?
797            .buffer
798            .pop_front()
799            .map(|(buffer, _of_type)| buffer)
800    }
801
802    /// Try to get a sample together with its output type, without waiting.
803    #[must_use]
804    pub fn try_next_typed(&self) -> Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)> {
805        self.iterator_state.lock().ok()?.buffer.pop_front()
806    }
807
808    /// Check if the stream has been closed
809    ///
810    /// Returns `true` once the stream has stopped — either because this
811    /// `AsyncSCStream` was dropped or because `ScreenCaptureKit` stopped it
812    /// with an error (see [`take_error`](Self::take_error)).
813    #[must_use]
814    pub fn is_closed(&self) -> bool {
815        self.iterator_state.lock().map_or(true, |s| s.closed)
816    }
817
818    /// Take the error that stopped the stream, if any.
819    ///
820    /// When `ScreenCaptureKit` stops the stream with an error (e.g. the
821    /// captured display is disconnected or screen-recording permission is
822    /// revoked), the sample iterator is closed — [`next`](Self::next) resolves
823    /// to `None` after any buffered frames drain — and the [`SCError`] is stored
824    /// here. Call this once the iteration loop ends to distinguish an error stop
825    /// from a normal end of stream:
826    ///
827    /// ```no_run
828    /// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
829    /// while let Some(_frame) = stream.next().await {
830    ///     // process frames …
831    /// }
832    /// if let Some(err) = stream.take_error() {
833    ///     eprintln!("capture stopped with error: {err}");
834    /// }
835    /// # }
836    /// ```
837    ///
838    /// The stored error is cleared once taken.
839    #[must_use]
840    pub fn take_error(&self) -> Option<SCError> {
841        self.iterator_state.lock().ok()?.stop_error.take()
842    }
843
844    /// Get the number of buffered samples
845    #[must_use]
846    pub fn buffered_count(&self) -> usize {
847        self.iterator_state.lock().map_or(0, |s| s.buffer.len())
848    }
849
850    /// Clear all buffered samples
851    pub fn clear_buffer(&self) {
852        if let Ok(mut state) = self.iterator_state.lock() {
853            state.buffer.clear();
854        }
855    }
856
857    /// Start capture asynchronously.
858    ///
859    /// Resolves when `ScreenCaptureKit` confirms the stream has started.
860    /// Unlike [`SCStream::start_capture`](crate::stream::SCStream::start_capture),
861    /// awaiting this **does not block the executor thread** — the task is parked
862    /// via its [`Waker`] and resumed from the Swift completion callback.
863    ///
864    /// The capture is initiated eagerly when this method is called; `.await`
865    /// observes the completion (or error).
866    ///
867    /// # Errors
868    ///
869    /// The awaited result is `Err(SCError::CaptureStartFailed)` if the stream
870    /// fails to start.
871    pub fn start_capture(&self) -> StreamControlFuture {
872        let (future, context) = AsyncCompletion::<()>::create();
873        // SAFETY: `self.stream.as_ptr()` is a valid, live `SCStream` pointer for
874        // the duration of this call; `context` is the one-shot completion
875        // pointer from `AsyncCompletion::create()`, invoked exactly once.
876        unsafe {
877            crate::ffi::sc_stream_start_capture(
878                self.stream.as_ptr(),
879                context,
880                stream_control_callback,
881            );
882        }
883        StreamControlFuture {
884            inner: future,
885            map_err: SCError::CaptureStartFailed,
886        }
887    }
888
889    /// Stop capture asynchronously.
890    ///
891    /// Resolves when `ScreenCaptureKit` confirms the stream has stopped. Awaiting
892    /// this **does not block the executor thread**.
893    ///
894    /// # Errors
895    ///
896    /// The awaited result is `Err(SCError::CaptureStopFailed)` if the stream
897    /// fails to stop.
898    pub fn stop_capture(&self) -> StreamControlFuture {
899        let (future, context) = AsyncCompletion::<()>::create();
900        // SAFETY: see `start_capture` — live stream pointer, one-shot context.
901        unsafe {
902            crate::ffi::sc_stream_stop_capture(
903                self.stream.as_ptr(),
904                context,
905                stream_control_callback,
906            );
907        }
908        StreamControlFuture {
909            inner: future,
910            map_err: SCError::CaptureStopFailed,
911        }
912    }
913
914    /// Update stream configuration asynchronously.
915    ///
916    /// Resolves when the reconfiguration completes. Awaiting this **does not
917    /// block the executor thread**.
918    ///
919    /// # Errors
920    ///
921    /// The awaited result is `Err(SCError::StreamError)` if the update fails.
922    pub fn update_configuration(&self, config: &SCStreamConfiguration) -> StreamControlFuture {
923        let (future, context) = AsyncCompletion::<()>::create();
924        // SAFETY: `self.stream.as_ptr()` and `config.as_ptr()` are valid for the
925        // duration of this call; `context` is the one-shot completion pointer.
926        unsafe {
927            crate::ffi::sc_stream_update_configuration(
928                self.stream.as_ptr(),
929                config.as_ptr(),
930                context,
931                stream_control_callback,
932            );
933        }
934        StreamControlFuture {
935            inner: future,
936            map_err: SCError::StreamError,
937        }
938    }
939
940    /// Update content filter asynchronously.
941    ///
942    /// Resolves when the filter swap completes. Awaiting this **does not block
943    /// the executor thread**.
944    ///
945    /// # Errors
946    ///
947    /// The awaited result is `Err(SCError::StreamError)` if the update fails.
948    pub fn update_content_filter(&self, filter: &SCContentFilter) -> StreamControlFuture {
949        let (future, context) = AsyncCompletion::<()>::create();
950        // SAFETY: `self.stream.as_ptr()` and `filter.as_ptr()` are valid for the
951        // duration of this call; `context` is the one-shot completion pointer.
952        unsafe {
953            crate::ffi::sc_stream_update_content_filter(
954                self.stream.as_ptr(),
955                filter.as_ptr(),
956                context,
957                stream_control_callback,
958            );
959        }
960        StreamControlFuture {
961            inner: future,
962            map_err: SCError::StreamError,
963        }
964    }
965
966    /// Get a reference to the underlying stream
967    #[must_use]
968    pub fn inner(&self) -> &crate::stream::SCStream {
969        &self.stream
970    }
971}
972
973impl std::fmt::Debug for AsyncSCStream {
974    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
975        f.debug_struct("AsyncSCStream")
976            .field("stream", &self.stream)
977            .field("buffered_count", &self.buffered_count())
978            .field("is_closed", &self.is_closed())
979            .finish_non_exhaustive()
980    }
981}
982
983// ============================================================================
984// AsyncSCScreenshotManager - Async screenshot capture (macOS 14.0+)
985// ============================================================================
986
987/// Async wrapper for `SCScreenshotManager`
988///
989/// Provides async methods for single-frame screenshot capture.
990/// **Executor-agnostic** - works with any async runtime.
991///
992/// Requires the `macos_14_0` feature flag.
993///
994/// # Examples
995///
996/// ```rust,no_run
997/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
998/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCScreenshotManager};
999/// use screencapturekit::stream::configuration::SCStreamConfiguration;
1000/// use screencapturekit::stream::content_filter::SCContentFilter;
1001///
1002/// let content = AsyncSCShareableContent::get().await?;
1003/// let display = &content.displays()[0];
1004/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
1005/// let config = SCStreamConfiguration::new()
1006///     .with_width(1920)
1007///     .with_height(1080);
1008///
1009/// let image = AsyncSCScreenshotManager::capture_image(&filter, &config).await?;
1010/// println!("Screenshot: {}x{}", image.width(), image.height());
1011/// # Ok(())
1012/// # }
1013/// ```
1014#[cfg(feature = "macos_14_0")]
1015#[derive(Debug, Clone, Copy)]
1016pub struct AsyncSCScreenshotManager;
1017
1018/// Callback for async `CGImage` capture
1019#[cfg(feature = "macos_14_0")]
1020extern "C" fn screenshot_image_callback(
1021    image_ptr: *const c_void,
1022    error_ptr: *const i8,
1023    user_data: *mut c_void,
1024) {
1025    crate::utils::panic_safe::catch_user_panic("screenshot_image_callback", move || {
1026        if !error_ptr.is_null() {
1027            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
1028            let error = unsafe { error_from_cstr(error_ptr) };
1029            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1030            unsafe {
1031                AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(
1032                    user_data, error,
1033                );
1034            }
1035        } else if !image_ptr.is_null() {
1036            // SAFETY: the Swift bridge hands back a retained `CGImageRef` on success.
1037            let image = unsafe { crate::screenshot_manager::cgimage_from_retained_ptr(image_ptr) };
1038            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1039            unsafe { AsyncCompletion::complete_ok(user_data, image) };
1040        } else {
1041            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1042            unsafe {
1043                AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(
1044                    user_data,
1045                    "Unknown error".to_string(),
1046                );
1047            };
1048        }
1049    });
1050}
1051
1052/// Callback for async `CMSampleBuffer` capture
1053#[cfg(feature = "macos_14_0")]
1054extern "C" fn screenshot_buffer_callback(
1055    buffer_ptr: *const c_void,
1056    error_ptr: *const i8,
1057    user_data: *mut c_void,
1058) {
1059    crate::utils::panic_safe::catch_user_panic("screenshot_buffer_callback", move || {
1060        if !error_ptr.is_null() {
1061            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
1062            let error = unsafe { error_from_cstr(error_ptr) };
1063            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1064            unsafe { AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
1065        } else if !buffer_ptr.is_null() {
1066            // SAFETY: `buffer_ptr` is non-null (checked above), is a valid `CMSampleBuffer` pointer, and `cast_mut()` is sound because the underlying object is uniquely owned at this point.
1067            let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
1068            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1069            unsafe { AsyncCompletion::complete_ok(user_data, buffer) };
1070        } else {
1071            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1072            unsafe {
1073                AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
1074                    user_data,
1075                    "Unknown error".to_string(),
1076                );
1077            };
1078        }
1079    });
1080}
1081
1082/// Future for async screenshot capture
1083#[cfg(feature = "macos_14_0")]
1084pub struct AsyncScreenshotFuture<T> {
1085    inner: AsyncCompletionFuture<T>,
1086}
1087
1088#[cfg(feature = "macos_14_0")]
1089impl<T> std::fmt::Debug for AsyncScreenshotFuture<T> {
1090    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1091        f.debug_struct("AsyncScreenshotFuture")
1092            .finish_non_exhaustive()
1093    }
1094}
1095
1096#[cfg(feature = "macos_14_0")]
1097impl<T> Future for AsyncScreenshotFuture<T> {
1098    type Output = Result<T, SCError>;
1099
1100    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1101        Pin::new(&mut self.inner)
1102            .poll(cx)
1103            .map(|r| r.map_err(SCError::ScreenshotError))
1104    }
1105}
1106
1107#[cfg(feature = "macos_14_0")]
1108impl AsyncSCScreenshotManager {
1109    /// Capture a single screenshot as a `CGImage` asynchronously
1110    ///
1111    /// # Errors
1112    /// Returns an error if:
1113    /// - Screen recording permission is not granted
1114    /// - The capture fails for any reason
1115    pub fn capture_image(
1116        content_filter: &crate::stream::content_filter::SCContentFilter,
1117        configuration: &SCStreamConfiguration,
1118    ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
1119        let (future, context) = AsyncCompletion::create();
1120
1121        // SAFETY: `content_filter.as_ptr()` and `configuration.as_ptr()` return valid non-null pointers for the duration of this call (borrowed via `&`). `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1122        unsafe {
1123            crate::ffi::sc_screenshot_manager_capture_image(
1124                content_filter.as_ptr(),
1125                configuration.as_ptr(),
1126                screenshot_image_callback,
1127                context,
1128            );
1129        }
1130
1131        AsyncScreenshotFuture { inner: future }
1132    }
1133
1134    /// Capture a single screenshot as a `CMSampleBuffer` asynchronously
1135    ///
1136    /// # Errors
1137    /// Returns an error if:
1138    /// - Screen recording permission is not granted
1139    /// - The capture fails for any reason
1140    pub fn capture_sample_buffer(
1141        content_filter: &crate::stream::content_filter::SCContentFilter,
1142        configuration: &SCStreamConfiguration,
1143    ) -> AsyncScreenshotFuture<crate::cm::CMSampleBuffer> {
1144        let (future, context) = AsyncCompletion::create();
1145
1146        // SAFETY: `content_filter.as_ptr()` and `configuration.as_ptr()` return valid non-null pointers for the duration of this call (borrowed via `&`). `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1147        unsafe {
1148            crate::ffi::sc_screenshot_manager_capture_sample_buffer(
1149                content_filter.as_ptr(),
1150                configuration.as_ptr(),
1151                screenshot_buffer_callback,
1152                context,
1153            );
1154        }
1155
1156        AsyncScreenshotFuture { inner: future }
1157    }
1158
1159    /// Capture a screenshot of a specific screen region asynchronously (macOS 15.2+)
1160    ///
1161    /// This method captures the content within the specified rectangle,
1162    /// which can span multiple displays.
1163    ///
1164    /// # Arguments
1165    /// * `rect` - The rectangle to capture, in screen coordinates (points)
1166    ///
1167    /// # Errors
1168    /// Returns an error if:
1169    /// - The system is not macOS 15.2+
1170    /// - Screen recording permission is not granted
1171    /// - The capture fails for any reason
1172    #[cfg(feature = "macos_15_2")]
1173    pub fn capture_image_in_rect(
1174        rect: crate::cg::CGRect,
1175    ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
1176        let (future, context) = AsyncCompletion::create();
1177
1178        // SAFETY: The rectangle coordinates are plain values passed by copy. `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1179        unsafe {
1180            crate::ffi::sc_screenshot_manager_capture_image_in_rect(
1181                rect.origin.x,
1182                rect.origin.y,
1183                rect.size.width,
1184                rect.size.height,
1185                screenshot_image_callback,
1186                context,
1187            );
1188        }
1189
1190        AsyncScreenshotFuture { inner: future }
1191    }
1192
1193    /// Capture a screenshot with advanced configuration asynchronously (macOS 26.0+)
1194    ///
1195    /// This method uses the new `SCScreenshotConfiguration` for more control
1196    /// over the screenshot output, including HDR support and file saving.
1197    ///
1198    /// # Arguments
1199    /// * `content_filter` - The content filter specifying what to capture
1200    /// * `configuration` - The screenshot configuration
1201    ///
1202    /// # Errors
1203    /// Returns an error if the capture fails
1204    #[cfg(feature = "macos_26_0")]
1205    pub fn capture_screenshot(
1206        content_filter: &crate::stream::content_filter::SCContentFilter,
1207        configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
1208    ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
1209        let (future, context) = AsyncCompletion::create();
1210
1211        // SAFETY: `content_filter.as_ptr()` and `configuration.as_ptr()` return valid non-null pointers for the duration of this call (borrowed via `&`). `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1212        unsafe {
1213            crate::ffi::sc_screenshot_manager_capture_screenshot(
1214                content_filter.as_ptr(),
1215                configuration.as_ptr(),
1216                screenshot_output_callback,
1217                context,
1218            );
1219        }
1220
1221        AsyncScreenshotFuture { inner: future }
1222    }
1223
1224    /// Capture a screenshot of a specific region with advanced configuration asynchronously (macOS 26.0+)
1225    ///
1226    /// # Arguments
1227    /// * `rect` - The rectangle to capture, in screen coordinates (points)
1228    /// * `configuration` - The screenshot configuration
1229    ///
1230    /// # Errors
1231    /// Returns an error if the capture fails
1232    #[cfg(feature = "macos_26_0")]
1233    pub fn capture_screenshot_in_rect(
1234        rect: crate::cg::CGRect,
1235        configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
1236    ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
1237        let (future, context) = AsyncCompletion::create();
1238
1239        // SAFETY: `configuration.as_ptr()` returns a valid non-null pointer for the duration of this call (borrowed via `&`). The rectangle coordinates are plain values passed by copy. `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1240        unsafe {
1241            crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
1242                rect.origin.x,
1243                rect.origin.y,
1244                rect.size.width,
1245                rect.size.height,
1246                configuration.as_ptr(),
1247                screenshot_output_callback,
1248                context,
1249            );
1250        }
1251
1252        AsyncScreenshotFuture { inner: future }
1253    }
1254}
1255
1256/// Callback for async `SCScreenshotOutput` capture (macOS 26.0+)
1257#[cfg(feature = "macos_26_0")]
1258extern "C" fn screenshot_output_callback(
1259    output_ptr: *const c_void,
1260    error_ptr: *const i8,
1261    user_data: *mut c_void,
1262) {
1263    crate::utils::panic_safe::catch_user_panic("screenshot_output_callback", move || {
1264        if !error_ptr.is_null() {
1265            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
1266            let error = unsafe { error_from_cstr(error_ptr) };
1267            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1268            unsafe {
1269                AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
1270                    user_data, error,
1271                );
1272            }
1273        } else if !output_ptr.is_null() {
1274            let output = crate::screenshot_manager::SCScreenshotOutput::from_ptr(output_ptr);
1275            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`.
1276            unsafe { AsyncCompletion::complete_ok(user_data, output) };
1277        } else {
1278            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1279            unsafe {
1280                AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
1281                    user_data,
1282                    "Unknown error".to_string(),
1283                );
1284            };
1285        }
1286    });
1287}
1288
1289// ============================================================================
1290// AsyncSCContentSharingPicker - Async content sharing picker (macOS 14.0+)
1291// ============================================================================
1292
1293/// Result from the async picker callback
1294#[cfg(feature = "macos_14_0")]
1295struct AsyncPickerCallbackResult {
1296    code: i32,
1297    ptr: *const c_void,
1298}
1299
1300#[cfg(feature = "macos_14_0")]
1301// SAFETY: `AsyncPickerCallbackResult` stores a `*const c_void` that is an
1302// Apple Objective-C object reference (`SCPickerResult`). All ScreenCaptureKit
1303// objects are thread-safe to pass across threads (they follow ObjC ARC rules),
1304// so sending this pointer to another thread is sound.
1305unsafe impl Send for AsyncPickerCallbackResult {}
1306
1307/// Callback for async picker
1308#[cfg(feature = "macos_14_0")]
1309extern "C" fn async_picker_callback(result_code: i32, ptr: *const c_void, user_data: *mut c_void) {
1310    crate::utils::panic_safe::catch_user_panic("async_picker_callback", move || {
1311        let result = AsyncPickerCallbackResult {
1312            code: result_code,
1313            ptr,
1314        };
1315        // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`.
1316        unsafe { AsyncCompletion::complete_ok(user_data, result) };
1317    });
1318}
1319
1320/// Future for async picker with full result
1321#[cfg(feature = "macos_14_0")]
1322pub struct AsyncPickerFuture {
1323    inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
1324}
1325
1326#[cfg(feature = "macos_14_0")]
1327impl std::fmt::Debug for AsyncPickerFuture {
1328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1329        f.debug_struct("AsyncPickerFuture").finish_non_exhaustive()
1330    }
1331}
1332
1333#[cfg(feature = "macos_14_0")]
1334impl Future for AsyncPickerFuture {
1335    type Output = crate::content_sharing_picker::SCPickerOutcome;
1336
1337    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1338        use crate::content_sharing_picker::{SCPickerOutcome, SCPickerResult};
1339
1340        match Pin::new(&mut self.inner).poll(cx) {
1341            Poll::Pending => Poll::Pending,
1342            Poll::Ready(Ok(result)) => {
1343                let outcome = match result.code {
1344                    1 if !result.ptr.is_null() => {
1345                        SCPickerOutcome::Picked(SCPickerResult::from_ptr(result.ptr))
1346                    }
1347                    0 => SCPickerOutcome::Cancelled,
1348                    _ => SCPickerOutcome::Error("Picker failed".to_string()),
1349                };
1350                Poll::Ready(outcome)
1351            }
1352            Poll::Ready(Err(e)) => Poll::Ready(SCPickerOutcome::Error(e)),
1353        }
1354    }
1355}
1356
1357/// Future for async picker returning filter only
1358#[cfg(feature = "macos_14_0")]
1359pub struct AsyncPickerFilterFuture {
1360    inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
1361}
1362
1363#[cfg(feature = "macos_14_0")]
1364impl std::fmt::Debug for AsyncPickerFilterFuture {
1365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1366        f.debug_struct("AsyncPickerFilterFuture")
1367            .finish_non_exhaustive()
1368    }
1369}
1370
1371#[cfg(feature = "macos_14_0")]
1372impl Future for AsyncPickerFilterFuture {
1373    type Output = crate::content_sharing_picker::SCPickerFilterOutcome;
1374
1375    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1376        use crate::content_sharing_picker::SCPickerFilterOutcome;
1377
1378        match Pin::new(&mut self.inner).poll(cx) {
1379            Poll::Pending => Poll::Pending,
1380            Poll::Ready(Ok(result)) => {
1381                let outcome = match result.code {
1382                    1 if !result.ptr.is_null() => {
1383                        SCPickerFilterOutcome::Filter(SCContentFilter::from_picker_ptr(result.ptr))
1384                    }
1385                    0 => SCPickerFilterOutcome::Cancelled,
1386                    _ => SCPickerFilterOutcome::Error("Picker failed".to_string()),
1387                };
1388                Poll::Ready(outcome)
1389            }
1390            Poll::Ready(Err(e)) => Poll::Ready(SCPickerFilterOutcome::Error(e)),
1391        }
1392    }
1393}
1394
1395/// Async wrapper for `SCContentSharingPicker` (macOS 14.0+)
1396///
1397/// Provides async methods to show the system content sharing picker UI.
1398/// **Executor-agnostic** - works with any async runtime.
1399///
1400/// # Examples
1401///
1402/// ```no_run
1403/// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1404/// use screencapturekit::content_sharing_picker::*;
1405///
1406/// async fn pick_content() {
1407///     let config = SCContentSharingPickerConfiguration::new();
1408///     match AsyncSCContentSharingPicker::show(&config).await {
1409///         SCPickerOutcome::Picked(result) => {
1410///             let (width, height) = result.pixel_size();
1411///             let filter = result.filter();
1412///             println!("Selected content: {}x{}", width, height);
1413///         }
1414///         SCPickerOutcome::Cancelled => println!("User cancelled"),
1415///         SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
1416///     }
1417/// }
1418/// ```
1419#[cfg(feature = "macos_14_0")]
1420#[derive(Debug, Clone, Copy)]
1421pub struct AsyncSCContentSharingPicker;
1422
1423#[cfg(feature = "macos_14_0")]
1424impl AsyncSCContentSharingPicker {
1425    /// Show the picker UI asynchronously and return `SCPickerResult` with filter and metadata
1426    ///
1427    /// This is the main API - use when you need content dimensions or want to build custom filters.
1428    /// The picker UI will be shown on the main thread, and the future will resolve when the user
1429    /// makes a selection or cancels.
1430    ///
1431    /// # Example
1432    /// ```no_run
1433    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1434    /// use screencapturekit::content_sharing_picker::*;
1435    ///
1436    /// async fn example() {
1437    ///     let config = SCContentSharingPickerConfiguration::new();
1438    ///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
1439    ///         let (width, height) = result.pixel_size();
1440    ///         let filter = result.filter();
1441    ///     }
1442    /// }
1443    /// ```
1444    pub fn show(
1445        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1446    ) -> AsyncPickerFuture {
1447        let (future, context) = AsyncCompletion::create();
1448
1449        // SAFETY: `config.as_ptr()` returns a valid non-null pointer for the duration of this call. `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1450        unsafe {
1451            crate::ffi::sc_content_sharing_picker_show_with_result(
1452                config.as_ptr(),
1453                async_picker_callback,
1454                context,
1455            );
1456        }
1457
1458        AsyncPickerFuture { inner: future }
1459    }
1460
1461    /// Show the picker UI asynchronously and return an `SCContentFilter` directly
1462    ///
1463    /// This is the simple API - use when you just need the filter without metadata.
1464    ///
1465    /// # Example
1466    /// ```no_run
1467    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1468    /// use screencapturekit::content_sharing_picker::*;
1469    ///
1470    /// async fn example() {
1471    ///     let config = SCContentSharingPickerConfiguration::new();
1472    ///     if let SCPickerFilterOutcome::Filter(filter) = AsyncSCContentSharingPicker::show_filter(&config).await {
1473    ///         // Use filter with SCStream
1474    ///     }
1475    /// }
1476    /// ```
1477    pub fn show_filter(
1478        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1479    ) -> AsyncPickerFilterFuture {
1480        let (future, context) = AsyncCompletion::create();
1481
1482        // SAFETY: `config.as_ptr()` returns a valid non-null pointer for the duration of this call. `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1483        unsafe {
1484            crate::ffi::sc_content_sharing_picker_show(
1485                config.as_ptr(),
1486                async_picker_callback,
1487                context,
1488            );
1489        }
1490
1491        AsyncPickerFilterFuture { inner: future }
1492    }
1493
1494    /// Show the picker UI for an existing stream to change source while capturing
1495    ///
1496    /// Use this when you have an active `SCStream` and want to let the user
1497    /// select a new content source. The result can be used with `stream.update_content_filter()`.
1498    ///
1499    /// # Example
1500    /// ```no_run
1501    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1502    /// use screencapturekit::content_sharing_picker::*;
1503    /// use screencapturekit::stream::SCStream;
1504    /// use screencapturekit::stream::configuration::SCStreamConfiguration;
1505    /// use screencapturekit::stream::content_filter::SCContentFilter;
1506    /// use screencapturekit::shareable_content::SCShareableContent;
1507    ///
1508    /// async fn example() -> Option<()> {
1509    ///     let content = SCShareableContent::get().ok()?;
1510    ///     let displays = content.displays();
1511    ///     let display = displays.first()?;
1512    ///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
1513    ///     let stream_config = SCStreamConfiguration::new();
1514    ///     let stream = SCStream::new(&filter, &stream_config);
1515    ///
1516    ///     // When stream is active and user wants to change source
1517    ///     let config = SCContentSharingPickerConfiguration::new();
1518    ///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show_for_stream(&config, &stream).await {
1519    ///         // Use result.filter() with stream.update_content_filter()
1520    ///         let _ = result.filter();
1521    ///     }
1522    ///     Some(())
1523    /// }
1524    /// ```
1525    pub fn show_for_stream(
1526        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1527        stream: &crate::stream::SCStream,
1528    ) -> AsyncPickerFuture {
1529        let (future, context) = AsyncCompletion::create();
1530
1531        // SAFETY: `config.as_ptr()` and `stream.as_ptr()` return valid non-null pointers for the duration of this call. `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1532        unsafe {
1533            crate::ffi::sc_content_sharing_picker_show_for_stream(
1534                config.as_ptr(),
1535                stream.as_ptr(),
1536                async_picker_callback,
1537                context,
1538            );
1539        }
1540
1541        AsyncPickerFuture { inner: future }
1542    }
1543}
1544
1545// ============================================================================
1546// AsyncSCRecordingOutput - Async recording with event stream (macOS 15.0+)
1547// ============================================================================
1548
1549/// Recording lifecycle event
1550#[cfg(feature = "macos_15_0")]
1551#[derive(Debug, Clone, PartialEq, Eq)]
1552pub enum RecordingEvent {
1553    /// Recording started successfully
1554    Started,
1555    /// Recording finished successfully
1556    Finished,
1557    /// Recording failed with an error
1558    Failed(String),
1559}
1560
1561#[cfg(feature = "macos_15_0")]
1562struct AsyncRecordingState {
1563    events: std::collections::VecDeque<RecordingEvent>,
1564    waker: Option<Waker>,
1565    finished: bool,
1566}
1567
1568#[cfg(feature = "macos_15_0")]
1569struct AsyncRecordingDelegate {
1570    state: Arc<Mutex<AsyncRecordingState>>,
1571}
1572
1573#[cfg(feature = "macos_15_0")]
1574impl crate::recording_output::SCRecordingOutputDelegate for AsyncRecordingDelegate {
1575    fn recording_did_start(&self) {
1576        if let Ok(mut state) = self.state.lock() {
1577            state.events.push_back(RecordingEvent::Started);
1578            if let Some(waker) = state.waker.take() {
1579                waker.wake();
1580            }
1581        }
1582    }
1583
1584    fn recording_did_fail(&self, error: String) {
1585        if let Ok(mut state) = self.state.lock() {
1586            state.events.push_back(RecordingEvent::Failed(error));
1587            state.finished = true;
1588            if let Some(waker) = state.waker.take() {
1589                waker.wake();
1590            }
1591        }
1592    }
1593
1594    fn recording_did_finish(&self) {
1595        if let Ok(mut state) = self.state.lock() {
1596            state.events.push_back(RecordingEvent::Finished);
1597            state.finished = true;
1598            if let Some(waker) = state.waker.take() {
1599                waker.wake();
1600            }
1601        }
1602    }
1603}
1604
1605/// Future for getting the next recording event
1606#[cfg(feature = "macos_15_0")]
1607pub struct NextRecordingEvent<'a> {
1608    state: &'a Arc<Mutex<AsyncRecordingState>>,
1609}
1610
1611#[cfg(feature = "macos_15_0")]
1612impl std::fmt::Debug for NextRecordingEvent<'_> {
1613    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1614        f.debug_struct("NextRecordingEvent").finish_non_exhaustive()
1615    }
1616}
1617
1618#[cfg(feature = "macos_15_0")]
1619impl Future for NextRecordingEvent<'_> {
1620    type Output = Option<RecordingEvent>;
1621
1622    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1623        poll_next_recording_event(self.state, cx)
1624    }
1625}
1626
1627/// Shared poll logic for the recording-event future/stream.
1628#[cfg(feature = "macos_15_0")]
1629fn poll_next_recording_event(
1630    state: &Arc<Mutex<AsyncRecordingState>>,
1631    cx: &Context<'_>,
1632) -> Poll<Option<RecordingEvent>> {
1633    let Ok(mut state) = state.lock() else {
1634        return Poll::Ready(None);
1635    };
1636
1637    if let Some(event) = state.events.pop_front() {
1638        return Poll::Ready(Some(event));
1639    }
1640
1641    if state.finished {
1642        Poll::Ready(None)
1643    } else {
1644        // Avoid the lost-wakeup race — see `poll_next_sample` above.
1645        let waker = cx.waker();
1646        match state.waker {
1647            Some(ref existing) if existing.will_wake(waker) => {}
1648            _ => state.waker = Some(waker.clone()),
1649        }
1650        Poll::Pending
1651    }
1652}
1653
1654/// A [`Stream`](futures_core::Stream) of recording lifecycle [`RecordingEvent`]s.
1655///
1656/// Yields `Started` / `Finished` / `Failed(_)` and ends (`None`) once the
1657/// recording finishes or fails. Returned by [`AsyncSCRecordingOutput::events`];
1658/// integrates with the `futures::StreamExt` combinators.
1659#[cfg(feature = "macos_15_0")]
1660pub struct RecordingEventStream<'a> {
1661    state: &'a Arc<Mutex<AsyncRecordingState>>,
1662}
1663
1664#[cfg(feature = "macos_15_0")]
1665impl std::fmt::Debug for RecordingEventStream<'_> {
1666    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1667        f.debug_struct("RecordingEventStream")
1668            .finish_non_exhaustive()
1669    }
1670}
1671
1672#[cfg(feature = "macos_15_0")]
1673impl futures_core::Stream for RecordingEventStream<'_> {
1674    type Item = RecordingEvent;
1675
1676    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1677        poll_next_recording_event(self.state, cx)
1678    }
1679}
1680
1681/// Async wrapper for `SCRecordingOutput` with event stream (macOS 15.0+)
1682///
1683/// Provides async iteration over recording lifecycle events.
1684/// **Executor-agnostic** - works with any async runtime.
1685///
1686/// # Examples
1687///
1688/// ```no_run
1689/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCRecordingOutput, RecordingEvent};
1690/// use screencapturekit::recording_output::SCRecordingOutputConfiguration;
1691/// use screencapturekit::stream::{SCStream, configuration::SCStreamConfiguration, content_filter::SCContentFilter};
1692/// use std::path::Path;
1693///
1694/// async fn record_screen() -> Option<()> {
1695///     let content = AsyncSCShareableContent::get().await.ok()?;
1696///     let displays = content.displays();
1697///     let display = displays.first()?;
1698///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
1699///     let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
1700///
1701///     let rec_config = SCRecordingOutputConfiguration::new()
1702///         .with_output_url(Path::new("/tmp/recording.mp4"));
1703///
1704///     let (recording, events) = AsyncSCRecordingOutput::new(&rec_config)?;
1705///
1706///     let mut stream = SCStream::new(&filter, &config);
1707///     stream.add_recording_output(&recording).ok()?;
1708///     stream.start_capture().ok()?;
1709///
1710///     // Wait for recording events
1711///     while let Some(event) = events.next().await {
1712///         match event {
1713///             RecordingEvent::Started => println!("Recording started!"),
1714///             RecordingEvent::Finished => {
1715///                 println!("Recording finished!");
1716///                 break;
1717///             }
1718///             RecordingEvent::Failed(e) => {
1719///                 eprintln!("Recording failed: {}", e);
1720///                 break;
1721///             }
1722///         }
1723///     }
1724///
1725///     Some(())
1726/// }
1727/// ```
1728#[cfg(feature = "macos_15_0")]
1729pub struct AsyncSCRecordingOutput {
1730    state: Arc<Mutex<AsyncRecordingState>>,
1731}
1732
1733#[cfg(feature = "macos_15_0")]
1734impl std::fmt::Debug for AsyncSCRecordingOutput {
1735    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1736        f.debug_struct("AsyncSCRecordingOutput")
1737            .finish_non_exhaustive()
1738    }
1739}
1740
1741#[cfg(feature = "macos_15_0")]
1742impl AsyncSCRecordingOutput {
1743    /// Create a new async recording output
1744    ///
1745    /// Returns a tuple of (`SCRecordingOutput`, `AsyncSCRecordingOutput`).
1746    /// The `SCRecordingOutput` should be added to an `SCStream`, while
1747    /// the `AsyncSCRecordingOutput` provides async event iteration.
1748    ///
1749    /// # Errors
1750    ///
1751    /// Returns `None` if the recording output cannot be created (requires macOS 15.0+).
1752    #[must_use]
1753    pub fn new(
1754        config: &crate::recording_output::SCRecordingOutputConfiguration,
1755    ) -> Option<(crate::recording_output::SCRecordingOutput, Self)> {
1756        let state = Arc::new(Mutex::new(AsyncRecordingState {
1757            events: std::collections::VecDeque::new(),
1758            waker: None,
1759            finished: false,
1760        }));
1761
1762        let delegate = AsyncRecordingDelegate {
1763            state: Arc::clone(&state),
1764        };
1765
1766        let recording =
1767            crate::recording_output::SCRecordingOutput::new_with_delegate(config, delegate)?;
1768
1769        Some((recording, Self { state }))
1770    }
1771
1772    /// Get the next recording event asynchronously
1773    ///
1774    /// Returns `None` when the recording has finished or failed.
1775    pub fn next(&self) -> NextRecordingEvent<'_> {
1776        NextRecordingEvent { state: &self.state }
1777    }
1778
1779    /// Borrow the recording events as a [`Stream`](futures_core::Stream) of
1780    /// [`RecordingEvent`]s.
1781    ///
1782    /// Unlocks the `futures::StreamExt` combinators for the recording event
1783    /// flow (`for_each`, `take_while`, …):
1784    ///
1785    /// ```no_run
1786    /// # #[cfg(feature = "macos_15_0")]
1787    /// # async fn example(recording: screencapturekit::async_api::AsyncSCRecordingOutput) {
1788    /// use futures_util::StreamExt;
1789    ///
1790    /// recording
1791    ///     .events()
1792    ///     .for_each(|event| {
1793    ///         println!("recording event: {event:?}");
1794    ///         std::future::ready(())
1795    ///     })
1796    ///     .await;
1797    /// # }
1798    /// ```
1799    #[must_use]
1800    pub fn events(&self) -> RecordingEventStream<'_> {
1801        RecordingEventStream { state: &self.state }
1802    }
1803
1804    /// Check if the recording has finished
1805    #[must_use]
1806    pub fn is_finished(&self) -> bool {
1807        self.state.lock().map_or(true, |s| s.finished)
1808    }
1809
1810    /// Get any pending events without waiting
1811    #[must_use]
1812    pub fn try_next(&self) -> Option<RecordingEvent> {
1813        self.state.lock().ok()?.events.pop_front()
1814    }
1815}