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::{
75    error_from_cstr, is_timeout_error, AsyncCompletion, AsyncCompletionFuture,
76};
77use std::ffi::c_void;
78use std::future::Future;
79use std::pin::Pin;
80use std::sync::{Arc, Mutex};
81use std::task::{Context, Poll, Waker};
82
83struct RegisteredWaker {
84    token: Arc<()>,
85    waker: Waker,
86}
87
88fn register_waker(
89    waiters: &mut Vec<RegisteredWaker>,
90    token: &Arc<()>,
91    waker: Waker,
92) -> Option<Waker> {
93    if let Some(waiter) = waiters
94        .iter_mut()
95        .find(|waiter| Arc::ptr_eq(&waiter.token, token))
96    {
97        return Some(if waiter.waker.will_wake(&waker) {
98            waker
99        } else {
100            std::mem::replace(&mut waiter.waker, waker)
101        });
102    }
103
104    waiters.push(RegisteredWaker {
105        token: Arc::clone(token),
106        waker,
107    });
108    None
109}
110
111fn unregister_waker(
112    waiters: &mut Vec<RegisteredWaker>,
113    token: &Arc<()>,
114) -> Option<RegisteredWaker> {
115    waiters
116        .iter()
117        .position(|waiter| Arc::ptr_eq(&waiter.token, token))
118        .map(|index| waiters.swap_remove(index))
119}
120
121fn wake_all(waiters: Vec<RegisteredWaker>) {
122    for waiter in waiters {
123        waiter.waker.wake();
124    }
125}
126
127// ============================================================================
128// AsyncSCShareableContent - True async with callback-based FFI
129// ============================================================================
130
131/// Callback from Swift FFI for shareable content
132extern "C" fn shareable_content_callback(
133    content: *const c_void,
134    error: *const i8,
135    user_data: *mut c_void,
136) {
137    crate::utils::panic_safe::catch_user_panic("shareable_content_callback", move || {
138        if !error.is_null() {
139            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
140            let error_msg = unsafe { error_from_cstr(error) };
141            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
142            unsafe { AsyncCompletion::<SCShareableContent>::complete_err(user_data, error_msg) };
143        } else if !content.is_null() {
144            // SAFETY: `content` is non-null (checked above) and is a valid `SCShareableContent` pointer retained for us by the Swift completion handler.
145            let sc = unsafe { SCShareableContent::from_ptr(content) };
146            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
147            unsafe { AsyncCompletion::complete_ok(user_data, sc) };
148        } else {
149            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
150            unsafe {
151                AsyncCompletion::<SCShareableContent>::complete_err(
152                    user_data,
153                    "Unknown error".to_string(),
154                );
155            };
156        }
157    });
158}
159
160/// Future for async shareable content retrieval
161pub struct AsyncShareableContentFuture {
162    inner: AsyncCompletionFuture<SCShareableContent>,
163}
164
165impl std::fmt::Debug for AsyncShareableContentFuture {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        f.debug_struct("AsyncShareableContentFuture")
168            .finish_non_exhaustive()
169    }
170}
171
172impl Future for AsyncShareableContentFuture {
173    type Output = Result<SCShareableContent, SCError>;
174
175    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
176        Pin::new(&mut self.inner)
177            .poll(cx)
178            .map(|r| r.map_err(SCError::NoShareableContent))
179    }
180}
181
182/// Async wrapper for `SCShareableContent`
183///
184/// Provides async methods to retrieve displays, windows, and applications
185/// without blocking. **Executor-agnostic** - works with any async runtime.
186#[derive(Debug, Clone, Copy)]
187pub struct AsyncSCShareableContent;
188
189impl AsyncSCShareableContent {
190    /// Asynchronously get the shareable content (displays, windows, applications)
191    ///
192    /// Uses callback-based Swift FFI for true async operation.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if:
197    /// - Screen recording permission is not granted
198    /// - The system fails to retrieve shareable content
199    pub fn get() -> AsyncShareableContentFuture {
200        Self::create().get()
201    }
202
203    /// Create options builder for customizing shareable content retrieval
204    #[must_use]
205    pub fn create() -> AsyncSCShareableContentOptions {
206        AsyncSCShareableContentOptions::default()
207    }
208}
209
210/// Options for async shareable content retrieval
211#[derive(Default, Debug, Clone, PartialEq, Eq)]
212pub struct AsyncSCShareableContentOptions {
213    exclude_desktop_windows: bool,
214    on_screen_windows_only: bool,
215}
216
217impl AsyncSCShareableContentOptions {
218    /// Exclude desktop windows from the shareable content
219    #[must_use]
220    pub fn with_exclude_desktop_windows(mut self, exclude: bool) -> Self {
221        self.exclude_desktop_windows = exclude;
222        self
223    }
224
225    /// Include only on-screen windows in the shareable content
226    #[must_use]
227    pub fn with_on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
228        self.on_screen_windows_only = on_screen_only;
229        self
230    }
231
232    /// Asynchronously get the shareable content with these options
233    pub fn get(self) -> AsyncShareableContentFuture {
234        let (future, context) = AsyncCompletion::create();
235
236        // 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.
237        unsafe {
238            crate::ffi::sc_shareable_content_get_with_options(
239                self.exclude_desktop_windows,
240                self.on_screen_windows_only,
241                shareable_content_callback,
242                context,
243            );
244        }
245
246        AsyncShareableContentFuture { inner: future }
247    }
248
249    /// Asynchronously get shareable content with only windows below a reference window
250    ///
251    /// This returns windows that are stacked below the specified reference window
252    /// in the window layering order.
253    ///
254    /// # Arguments
255    ///
256    /// * `reference_window` - The window to use as the reference point
257    pub fn below_window(
258        self,
259        reference_window: &crate::shareable_content::SCWindow,
260    ) -> AsyncShareableContentFuture {
261        let (future, context) = AsyncCompletion::create();
262
263        // 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.
264        unsafe {
265            crate::ffi::sc_shareable_content_get_below_window(
266                self.exclude_desktop_windows,
267                reference_window.as_ptr(),
268                shareable_content_callback,
269                context,
270            );
271        }
272
273        AsyncShareableContentFuture { inner: future }
274    }
275
276    /// Asynchronously get shareable content with only windows above a reference window
277    ///
278    /// This returns windows that are stacked above the specified reference window
279    /// in the window layering order.
280    ///
281    /// # Arguments
282    ///
283    /// * `reference_window` - The window to use as the reference point
284    pub fn above_window(
285        self,
286        reference_window: &crate::shareable_content::SCWindow,
287    ) -> AsyncShareableContentFuture {
288        let (future, context) = AsyncCompletion::create();
289
290        // 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.
291        unsafe {
292            crate::ffi::sc_shareable_content_get_above_window(
293                self.exclude_desktop_windows,
294                reference_window.as_ptr(),
295                shareable_content_callback,
296                context,
297            );
298        }
299
300        AsyncShareableContentFuture { inner: future }
301    }
302}
303
304impl AsyncSCShareableContent {
305    /// Asynchronously get shareable content for the current process only (macOS 14.4+)
306    ///
307    /// This retrieves content that the current process can capture without
308    /// requiring user authorization via TCC (Transparency, Consent, and Control).
309    ///
310    /// # Examples
311    ///
312    /// ```rust,no_run
313    /// # #[tokio::main]
314    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
315    /// use screencapturekit::async_api::AsyncSCShareableContent;
316    ///
317    /// // Get content capturable without TCC authorization
318    /// let content = AsyncSCShareableContent::current_process().await?;
319    /// println!("Found {} windows for current process", content.windows().len());
320    /// # Ok(())
321    /// # }
322    /// ```
323    #[cfg(feature = "macos_14_4")]
324    pub fn current_process() -> AsyncShareableContentFuture {
325        let (future, context) = AsyncCompletion::create();
326
327        // 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.
328        unsafe {
329            crate::ffi::sc_shareable_content_get_current_process_displays(
330                shareable_content_callback,
331                context,
332            );
333        }
334
335        AsyncShareableContentFuture { inner: future }
336    }
337}
338
339// ============================================================================
340// AsyncSCStream - Async stream with integrated frame iteration
341// ============================================================================
342
343/// Async iterator over sample buffers.
344///
345/// # Delivery semantics
346///
347/// This is a **lossy, bounded** buffer. When the queue is full (`capacity`
348/// reached) and a new sample arrives faster than the consumer polls it,
349/// the **oldest** buffered sample is dropped to make room for the newest
350/// (drop-oldest policy). This keeps latency low and favors fresh frames over
351/// stale ones, but means consumers that fall behind will miss intermediate
352/// frames rather than blocking the capture callback.
353struct AsyncSampleIteratorState {
354    buffer: std::collections::VecDeque<(crate::cm::CMSampleBuffer, SCStreamOutputType)>,
355    /// Every task currently parked on this queue.
356    ///
357    /// A single `Option<Waker>` would lose wakeups as soon as two consumers
358    /// exist (e.g. `frames()` in one task and `next_typed()` in another): the
359    /// second `poll` would overwrite the first task's waker and that task would
360    /// never be woken. `AsyncSCStream` hands out `&self` borrows, so multiple
361    /// concurrent consumers are perfectly legal — we wake all of them and let
362    /// the losers see an empty queue and re-park.
363    waiters: Vec<RegisteredWaker>,
364    closed: bool,
365    /// Set once `stop_capture()` succeeds. `ScreenCaptureKit` cannot restart a
366    /// stopped `SCStream`, so this makes the refusal explicit instead of
367    /// letting `start_capture()` fail with an opaque Apple error.
368    stopped: bool,
369    capacity: usize,
370    /// Live `AsyncSampleSender`s. The queue closes when the last one drops, not
371    /// the first — a multi-output stream has one sender per output type.
372    senders: usize,
373    stop_error: Option<SCError>,
374}
375
376/// Internal sender for async sample iterator
377struct AsyncSampleSender {
378    inner: Arc<Mutex<AsyncSampleIteratorState>>,
379}
380
381impl AsyncSampleSender {
382    fn new(state: &Arc<Mutex<AsyncSampleIteratorState>>) -> Self {
383        if let Ok(mut s) = state.lock() {
384            s.senders += 1;
385        }
386        Self {
387            inner: Arc::clone(state),
388        }
389    }
390}
391
392impl crate::stream::output_trait::SCStreamOutputTrait for AsyncSampleSender {
393    fn did_output_sample_buffer(
394        &self,
395        sample_buffer: crate::cm::CMSampleBuffer,
396        of_type: SCStreamOutputType,
397    ) {
398        let Ok(mut state) = self.inner.lock() else {
399            return;
400        };
401
402        // Drop oldest if at capacity. The evicted buffer is released *after*
403        // the lock, because dropping a CMSampleBuffer calls into CoreMedia.
404        let evicted = if state.buffer.len() >= state.capacity {
405            state.buffer.pop_front()
406        } else {
407            None
408        };
409
410        state.buffer.push_back((sample_buffer, of_type));
411
412        let waiters = std::mem::take(&mut state.waiters);
413        drop(state);
414
415        drop(evicted);
416        // Waking outside the lock: a waker may poll the future inline (single
417        // threaded executors do), which would re-enter `lock()` and deadlock.
418        wake_all(waiters);
419    }
420}
421
422impl Drop for AsyncSampleSender {
423    fn drop(&mut self) {
424        let Ok(mut state) = self.inner.lock() else {
425            return;
426        };
427        state.senders = state.senders.saturating_sub(1);
428        if state.senders > 0 {
429            return;
430        }
431        state.closed = true;
432        let waiters = std::mem::take(&mut state.waiters);
433        drop(state);
434        wake_all(waiters);
435    }
436}
437
438/// Shared poll logic for the sample futures/streams: pop the next buffered
439/// `(buffer, type)` pair, resolve to `None` when closed, or register the waker.
440fn poll_next_sample(
441    state: &Arc<Mutex<AsyncSampleIteratorState>>,
442    waiter: &Arc<()>,
443    cx: &Context<'_>,
444) -> Poll<Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)>> {
445    let waker = cx.waker().clone();
446    let Ok(mut state) = state.lock() else {
447        return Poll::Ready(None);
448    };
449
450    if let Some(sample) = state.buffer.pop_front() {
451        let removed = unregister_waker(&mut state.waiters, waiter);
452        drop(state);
453        drop(removed);
454        drop(waker);
455        return Poll::Ready(Some(sample));
456    }
457
458    if state.closed {
459        let removed = unregister_waker(&mut state.waiters, waiter);
460        drop(state);
461        drop(removed);
462        drop(waker);
463        Poll::Ready(None)
464    } else {
465        let replaced = register_waker(&mut state.waiters, waiter, waker);
466        drop(state);
467        drop(replaced);
468        Poll::Pending
469    }
470}
471
472/// Future for getting the next sample buffer
473pub struct NextSample<'a> {
474    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
475    waiter: Arc<()>,
476}
477
478impl std::fmt::Debug for NextSample<'_> {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        f.debug_struct("NextSample").finish_non_exhaustive()
481    }
482}
483
484impl Future for NextSample<'_> {
485    type Output = Option<crate::cm::CMSampleBuffer>;
486
487    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
488        poll_next_sample(self.state, &self.waiter, cx)
489            .map(|opt| opt.map(|(buffer, _of_type)| buffer))
490    }
491}
492
493impl Drop for NextSample<'_> {
494    fn drop(&mut self) {
495        let removed = self
496            .state
497            .lock()
498            .ok()
499            .and_then(|mut state| unregister_waker(&mut state.waiters, &self.waiter));
500        drop(removed);
501    }
502}
503
504/// Future for getting the next sample buffer together with its output type.
505///
506/// Like [`NextSample`], but yields the [`SCStreamOutputType`] alongside the
507/// buffer so consumers of a multi-output stream (e.g. screen + audio via
508/// [`AsyncSCStream::add_output_type`]) can tell frames apart. Returned by
509/// [`AsyncSCStream::next_typed`].
510pub struct NextSampleTyped<'a> {
511    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
512    waiter: Arc<()>,
513}
514
515impl std::fmt::Debug for NextSampleTyped<'_> {
516    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517        f.debug_struct("NextSampleTyped").finish_non_exhaustive()
518    }
519}
520
521impl Future for NextSampleTyped<'_> {
522    type Output = Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)>;
523
524    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
525        poll_next_sample(self.state, &self.waiter, cx)
526    }
527}
528
529impl Drop for NextSampleTyped<'_> {
530    fn drop(&mut self) {
531        let removed = self
532            .state
533            .lock()
534            .ok()
535            .and_then(|mut state| unregister_waker(&mut state.waiters, &self.waiter));
536        drop(removed);
537    }
538}
539
540/// A [`Stream`](futures_core::Stream) of captured sample buffers.
541///
542/// Yields `CMSampleBuffer`s and ends (`None`) when the stream closes. Returned
543/// by [`AsyncSCStream::frames`]; it borrows the stream and is `Unpin`, so it
544/// plugs straight into the `futures::StreamExt` combinators:
545///
546/// ```no_run
547/// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
548/// use futures_util::StreamExt;
549/// let first_ten: Vec<_> = stream.frames().take(10).collect().await;
550/// # let _ = first_ten;
551/// # }
552/// ```
553pub struct SampleStream<'a> {
554    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
555    waiter: Arc<()>,
556}
557
558impl std::fmt::Debug for SampleStream<'_> {
559    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560        f.debug_struct("SampleStream").finish_non_exhaustive()
561    }
562}
563
564impl futures_core::Stream for SampleStream<'_> {
565    type Item = crate::cm::CMSampleBuffer;
566
567    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
568        poll_next_sample(self.state, &self.waiter, cx)
569            .map(|opt| opt.map(|(buffer, _of_type)| buffer))
570    }
571}
572
573impl Drop for SampleStream<'_> {
574    fn drop(&mut self) {
575        let removed = self
576            .state
577            .lock()
578            .ok()
579            .and_then(|mut state| unregister_waker(&mut state.waiters, &self.waiter));
580        drop(removed);
581    }
582}
583
584/// A [`Stream`](futures_core::Stream) of captured sample buffers tagged with
585/// their [`SCStreamOutputType`].
586///
587/// Like [`SampleStream`] but yields `(CMSampleBuffer, SCStreamOutputType)` so a
588/// multi-output stream's audio and video can be told apart. Returned by
589/// [`AsyncSCStream::frames_typed`].
590pub struct TypedSampleStream<'a> {
591    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
592    waiter: Arc<()>,
593}
594
595impl std::fmt::Debug for TypedSampleStream<'_> {
596    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
597        f.debug_struct("TypedSampleStream").finish_non_exhaustive()
598    }
599}
600
601impl futures_core::Stream for TypedSampleStream<'_> {
602    type Item = (crate::cm::CMSampleBuffer, SCStreamOutputType);
603
604    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
605        poll_next_sample(self.state, &self.waiter, cx)
606    }
607}
608
609impl Drop for TypedSampleStream<'_> {
610    fn drop(&mut self) {
611        let removed = self
612            .state
613            .lock()
614            .ok()
615            .and_then(|mut state| unregister_waker(&mut state.waiters, &self.waiter));
616        drop(removed);
617    }
618}
619
620// SAFETY: `AsyncSampleSender` holds `Arc<Mutex<AsyncSampleIteratorState>>`.
621// `AsyncSampleIteratorState` buffers `(CMSampleBuffer, SCStreamOutputType)`
622// pairs plus registered wakers and `Option<SCError>`; `CMSampleBuffer` has its
623// own `unsafe impl Send` (it is an Apple-owned handle safe to transfer across
624// threads) and the rest are `Send + Sync`, so the whole `Arc<Mutex<...>>` is
625// safe to send and share across threads.
626unsafe impl Send for AsyncSampleSender {}
627unsafe impl Sync for AsyncSampleSender {}
628
629/// Stream delegate for [`AsyncSCStream`] that closes the sample iterator when
630/// `ScreenCaptureKit` stops the stream with an error.
631///
632/// Without this, a stream that fails mid-capture (captured display
633/// disconnected, permission revoked, …) would leave [`NextSample`] pending
634/// forever. On an error stop this records the [`SCError`], marks the iterator
635/// closed (so `next()` resolves to `None` once buffered frames drain), and
636/// wakes any parked task. The error is retrievable via
637/// [`AsyncSCStream::take_error`].
638struct AsyncStreamDelegate {
639    state: Arc<Mutex<AsyncSampleIteratorState>>,
640}
641
642impl crate::stream::delegate_trait::SCStreamDelegateTrait for AsyncStreamDelegate {
643    fn did_stop_with_error(&self, error: SCError) {
644        close_sample_state(&self.state, Some(error), false);
645    }
646}
647
648/// Close the sample queue, optionally recording why, and wake every parked
649/// consumer once the lock is released.
650fn close_sample_state(
651    state: &Arc<Mutex<AsyncSampleIteratorState>>,
652    error: Option<SCError>,
653    stopped: bool,
654) {
655    let Ok(mut state) = state.lock() else {
656        return;
657    };
658    if let Some(error) = error {
659        state.stop_error = Some(error);
660    }
661    state.stopped |= stopped;
662    state.closed = true;
663    let waiters = std::mem::take(&mut state.waiters);
664    drop(state);
665    wake_all(waiters);
666}
667
668/// Reopen a queue that a failed `start_capture` closed, so the retry that
669/// failure permits can deliver frames.
670///
671/// The start-failure hook deliberately re-arms `capture_state` (a start that
672/// never began capturing has not stopped the stream), but `closed` is
673/// otherwise write-once. Without this the retry would hand the consumer
674/// whatever is still buffered and then report end-of-stream on a capture that
675/// is genuinely running.
676///
677/// No-op once `stop_capture` has succeeded: `ScreenCaptureKit` cannot restart
678/// a stopped `SCStream`.
679fn reopen_sample_state(state: &Arc<Mutex<AsyncSampleIteratorState>>) {
680    let Ok(mut state) = state.lock() else {
681        return;
682    };
683    if state.stopped {
684        return;
685    }
686    state.closed = false;
687    state.stop_error = None;
688}
689
690// SAFETY: mirrors `AsyncSampleSender` — `AsyncStreamDelegate` holds the same
691// `Arc<Mutex<AsyncSampleIteratorState>>`, whose contents (`CMSampleBuffer`,
692// `Waker`, `SCError`) are all safe to send and share across threads.
693unsafe impl Send for AsyncStreamDelegate {}
694unsafe impl Sync for AsyncStreamDelegate {}
695
696// ----------------------------------------------------------------------------
697// Stream lifecycle control futures (start / stop / update)
698// ----------------------------------------------------------------------------
699
700/// FFI completion callback for [`AsyncSCStream`] lifecycle operations.
701///
702/// Translates the Swift `(context, success, message)` completion into the
703/// waker-based [`AsyncCompletion`] machinery, so awaiting a control future
704/// resumes the task via its [`Waker`] instead of parking a thread. This is the
705/// same primitive used by the content / screenshot / picker futures.
706extern "C" fn stream_control_callback(context: *mut c_void, success: bool, msg: *const i8) {
707    crate::utils::panic_safe::catch_user_panic("stream_control_callback", move || {
708        if success {
709            // SAFETY: `context` is the one-shot completion pointer from
710            // `AsyncCompletion::<()>::create()`; Swift invokes this callback
711            // exactly once, after which the pointer is consumed.
712            unsafe { AsyncCompletion::<()>::complete_ok(context, ()) };
713        } else {
714            let error = unsafe { error_from_cstr(msg) };
715            // SAFETY: see above — one-shot completion pointer, fired once.
716            unsafe { AsyncCompletion::<()>::complete_err(context, error) };
717        }
718    });
719}
720
721/// Future for an [`AsyncSCStream`] lifecycle operation — `start_capture`,
722/// `stop_capture`, `update_configuration`, or `update_content_filter`.
723///
724/// Resolves once `ScreenCaptureKit` acknowledges the operation. Awaiting it
725/// **never blocks the executor thread**: the task is parked via its [`Waker`]
726/// and resumed from the Swift completion callback. This mirrors the underlying
727/// Swift `Task { try await … }` entry points, keeping the async surface
728/// consistent end to end.
729///
730/// The operation is kicked off eagerly when the method is called (a "hot"
731/// future), matching the rest of this module — e.g.
732/// [`AsyncSCShareableContent::get`]. Dropping the future without awaiting is
733/// safe; it simply means success/failure is not observed.
734#[must_use = "the operation starts eagerly, but you must .await the future to observe success or failure"]
735pub struct StreamControlFuture {
736    inner: AsyncCompletionFuture<()>,
737    map_err: fn(String) -> SCError,
738}
739
740impl StreamControlFuture {
741    fn succeeded(map_err: fn(String) -> SCError) -> Self {
742        let (inner, context) = AsyncCompletion::<()>::create();
743        // SAFETY: this completion context has not been shared with FFI.
744        unsafe { AsyncCompletion::<()>::complete_ok(context, ()) };
745        Self { inner, map_err }
746    }
747
748    /// A future that is already resolved with `error`, for operations rejected
749    /// before they reach `ScreenCaptureKit`.
750    fn failed(map_err: fn(String) -> SCError, error: String) -> Self {
751        let (inner, context) = AsyncCompletion::<()>::create();
752        // SAFETY: `context` is the one-shot completion pointer we just created
753        // and have not handed to FFI, so this is its only completion.
754        unsafe { AsyncCompletion::<()>::complete_err(context, error) };
755        Self { inner, map_err }
756    }
757}
758
759impl std::fmt::Debug for StreamControlFuture {
760    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
761        f.debug_struct("StreamControlFuture")
762            .finish_non_exhaustive()
763    }
764}
765
766impl Future for StreamControlFuture {
767    type Output = Result<(), SCError>;
768
769    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
770        let map_err = self.map_err;
771        let Poll::Ready(result) = Pin::new(&mut self.inner).poll(cx) else {
772            return Poll::Pending;
773        };
774
775        Poll::Ready(result.map_err(map_err))
776    }
777}
778
779/// Async wrapper for `SCStream` with integrated frame iteration
780///
781/// Provides async methods for stream lifecycle and frame iteration.
782/// **Executor-agnostic** - works with any async runtime.
783///
784/// # Examples
785///
786/// ```rust,no_run
787/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
788/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
789/// use screencapturekit::stream::configuration::SCStreamConfiguration;
790/// use screencapturekit::stream::content_filter::SCContentFilter;
791/// use screencapturekit::stream::output_type::SCStreamOutputType;
792///
793/// let content = AsyncSCShareableContent::get().await?;
794/// let display = &content.displays()[0];
795/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
796/// let config = SCStreamConfiguration::new()
797///     .with_width(1920)
798///     .with_height(1080);
799///
800/// let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen)?;
801/// stream.start_capture().await?;
802///
803/// // Process frames asynchronously
804/// while let Some(frame) = stream.next().await {
805///     println!("Got frame!");
806/// }
807/// # Ok(())
808/// # }
809/// ```
810/// Async wrapper for `SCStream` with integrated frame iteration.
811///
812/// # Back-pressure and frame loss
813///
814/// `AsyncSCStream` buffers samples in a **bounded** internal queue sized
815/// by the `buffer_capacity` argument to [`AsyncSCStream::new`]. When the
816/// queue is full and a new sample arrives from `ScreenCaptureKit`, the
817/// **oldest** queued sample is dropped to make room — the stream is
818/// **lossy by design**.
819///
820/// This is the right policy for real-time UI rendering, screen-share
821/// previews, and live encoding: a slow consumer would rather see the
822/// most recent frame than a stale one. It is the *wrong* policy for
823/// lossless capture (e.g. saving every frame to disk for later
824/// editing) — for that, use the synchronous [`SCStream`](crate::stream::SCStream)
825/// directly, where back-pressure is naturally enforced by Apple's
826/// `queueDepth` setting and your handler's runtime.
827///
828/// To detect when frames are being dropped, watch `buffered_count()`
829/// against `buffer_capacity` over time, or instrument your handler
830/// with a per-frame timestamp delta and compare to your expected
831/// frame interval.
832pub struct AsyncSCStream {
833    stream: crate::stream::SCStream,
834    iterator_state: Arc<Mutex<AsyncSampleIteratorState>>,
835}
836
837impl AsyncSCStream {
838    /// Create a new async stream
839    ///
840    /// # Arguments
841    ///
842    /// * `filter` - Content filter specifying what to capture
843    /// * `config` - Stream configuration
844    /// * `buffer_capacity` - Max frames to buffer (oldest dropped when full);
845    ///   `0` is treated as `1`, since the queue must hold the sample it is
846    ///   about to hand to the consumer
847    /// * `output_type` - Type of output (Screen, Audio, Microphone)
848    #[allow(clippy::missing_errors_doc)]
849    pub fn new(
850        filter: &SCContentFilter,
851        config: &SCStreamConfiguration,
852        buffer_capacity: usize,
853        output_type: crate::stream::output_type::SCStreamOutputType,
854    ) -> Result<Self, SCError> {
855        let state = Arc::new(Mutex::new(AsyncSampleIteratorState {
856            buffer: std::collections::VecDeque::with_capacity(buffer_capacity),
857            waiters: Vec::new(),
858            closed: false,
859            stopped: false,
860            capacity: buffer_capacity.max(1),
861            senders: 0,
862            stop_error: None,
863        }));
864
865        let sender = AsyncSampleSender::new(&state);
866
867        let delegate = AsyncStreamDelegate {
868            state: Arc::clone(&state),
869        };
870
871        let mut stream = crate::stream::SCStream::new_with_delegate(filter, config, delegate)?;
872        stream.add_output_handler(sender, output_type)?;
873
874        Ok(Self {
875            stream,
876            iterator_state: state,
877        })
878    }
879
880    /// Get the next sample buffer asynchronously
881    ///
882    /// Returns `None` when the stream is closed. For a multi-output stream
883    /// (see [`add_output_type`](Self::add_output_type)) use
884    /// [`next_typed`](Self::next_typed) to also learn each sample's
885    /// [`SCStreamOutputType`].
886    pub fn next(&self) -> NextSample<'_> {
887        NextSample {
888            state: &self.iterator_state,
889            waiter: Arc::new(()),
890        }
891    }
892
893    /// Get the next sample buffer together with its output type.
894    ///
895    /// Use this when the stream carries more than one output type (e.g. screen
896    /// and audio) and you need to tell the samples apart. Returns `None` when
897    /// the stream is closed.
898    pub fn next_typed(&self) -> NextSampleTyped<'_> {
899        NextSampleTyped {
900            state: &self.iterator_state,
901            waiter: Arc::new(()),
902        }
903    }
904
905    /// Borrow the captured frames as a [`Stream`](futures_core::Stream) of
906    /// `CMSampleBuffer`s.
907    ///
908    /// This unlocks the `futures::StreamExt` combinator ecosystem
909    /// (`map`, `filter`, `take`, `for_each`, `zip`, …) for processing frames:
910    ///
911    /// ```no_run
912    /// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
913    /// use futures_util::StreamExt;
914    ///
915    /// let frames: Vec<_> = stream.frames().take(30).collect().await;
916    /// # let _ = frames;
917    /// # }
918    /// ```
919    ///
920    /// The returned stream borrows `self`; for a multi-output stream use
921    /// [`frames_typed`](Self::frames_typed) to keep each sample's output type.
922    #[must_use]
923    pub fn frames(&self) -> SampleStream<'_> {
924        SampleStream {
925            state: &self.iterator_state,
926            waiter: Arc::new(()),
927        }
928    }
929
930    /// Borrow the captured frames as a [`Stream`](futures_core::Stream) of
931    /// `(CMSampleBuffer, SCStreamOutputType)` pairs.
932    ///
933    /// Like [`frames`](Self::frames) but keeps each sample's output type, so a
934    /// stream carrying both audio and video (see
935    /// [`add_output_type`](Self::add_output_type)) can route samples with
936    /// `StreamExt` combinators:
937    ///
938    /// ```no_run
939    /// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
940    /// use futures_util::StreamExt;
941    /// use screencapturekit::stream::output_type::SCStreamOutputType;
942    ///
943    /// let audio: Vec<_> = stream
944    ///     .frames_typed()
945    ///     .filter(|(_, kind)| std::future::ready(*kind == SCStreamOutputType::Audio))
946    ///     .take(10)
947    ///     .collect()
948    ///     .await;
949    /// # let _ = audio;
950    /// # }
951    /// ```
952    #[must_use]
953    pub fn frames_typed(&self) -> TypedSampleStream<'_> {
954        TypedSampleStream {
955            state: &self.iterator_state,
956            waiter: Arc::new(()),
957        }
958    }
959
960    /// Also deliver samples of an additional output type.
961    ///
962    /// By default an [`AsyncSCStream`] carries the single output type passed to
963    /// [`new`](Self::new). Call this to capture more than one type from one
964    /// stream — for example add [`SCStreamOutputType::Audio`] to a stream
965    /// created for [`SCStreamOutputType::Screen`] to capture audio and video
966    /// together. Samples from every registered type share the same lossy
967    /// buffer; use [`next_typed`](Self::next_typed) /
968    /// [`try_next_typed`](Self::try_next_typed) to distinguish them.
969    ///
970    /// # Errors
971    ///
972    /// Returns the [`SCStream::add_output_handler`](crate::stream::SCStream::add_output_handler)
973    /// error when registration fails, for example if the stream configuration
974    /// does not enable that type (e.g. audio capture was not configured); on
975    /// failure the already-registered types keep flowing and the queue stays
976    /// open.
977    pub fn add_output_type(&mut self, output_type: SCStreamOutputType) -> Result<(), SCError> {
978        let sender = AsyncSampleSender::new(&self.iterator_state);
979        self.stream.add_output_handler(sender, output_type)?;
980        Ok(())
981    }
982
983    /// Try to get a sample without waiting
984    #[must_use]
985    pub fn try_next(&self) -> Option<crate::cm::CMSampleBuffer> {
986        self.iterator_state
987            .lock()
988            .ok()?
989            .buffer
990            .pop_front()
991            .map(|(buffer, _of_type)| buffer)
992    }
993
994    /// Try to get a sample together with its output type, without waiting.
995    #[must_use]
996    pub fn try_next_typed(&self) -> Option<(crate::cm::CMSampleBuffer, SCStreamOutputType)> {
997        self.iterator_state.lock().ok()?.buffer.pop_front()
998    }
999
1000    /// Check if the stream has been closed
1001    ///
1002    /// Returns `true` once the sample queue has been closed — because
1003    /// [`stop_capture`](Self::stop_capture) succeeded, because this
1004    /// `AsyncSCStream`'s handlers were dropped, or because `ScreenCaptureKit`
1005    /// stopped the stream with an error (see [`take_error`](Self::take_error)).
1006    /// Buffered frames still drain through [`next`](Self::next) after this
1007    /// turns `true`.
1008    #[must_use]
1009    pub fn is_closed(&self) -> bool {
1010        self.iterator_state.lock().map_or(true, |s| s.closed)
1011    }
1012
1013    /// Take the error that stopped the stream, if any.
1014    ///
1015    /// When `ScreenCaptureKit` stops the stream with an error (e.g. the
1016    /// captured display is disconnected or screen-recording permission is
1017    /// revoked), the sample iterator is closed — [`next`](Self::next) resolves
1018    /// to `None` after any buffered frames drain — and the [`SCError`] is stored
1019    /// here. Call this once the iteration loop ends to distinguish an error stop
1020    /// from a normal end of stream:
1021    ///
1022    /// ```no_run
1023    /// # async fn example(stream: screencapturekit::async_api::AsyncSCStream) {
1024    /// while let Some(_frame) = stream.next().await {
1025    ///     // process frames …
1026    /// }
1027    /// if let Some(err) = stream.take_error() {
1028    ///     eprintln!("capture stopped with error: {err}");
1029    /// }
1030    /// # }
1031    /// ```
1032    ///
1033    /// The stored error is cleared once taken.
1034    #[must_use]
1035    pub fn take_error(&self) -> Option<SCError> {
1036        self.iterator_state.lock().ok()?.stop_error.take()
1037    }
1038
1039    /// Get the number of buffered samples
1040    #[must_use]
1041    pub fn buffered_count(&self) -> usize {
1042        self.iterator_state.lock().map_or(0, |s| s.buffer.len())
1043    }
1044
1045    /// Clear all buffered samples
1046    pub fn clear_buffer(&self) {
1047        let Ok(mut state) = self.iterator_state.lock() else {
1048            return;
1049        };
1050        // Dropping a CMSampleBuffer calls into CoreMedia, so hand the samples
1051        // out of the guard and release them unlocked.
1052        let discarded = std::mem::take(&mut state.buffer);
1053        drop(state);
1054        drop(discarded);
1055    }
1056
1057    /// Start capture asynchronously.
1058    ///
1059    /// Resolves when `ScreenCaptureKit` confirms the stream has started.
1060    /// Unlike [`SCStream::start_capture`](crate::stream::SCStream::start_capture),
1061    /// awaiting this **does not block the executor thread** — the task is parked
1062    /// via its [`Waker`] and resumed from the Swift completion callback.
1063    ///
1064    /// The capture is initiated eagerly when this method is called; `.await`
1065    /// observes the completion (or error). If starting fails, the sample queue
1066    /// is closed so [`next`](Self::next) resolves to `None` rather than pending
1067    /// forever on a stream that never ran.
1068    ///
1069    /// # Restarting is not supported
1070    ///
1071    /// `ScreenCaptureKit` cannot restart a stopped `SCStream`. Once
1072    /// [`stop_capture`](Self::stop_capture) has succeeded, this resolves to
1073    /// `Err` without touching the native stream; create a new `AsyncSCStream`
1074    /// to capture again.
1075    ///
1076    /// # Errors
1077    ///
1078    /// The awaited result is `Err(SCError::CaptureStartFailed)` if the stream
1079    /// fails to start or has already been stopped.
1080    pub fn start_capture(&self) -> StreamControlFuture {
1081        if self.iterator_state.lock().is_ok_and(|s| s.stopped) {
1082            return StreamControlFuture::failed(
1083                SCError::CaptureStartFailed,
1084                "an SCStream cannot be restarted after stop_capture(); create a new AsyncSCStream"
1085                    .to_string(),
1086            );
1087        }
1088
1089        let capture_state = self.stream.capture_state();
1090        let start_unconfirmed = self.stream.start_unconfirmed_state();
1091        if !crate::stream::sc_stream::claim_start(&capture_state, &start_unconfirmed) {
1092            return StreamControlFuture::succeeded(SCError::CaptureStartFailed);
1093        }
1094        reopen_sample_state(&self.iterator_state);
1095        let iterator_state = Arc::clone(&self.iterator_state);
1096        let (future, context) =
1097            AsyncCompletion::<()>::create_with_hook(move |result| match result {
1098                Ok(()) => capture_state.store(true, std::sync::atomic::Ordering::Release),
1099                // The native start is still outstanding: clearing
1100                // `capture_state` would let a retry double-start, so record
1101                // the unconfirmed outcome and let the next start reissue.
1102                Err(message) if is_timeout_error(message) => {
1103                    start_unconfirmed.store(true, std::sync::atomic::Ordering::Release);
1104                }
1105                Err(message) => {
1106                    capture_state.store(false, std::sync::atomic::Ordering::Release);
1107                    close_sample_state(
1108                        &iterator_state,
1109                        Some(SCError::CaptureStartFailed(message.clone())),
1110                        false,
1111                    );
1112                }
1113            });
1114        // SAFETY: `self.stream.as_ptr()` is a valid, live `SCStream` pointer for
1115        // the duration of this call; `context` is the one-shot completion
1116        // pointer from `AsyncCompletion::create()`, invoked exactly once.
1117        unsafe {
1118            crate::ffi::sc_stream_start_capture(
1119                self.stream.as_ptr(),
1120                context,
1121                stream_control_callback,
1122            );
1123        }
1124        StreamControlFuture {
1125            inner: future,
1126            map_err: SCError::CaptureStartFailed,
1127        }
1128    }
1129
1130    /// Stop capture asynchronously.
1131    ///
1132    /// Resolves when `ScreenCaptureKit` confirms the stream has stopped. Awaiting
1133    /// this **does not block the executor thread**.
1134    ///
1135    /// A clean stop is never reported to the delegate, so awaiting a successful
1136    /// stop also closes the sample queue: [`next`](Self::next) resolves to
1137    /// `None` once the already-buffered frames drain, and
1138    /// [`take_error`](Self::take_error) stays `None`. The stream cannot be
1139    /// restarted afterwards — see [`start_capture`](Self::start_capture).
1140    ///
1141    /// # Errors
1142    ///
1143    /// The awaited result is `Err(SCError::CaptureStopFailed)` if the stream
1144    /// fails to stop.
1145    pub fn stop_capture(&self) -> StreamControlFuture {
1146        let capture_state = self.stream.capture_state();
1147        let iterator_state = Arc::clone(&self.iterator_state);
1148        let (future, context) = AsyncCompletion::<()>::create_with_hook(move |result| {
1149            if result.is_ok() {
1150                capture_state.store(false, std::sync::atomic::Ordering::Release);
1151                close_sample_state(&iterator_state, None, true);
1152            }
1153        });
1154        // SAFETY: see `start_capture` — live stream pointer, one-shot context.
1155        unsafe {
1156            crate::ffi::sc_stream_stop_capture(
1157                self.stream.as_ptr(),
1158                context,
1159                stream_control_callback,
1160            );
1161        }
1162        StreamControlFuture {
1163            inner: future,
1164            map_err: SCError::CaptureStopFailed,
1165        }
1166    }
1167
1168    /// Update stream configuration asynchronously.
1169    ///
1170    /// Resolves when the reconfiguration completes. Awaiting this **does not
1171    /// block the executor thread**.
1172    ///
1173    /// # Errors
1174    ///
1175    /// The awaited result is `Err(SCError::StreamError)` if the update fails.
1176    #[cfg(feature = "macos_14_0")]
1177    pub fn update_configuration(&self, config: &SCStreamConfiguration) -> StreamControlFuture {
1178        let config = config.clone();
1179        let (future, context) = AsyncCompletion::<()>::create();
1180        // SAFETY: `self.stream.as_ptr()` and `config.as_ptr()` are valid for the
1181        // duration of this call; `context` is the one-shot completion pointer.
1182        unsafe {
1183            crate::ffi::sc_stream_update_configuration(
1184                self.stream.as_ptr(),
1185                config.as_ptr(),
1186                context,
1187                stream_control_callback,
1188            );
1189        }
1190        StreamControlFuture {
1191            inner: future,
1192            map_err: SCError::StreamError,
1193        }
1194    }
1195
1196    /// Update content filter asynchronously.
1197    ///
1198    /// Resolves when the filter swap completes. Awaiting this **does not block
1199    /// the executor thread**.
1200    ///
1201    /// # Errors
1202    ///
1203    /// The awaited result is `Err(SCError::StreamError)` if the update fails.
1204    pub fn update_content_filter(&self, filter: &SCContentFilter) -> StreamControlFuture {
1205        let (future, context) = AsyncCompletion::<()>::create();
1206        // SAFETY: `self.stream.as_ptr()` and `filter.as_ptr()` are valid for the
1207        // duration of this call; `context` is the one-shot completion pointer.
1208        unsafe {
1209            crate::ffi::sc_stream_update_content_filter(
1210                self.stream.as_ptr(),
1211                filter.as_ptr(),
1212                context,
1213                stream_control_callback,
1214            );
1215        }
1216        StreamControlFuture {
1217            inner: future,
1218            map_err: SCError::StreamError,
1219        }
1220    }
1221
1222    /// Get a reference to the underlying stream
1223    #[must_use]
1224    pub fn inner(&self) -> &crate::stream::SCStream {
1225        &self.stream
1226    }
1227}
1228
1229impl std::fmt::Debug for AsyncSCStream {
1230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1231        f.debug_struct("AsyncSCStream")
1232            .field("stream", &self.stream)
1233            .field("buffered_count", &self.buffered_count())
1234            .field("is_closed", &self.is_closed())
1235            .finish_non_exhaustive()
1236    }
1237}
1238
1239// ============================================================================
1240// AsyncSCScreenshotManager - Async screenshot capture (macOS 14.0+)
1241// ============================================================================
1242
1243/// Async wrapper for `SCScreenshotManager`
1244///
1245/// Provides async methods for single-frame screenshot capture.
1246/// **Executor-agnostic** - works with any async runtime.
1247///
1248/// Requires the `macos_14_0` feature flag.
1249///
1250/// # Examples
1251///
1252/// ```rust,no_run
1253/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1254/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCScreenshotManager};
1255/// use screencapturekit::stream::configuration::SCStreamConfiguration;
1256/// use screencapturekit::stream::content_filter::SCContentFilter;
1257///
1258/// let content = AsyncSCShareableContent::get().await?;
1259/// let display = &content.displays()[0];
1260/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
1261/// let config = SCStreamConfiguration::new()
1262///     .with_width(1920)
1263///     .with_height(1080);
1264///
1265/// let image = AsyncSCScreenshotManager::capture_image(&filter, &config).await?;
1266/// println!("Screenshot: {}x{}", image.width(), image.height());
1267/// # Ok(())
1268/// # }
1269/// ```
1270#[cfg(feature = "macos_14_0")]
1271#[derive(Debug, Clone, Copy)]
1272pub struct AsyncSCScreenshotManager;
1273
1274/// Callback for async `CGImage` capture
1275#[cfg(feature = "macos_14_0")]
1276extern "C" fn screenshot_image_callback(
1277    image_ptr: *const c_void,
1278    error_ptr: *const i8,
1279    user_data: *mut c_void,
1280) {
1281    crate::utils::panic_safe::catch_user_panic("screenshot_image_callback", move || {
1282        if !error_ptr.is_null() {
1283            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
1284            let error = unsafe { error_from_cstr(error_ptr) };
1285            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1286            unsafe {
1287                AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(
1288                    user_data, error,
1289                );
1290            }
1291        } else if !image_ptr.is_null() {
1292            // SAFETY: the Swift bridge hands back a retained `CGImageRef` on success.
1293            let image = unsafe { crate::screenshot_manager::cgimage_from_retained_ptr(image_ptr) };
1294            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1295            unsafe { AsyncCompletion::complete_ok(user_data, image) };
1296        } else {
1297            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1298            unsafe {
1299                AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(
1300                    user_data,
1301                    "Unknown error".to_string(),
1302                );
1303            };
1304        }
1305    });
1306}
1307
1308/// Callback for async `CMSampleBuffer` capture
1309#[cfg(feature = "macos_14_0")]
1310extern "C" fn screenshot_buffer_callback(
1311    buffer_ptr: *const c_void,
1312    error_ptr: *const i8,
1313    user_data: *mut c_void,
1314) {
1315    crate::utils::panic_safe::catch_user_panic("screenshot_buffer_callback", move || {
1316        if !error_ptr.is_null() {
1317            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
1318            let error = unsafe { error_from_cstr(error_ptr) };
1319            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1320            unsafe { AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
1321        } else if !buffer_ptr.is_null() {
1322            // 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.
1323            let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
1324            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1325            unsafe { AsyncCompletion::complete_ok(user_data, buffer) };
1326        } else {
1327            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1328            unsafe {
1329                AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
1330                    user_data,
1331                    "Unknown error".to_string(),
1332                );
1333            };
1334        }
1335    });
1336}
1337
1338/// Future for async screenshot capture
1339#[cfg(feature = "macos_14_0")]
1340pub struct AsyncScreenshotFuture<T: Send + 'static> {
1341    inner: AsyncCompletionFuture<T>,
1342}
1343
1344#[cfg(feature = "macos_14_0")]
1345impl<T: Send + 'static> std::fmt::Debug for AsyncScreenshotFuture<T> {
1346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1347        f.debug_struct("AsyncScreenshotFuture")
1348            .finish_non_exhaustive()
1349    }
1350}
1351
1352#[cfg(feature = "macos_14_0")]
1353impl<T: Send + 'static> Future for AsyncScreenshotFuture<T> {
1354    type Output = Result<T, SCError>;
1355
1356    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1357        Pin::new(&mut self.inner)
1358            .poll(cx)
1359            .map(|r| r.map_err(SCError::ScreenshotError))
1360    }
1361}
1362
1363#[cfg(feature = "macos_14_0")]
1364impl AsyncSCScreenshotManager {
1365    /// Capture a single screenshot as a `CGImage` asynchronously
1366    ///
1367    /// # Errors
1368    /// Returns an error if:
1369    /// - Screen recording permission is not granted
1370    /// - The capture fails for any reason
1371    pub fn capture_image(
1372        content_filter: &crate::stream::content_filter::SCContentFilter,
1373        configuration: &SCStreamConfiguration,
1374    ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
1375        let configuration = configuration.clone();
1376        let (future, context) = AsyncCompletion::create();
1377
1378        // 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()`.
1379        unsafe {
1380            crate::ffi::sc_screenshot_manager_capture_image(
1381                content_filter.as_ptr(),
1382                configuration.as_ptr(),
1383                screenshot_image_callback,
1384                context,
1385            );
1386        }
1387
1388        AsyncScreenshotFuture { inner: future }
1389    }
1390
1391    /// Capture a single screenshot as a `CMSampleBuffer` asynchronously
1392    ///
1393    /// # Errors
1394    /// Returns an error if:
1395    /// - Screen recording permission is not granted
1396    /// - The capture fails for any reason
1397    pub fn capture_sample_buffer(
1398        content_filter: &crate::stream::content_filter::SCContentFilter,
1399        configuration: &SCStreamConfiguration,
1400    ) -> AsyncScreenshotFuture<crate::cm::CMSampleBuffer> {
1401        let configuration = configuration.clone();
1402        let (future, context) = AsyncCompletion::create();
1403
1404        // 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()`.
1405        unsafe {
1406            crate::ffi::sc_screenshot_manager_capture_sample_buffer(
1407                content_filter.as_ptr(),
1408                configuration.as_ptr(),
1409                screenshot_buffer_callback,
1410                context,
1411            );
1412        }
1413
1414        AsyncScreenshotFuture { inner: future }
1415    }
1416
1417    /// Capture a screenshot of a specific screen region asynchronously (macOS 15.2+)
1418    ///
1419    /// This method captures the content within the specified rectangle,
1420    /// which can span multiple displays.
1421    ///
1422    /// # Arguments
1423    /// * `rect` - The rectangle to capture, in screen coordinates (points)
1424    ///
1425    /// # Errors
1426    /// Returns an error if:
1427    /// - The system is not macOS 15.2+
1428    /// - Screen recording permission is not granted
1429    /// - The capture fails for any reason
1430    #[cfg(feature = "macos_15_2")]
1431    pub fn capture_image_in_rect(
1432        rect: crate::cg::CGRect,
1433    ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
1434        let (future, context) = AsyncCompletion::create();
1435
1436        // SAFETY: The rectangle coordinates are plain values passed by copy. `context` is a one-shot completion pointer from `AsyncCompletion::create()`.
1437        unsafe {
1438            crate::ffi::sc_screenshot_manager_capture_image_in_rect(
1439                rect.origin.x,
1440                rect.origin.y,
1441                rect.size.width,
1442                rect.size.height,
1443                screenshot_image_callback,
1444                context,
1445            );
1446        }
1447
1448        AsyncScreenshotFuture { inner: future }
1449    }
1450
1451    /// Capture a screenshot with advanced configuration asynchronously (macOS 26.0+)
1452    ///
1453    /// This method uses the new `SCScreenshotConfiguration` for more control
1454    /// over the screenshot output, including HDR support and file saving.
1455    ///
1456    /// # Arguments
1457    /// * `content_filter` - The content filter specifying what to capture
1458    /// * `configuration` - The screenshot configuration
1459    ///
1460    /// # Errors
1461    /// Returns an error if the capture fails
1462    #[cfg(feature = "macos_26_0")]
1463    pub fn capture_screenshot(
1464        content_filter: &crate::stream::content_filter::SCContentFilter,
1465        configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
1466    ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
1467        let (future, context) = AsyncCompletion::create();
1468
1469        // 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()`.
1470        unsafe {
1471            crate::ffi::sc_screenshot_manager_capture_screenshot(
1472                content_filter.as_ptr(),
1473                configuration.as_ptr(),
1474                screenshot_output_callback,
1475                context,
1476            );
1477        }
1478
1479        AsyncScreenshotFuture { inner: future }
1480    }
1481
1482    /// Capture a screenshot of a specific region with advanced configuration asynchronously (macOS 26.0+)
1483    ///
1484    /// # Arguments
1485    /// * `rect` - The rectangle to capture, in screen coordinates (points)
1486    /// * `configuration` - The screenshot configuration
1487    ///
1488    /// # Errors
1489    /// Returns an error if the capture fails
1490    #[cfg(feature = "macos_26_0")]
1491    pub fn capture_screenshot_in_rect(
1492        rect: crate::cg::CGRect,
1493        configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
1494    ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
1495        let (future, context) = AsyncCompletion::create();
1496
1497        // 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()`.
1498        unsafe {
1499            crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
1500                rect.origin.x,
1501                rect.origin.y,
1502                rect.size.width,
1503                rect.size.height,
1504                configuration.as_ptr(),
1505                screenshot_output_callback,
1506                context,
1507            );
1508        }
1509
1510        AsyncScreenshotFuture { inner: future }
1511    }
1512}
1513
1514/// Callback for async `SCScreenshotOutput` capture (macOS 26.0+)
1515#[cfg(feature = "macos_26_0")]
1516extern "C" fn screenshot_output_callback(
1517    output_ptr: *const c_void,
1518    error_ptr: *const i8,
1519    user_data: *mut c_void,
1520) {
1521    crate::utils::panic_safe::catch_user_panic("screenshot_output_callback", move || {
1522        if !error_ptr.is_null() {
1523            // SAFETY: `error` is non-null (checked above) and points to a valid null-terminated C string provided by the Swift completion handler.
1524            let error = unsafe { error_from_cstr(error_ptr) };
1525            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1526            unsafe {
1527                AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
1528                    user_data, error,
1529                );
1530            }
1531        } else if !output_ptr.is_null() {
1532            let output = crate::screenshot_manager::SCScreenshotOutput::from_ptr(output_ptr);
1533            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`.
1534            unsafe { AsyncCompletion::complete_ok(user_data, output) };
1535        } else {
1536            // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`; Swift invokes this callback exactly once, so the pointer is still valid.
1537            unsafe {
1538                AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
1539                    user_data,
1540                    "Unknown error".to_string(),
1541                );
1542            };
1543        }
1544    });
1545}
1546
1547// ============================================================================
1548// AsyncSCContentSharingPicker - Async content sharing picker (macOS 14.0+)
1549// ============================================================================
1550
1551/// Result from the async picker callback
1552#[cfg(feature = "macos_14_0")]
1553struct AsyncPickerCallbackResult {
1554    code: i32,
1555    ptr: *const c_void,
1556    ownership: AsyncPickerOwnership,
1557}
1558
1559#[cfg(feature = "macos_14_0")]
1560#[derive(Clone, Copy)]
1561enum AsyncPickerOwnership {
1562    Result,
1563    Filter,
1564}
1565
1566#[cfg(feature = "macos_14_0")]
1567impl AsyncPickerCallbackResult {
1568    fn take_ptr(&mut self) -> *const c_void {
1569        std::mem::replace(&mut self.ptr, std::ptr::null())
1570    }
1571}
1572
1573#[cfg(feature = "macos_14_0")]
1574impl Drop for AsyncPickerCallbackResult {
1575    fn drop(&mut self) {
1576        if self.ptr.is_null() {
1577            return;
1578        }
1579        unsafe {
1580            match self.ownership {
1581                AsyncPickerOwnership::Result => crate::ffi::sc_picker_result_release(self.ptr),
1582                AsyncPickerOwnership::Filter => crate::ffi::sc_content_filter_release(self.ptr),
1583            }
1584        }
1585    }
1586}
1587
1588#[cfg(feature = "macos_14_0")]
1589// SAFETY: `AsyncPickerCallbackResult` stores a `*const c_void` that is an
1590// Apple Objective-C object reference (`SCPickerResult`). All ScreenCaptureKit
1591// objects are thread-safe to pass across threads (they follow ObjC ARC rules),
1592// so sending this pointer to another thread is sound.
1593unsafe impl Send for AsyncPickerCallbackResult {}
1594
1595#[cfg(feature = "macos_14_0")]
1596fn complete_async_picker(
1597    result_code: i32,
1598    ptr: *const c_void,
1599    ownership: AsyncPickerOwnership,
1600    user_data: *mut c_void,
1601) {
1602    crate::utils::panic_safe::catch_user_panic("async_picker_callback", move || {
1603        let result = AsyncPickerCallbackResult {
1604            code: result_code,
1605            ptr,
1606            ownership,
1607        };
1608        // SAFETY: `user_data` is the one-shot completion context from `AsyncCompletion::create()`.
1609        unsafe { AsyncCompletion::complete_ok(user_data, result) };
1610    });
1611}
1612
1613#[cfg(feature = "macos_14_0")]
1614extern "C" fn async_picker_result_callback(
1615    result_code: i32,
1616    ptr: *const c_void,
1617    user_data: *mut c_void,
1618) {
1619    complete_async_picker(result_code, ptr, AsyncPickerOwnership::Result, user_data);
1620}
1621
1622#[cfg(feature = "macos_14_0")]
1623extern "C" fn async_picker_filter_callback(
1624    result_code: i32,
1625    ptr: *const c_void,
1626    user_data: *mut c_void,
1627) {
1628    complete_async_picker(result_code, ptr, AsyncPickerOwnership::Filter, user_data);
1629}
1630
1631/// Future for async picker with full result
1632#[cfg(feature = "macos_14_0")]
1633pub struct AsyncPickerFuture {
1634    inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
1635}
1636
1637#[cfg(feature = "macos_14_0")]
1638impl std::fmt::Debug for AsyncPickerFuture {
1639    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1640        f.debug_struct("AsyncPickerFuture").finish_non_exhaustive()
1641    }
1642}
1643
1644#[cfg(feature = "macos_14_0")]
1645impl Future for AsyncPickerFuture {
1646    type Output = crate::content_sharing_picker::SCPickerOutcome;
1647
1648    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1649        use crate::content_sharing_picker::{SCPickerOutcome, SCPickerResult};
1650
1651        match Pin::new(&mut self.inner).poll(cx) {
1652            Poll::Pending => Poll::Pending,
1653            Poll::Ready(Ok(mut result)) => {
1654                let outcome = match result.code {
1655                    1 if !result.ptr.is_null() => {
1656                        SCPickerOutcome::Picked(SCPickerResult::from_ptr(result.take_ptr()))
1657                    }
1658                    0 => SCPickerOutcome::Cancelled,
1659                    _ => SCPickerOutcome::Error("Picker failed".to_string()),
1660                };
1661                Poll::Ready(outcome)
1662            }
1663            Poll::Ready(Err(e)) => Poll::Ready(SCPickerOutcome::Error(e)),
1664        }
1665    }
1666}
1667
1668/// Future for async picker returning filter only
1669#[cfg(feature = "macos_14_0")]
1670pub struct AsyncPickerFilterFuture {
1671    inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
1672}
1673
1674#[cfg(feature = "macos_14_0")]
1675impl std::fmt::Debug for AsyncPickerFilterFuture {
1676    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1677        f.debug_struct("AsyncPickerFilterFuture")
1678            .finish_non_exhaustive()
1679    }
1680}
1681
1682#[cfg(feature = "macos_14_0")]
1683impl Future for AsyncPickerFilterFuture {
1684    type Output = crate::content_sharing_picker::SCPickerFilterOutcome;
1685
1686    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1687        use crate::content_sharing_picker::SCPickerFilterOutcome;
1688
1689        match Pin::new(&mut self.inner).poll(cx) {
1690            Poll::Pending => Poll::Pending,
1691            Poll::Ready(Ok(mut result)) => {
1692                let outcome = match result.code {
1693                    1 if !result.ptr.is_null() => SCPickerFilterOutcome::Filter(
1694                        SCContentFilter::from_picker_ptr(result.take_ptr()),
1695                    ),
1696                    0 => SCPickerFilterOutcome::Cancelled,
1697                    _ => SCPickerFilterOutcome::Error("Picker failed".to_string()),
1698                };
1699                Poll::Ready(outcome)
1700            }
1701            Poll::Ready(Err(e)) => Poll::Ready(SCPickerFilterOutcome::Error(e)),
1702        }
1703    }
1704}
1705
1706/// Async wrapper for `SCContentSharingPicker` (macOS 14.0+)
1707///
1708/// Provides async methods to show the system content sharing picker UI.
1709/// **Executor-agnostic** - works with any async runtime.
1710///
1711/// Picker futures intentionally have no built-in deadline because completion
1712/// depends on a human choice. Callers that impose their own timeout may safely
1713/// drop the future; call
1714/// [`SCContentSharingPicker::deactivate`](crate::content_sharing_picker::SCContentSharingPicker::deactivate)
1715/// as well when the picker UI should be dismissed.
1716///
1717/// # Examples
1718///
1719/// ```no_run
1720/// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1721/// use screencapturekit::content_sharing_picker::*;
1722///
1723/// async fn pick_content() {
1724///     let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
1725///     match AsyncSCContentSharingPicker::show(&config).await {
1726///         SCPickerOutcome::Picked(result) => {
1727///             let (width, height) = result.pixel_size();
1728///             let filter = result.filter();
1729///             println!("Selected content: {}x{}", width, height);
1730///         }
1731///         SCPickerOutcome::Cancelled => println!("User cancelled"),
1732///         SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
1733///     }
1734/// }
1735/// ```
1736#[cfg(feature = "macos_14_0")]
1737#[derive(Debug, Clone, Copy)]
1738pub struct AsyncSCContentSharingPicker;
1739
1740#[cfg(feature = "macos_14_0")]
1741impl AsyncSCContentSharingPicker {
1742    /// Show the picker UI asynchronously and return `SCPickerResult` with filter and metadata
1743    ///
1744    /// This is the main API - use when you need content dimensions or want to build custom filters.
1745    /// The picker UI will be shown on the main thread, and the future will resolve when the user
1746    /// makes a selection or cancels.
1747    ///
1748    /// # Example
1749    /// ```no_run
1750    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1751    /// use screencapturekit::content_sharing_picker::*;
1752    ///
1753    /// async fn example() {
1754    ///     let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
1755    ///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
1756    ///         let (width, height) = result.pixel_size();
1757    ///         let filter = result.filter();
1758    ///     }
1759    /// }
1760    /// ```
1761    pub fn show(
1762        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1763    ) -> AsyncPickerFuture {
1764        let (future, context) = AsyncCompletion::create_unbounded();
1765
1766        // 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()`.
1767        unsafe {
1768            crate::ffi::sc_content_sharing_picker_show_with_result(
1769                config.as_ptr(),
1770                async_picker_result_callback,
1771                context,
1772            );
1773        }
1774
1775        AsyncPickerFuture { inner: future }
1776    }
1777
1778    /// Show the picker UI asynchronously and return an `SCContentFilter` directly
1779    ///
1780    /// This is the simple API - use when you just need the filter without metadata.
1781    ///
1782    /// # Example
1783    /// ```no_run
1784    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1785    /// use screencapturekit::content_sharing_picker::*;
1786    ///
1787    /// async fn example() {
1788    ///     let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
1789    ///     if let SCPickerFilterOutcome::Filter(filter) = AsyncSCContentSharingPicker::show_filter(&config).await {
1790    ///         // Use filter with SCStream
1791    ///     }
1792    /// }
1793    /// ```
1794    pub fn show_filter(
1795        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1796    ) -> AsyncPickerFilterFuture {
1797        let (future, context) = AsyncCompletion::create_unbounded();
1798
1799        // 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()`.
1800        unsafe {
1801            crate::ffi::sc_content_sharing_picker_show(
1802                config.as_ptr(),
1803                async_picker_filter_callback,
1804                context,
1805            );
1806        }
1807
1808        AsyncPickerFilterFuture { inner: future }
1809    }
1810
1811    /// Show the picker UI for an existing stream to change source while capturing
1812    ///
1813    /// Use this when you have an active `SCStream` and want to let the user
1814    /// select a new content source. The result can be used with `stream.update_content_filter()`.
1815    ///
1816    /// # Example
1817    /// ```no_run
1818    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1819    /// use screencapturekit::content_sharing_picker::*;
1820    /// use screencapturekit::stream::SCStream;
1821    /// use screencapturekit::stream::configuration::SCStreamConfiguration;
1822    /// use screencapturekit::stream::content_filter::SCContentFilter;
1823    /// use screencapturekit::shareable_content::SCShareableContent;
1824    ///
1825    /// async fn example() -> Option<()> {
1826    ///     let content = SCShareableContent::get().ok()?;
1827    ///     let displays = content.displays();
1828    ///     let display = displays.first()?;
1829    ///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build().ok()?;
1830    ///     let stream_config = SCStreamConfiguration::new();
1831    ///     let stream = SCStream::new(&filter, &stream_config).ok()?;
1832    ///
1833    ///     // When stream is active and user wants to change source
1834    ///     let config = SCContentSharingPickerConfiguration::new().ok()?;
1835    ///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show_for_stream(&config, &stream).await {
1836    ///         // Use result.filter() with stream.update_content_filter()
1837    ///         let _ = result.filter();
1838    ///     }
1839    ///     Some(())
1840    /// }
1841    /// ```
1842    pub fn show_for_stream(
1843        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1844        stream: &crate::stream::SCStream,
1845    ) -> AsyncPickerFuture {
1846        let (future, context) = AsyncCompletion::create_unbounded();
1847
1848        // 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()`.
1849        unsafe {
1850            crate::ffi::sc_content_sharing_picker_show_for_stream(
1851                config.as_ptr(),
1852                stream.as_ptr(),
1853                async_picker_result_callback,
1854                context,
1855            );
1856        }
1857
1858        AsyncPickerFuture { inner: future }
1859    }
1860}
1861
1862// ============================================================================
1863// AsyncSCRecordingOutput - Async recording with event stream (macOS 15.0+)
1864// ============================================================================
1865
1866/// Recording lifecycle event
1867#[cfg(feature = "macos_15_0")]
1868#[derive(Debug, Clone, PartialEq, Eq)]
1869pub enum RecordingEvent {
1870    /// Recording started successfully
1871    Started,
1872    /// Recording finished successfully
1873    Finished,
1874    /// Recording failed with an error
1875    Failed(String),
1876}
1877
1878#[cfg(feature = "macos_15_0")]
1879struct AsyncRecordingState {
1880    events: std::collections::VecDeque<RecordingEvent>,
1881    /// Every task parked on this event queue — see
1882    /// [`AsyncSampleIteratorState::waiters`] for why this is not a single
1883    /// `Option<Waker>`.
1884    waiters: Vec<RegisteredWaker>,
1885    finished: bool,
1886}
1887
1888#[cfg(feature = "macos_15_0")]
1889struct AsyncRecordingDelegate {
1890    state: Arc<Mutex<AsyncRecordingState>>,
1891}
1892
1893/// Push `event` onto the recording queue, mark the queue finished when the
1894/// event is terminal, and wake parked consumers **after** releasing the lock.
1895#[cfg(feature = "macos_15_0")]
1896fn push_recording_event(state: &Arc<Mutex<AsyncRecordingState>>, event: Option<RecordingEvent>) {
1897    let Ok(mut guard) = state.lock() else {
1898        return;
1899    };
1900    match event {
1901        Some(event) => {
1902            guard.finished |= matches!(event, RecordingEvent::Finished | RecordingEvent::Failed(_));
1903            guard.events.push_back(event);
1904        }
1905        None => guard.finished = true,
1906    }
1907    let waiters = std::mem::take(&mut guard.waiters);
1908    drop(guard);
1909    wake_all(waiters);
1910}
1911
1912#[cfg(feature = "macos_15_0")]
1913impl crate::recording_output::SCRecordingOutputDelegate for AsyncRecordingDelegate {
1914    fn recording_did_start(&self) {
1915        push_recording_event(&self.state, Some(RecordingEvent::Started));
1916    }
1917
1918    fn recording_did_fail(&self, error: String) {
1919        push_recording_event(&self.state, Some(RecordingEvent::Failed(error)));
1920    }
1921
1922    fn recording_did_finish(&self) {
1923        push_recording_event(&self.state, Some(RecordingEvent::Finished));
1924    }
1925}
1926
1927#[cfg(feature = "macos_15_0")]
1928impl Drop for AsyncRecordingDelegate {
1929    /// Close the event queue when the `SCRecordingOutput` (and with it this
1930    /// delegate) goes away without a terminal event — otherwise a caller
1931    /// awaiting [`AsyncSCRecordingOutput::next`] would park forever.
1932    fn drop(&mut self) {
1933        push_recording_event(&self.state, None);
1934    }
1935}
1936
1937/// Future for getting the next recording event
1938#[cfg(feature = "macos_15_0")]
1939pub struct NextRecordingEvent<'a> {
1940    state: &'a Arc<Mutex<AsyncRecordingState>>,
1941    waiter: Arc<()>,
1942}
1943
1944#[cfg(feature = "macos_15_0")]
1945impl std::fmt::Debug for NextRecordingEvent<'_> {
1946    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1947        f.debug_struct("NextRecordingEvent").finish_non_exhaustive()
1948    }
1949}
1950
1951#[cfg(feature = "macos_15_0")]
1952impl Future for NextRecordingEvent<'_> {
1953    type Output = Option<RecordingEvent>;
1954
1955    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1956        poll_next_recording_event(self.state, &self.waiter, cx)
1957    }
1958}
1959
1960#[cfg(feature = "macos_15_0")]
1961impl Drop for NextRecordingEvent<'_> {
1962    fn drop(&mut self) {
1963        let removed = self
1964            .state
1965            .lock()
1966            .ok()
1967            .and_then(|mut state| unregister_waker(&mut state.waiters, &self.waiter));
1968        drop(removed);
1969    }
1970}
1971
1972/// Shared poll logic for the recording-event future/stream.
1973#[cfg(feature = "macos_15_0")]
1974fn poll_next_recording_event(
1975    state: &Arc<Mutex<AsyncRecordingState>>,
1976    waiter: &Arc<()>,
1977    cx: &Context<'_>,
1978) -> Poll<Option<RecordingEvent>> {
1979    let waker = cx.waker().clone();
1980    let Ok(mut state) = state.lock() else {
1981        return Poll::Ready(None);
1982    };
1983
1984    if let Some(event) = state.events.pop_front() {
1985        let removed = unregister_waker(&mut state.waiters, waiter);
1986        drop(state);
1987        drop(removed);
1988        drop(waker);
1989        return Poll::Ready(Some(event));
1990    }
1991
1992    if state.finished {
1993        let removed = unregister_waker(&mut state.waiters, waiter);
1994        drop(state);
1995        drop(removed);
1996        drop(waker);
1997        Poll::Ready(None)
1998    } else {
1999        let replaced = register_waker(&mut state.waiters, waiter, waker);
2000        drop(state);
2001        drop(replaced);
2002        Poll::Pending
2003    }
2004}
2005
2006/// A [`Stream`](futures_core::Stream) of recording lifecycle [`RecordingEvent`]s.
2007///
2008/// Yields `Started` / `Finished` / `Failed(_)` and ends (`None`) once the
2009/// recording finishes or fails. Returned by [`AsyncSCRecordingOutput::events`];
2010/// integrates with the `futures::StreamExt` combinators.
2011#[cfg(feature = "macos_15_0")]
2012pub struct RecordingEventStream<'a> {
2013    state: &'a Arc<Mutex<AsyncRecordingState>>,
2014    waiter: Arc<()>,
2015}
2016
2017#[cfg(feature = "macos_15_0")]
2018impl std::fmt::Debug for RecordingEventStream<'_> {
2019    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2020        f.debug_struct("RecordingEventStream")
2021            .finish_non_exhaustive()
2022    }
2023}
2024
2025#[cfg(feature = "macos_15_0")]
2026impl futures_core::Stream for RecordingEventStream<'_> {
2027    type Item = RecordingEvent;
2028
2029    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
2030        poll_next_recording_event(self.state, &self.waiter, cx)
2031    }
2032}
2033
2034#[cfg(feature = "macos_15_0")]
2035impl Drop for RecordingEventStream<'_> {
2036    fn drop(&mut self) {
2037        let removed = self
2038            .state
2039            .lock()
2040            .ok()
2041            .and_then(|mut state| unregister_waker(&mut state.waiters, &self.waiter));
2042        drop(removed);
2043    }
2044}
2045
2046/// Async wrapper for `SCRecordingOutput` with event stream (macOS 15.0+)
2047///
2048/// Provides async iteration over recording lifecycle events.
2049/// **Executor-agnostic** - works with any async runtime.
2050///
2051/// # Examples
2052///
2053/// ```no_run
2054/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCRecordingOutput, RecordingEvent};
2055/// use screencapturekit::recording_output::SCRecordingOutputConfiguration;
2056/// use screencapturekit::stream::{SCStream, configuration::SCStreamConfiguration, content_filter::SCContentFilter};
2057/// use std::path::Path;
2058///
2059/// async fn record_screen() -> Option<()> {
2060///     let content = AsyncSCShareableContent::get().await.ok()?;
2061///     let displays = content.displays();
2062///     let display = displays.first()?;
2063///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build().ok()?;
2064///     let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
2065///
2066///     let rec_config = SCRecordingOutputConfiguration::new().ok()?
2067///         .with_output_url(Path::new("/tmp/recording.mp4")).ok()?;
2068///
2069///     let (recording, events) = AsyncSCRecordingOutput::new(&rec_config)?;
2070///
2071///     let mut stream = SCStream::new(&filter, &config).ok()?;
2072///     stream.add_recording_output(&recording).ok()?;
2073///     stream.start_capture().ok()?;
2074///
2075///     // Wait for recording events
2076///     while let Some(event) = events.next().await {
2077///         match event {
2078///             RecordingEvent::Started => println!("Recording started!"),
2079///             RecordingEvent::Finished => {
2080///                 println!("Recording finished!");
2081///                 break;
2082///             }
2083///             RecordingEvent::Failed(e) => {
2084///                 eprintln!("Recording failed: {}", e);
2085///                 break;
2086///             }
2087///         }
2088///     }
2089///
2090///     Some(())
2091/// }
2092/// ```
2093#[cfg(feature = "macos_15_0")]
2094pub struct AsyncSCRecordingOutput {
2095    state: Arc<Mutex<AsyncRecordingState>>,
2096}
2097
2098#[cfg(feature = "macos_15_0")]
2099impl std::fmt::Debug for AsyncSCRecordingOutput {
2100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2101        f.debug_struct("AsyncSCRecordingOutput")
2102            .finish_non_exhaustive()
2103    }
2104}
2105
2106#[cfg(feature = "macos_15_0")]
2107impl AsyncSCRecordingOutput {
2108    /// Create a new async recording output
2109    ///
2110    /// Returns a tuple of (`SCRecordingOutput`, `AsyncSCRecordingOutput`).
2111    /// The `SCRecordingOutput` should be added to an `SCStream`, while
2112    /// the `AsyncSCRecordingOutput` provides async event iteration.
2113    ///
2114    /// # Errors
2115    ///
2116    /// Returns `None` if the recording output cannot be created (requires macOS 15.0+).
2117    #[must_use]
2118    pub fn new(
2119        config: &crate::recording_output::SCRecordingOutputConfiguration,
2120    ) -> Option<(crate::recording_output::SCRecordingOutput, Self)> {
2121        let state = Arc::new(Mutex::new(AsyncRecordingState {
2122            events: std::collections::VecDeque::new(),
2123            waiters: Vec::new(),
2124            finished: false,
2125        }));
2126
2127        let delegate = AsyncRecordingDelegate {
2128            state: Arc::clone(&state),
2129        };
2130
2131        let recording =
2132            crate::recording_output::SCRecordingOutput::new_with_delegate(config, delegate)?;
2133
2134        Some((recording, Self { state }))
2135    }
2136
2137    /// Get the next recording event asynchronously
2138    ///
2139    /// Returns `None` when the recording has finished or failed.
2140    pub fn next(&self) -> NextRecordingEvent<'_> {
2141        NextRecordingEvent {
2142            state: &self.state,
2143            waiter: Arc::new(()),
2144        }
2145    }
2146
2147    /// Borrow the recording events as a [`Stream`](futures_core::Stream) of
2148    /// [`RecordingEvent`]s.
2149    ///
2150    /// Unlocks the `futures::StreamExt` combinators for the recording event
2151    /// flow (`for_each`, `take_while`, …):
2152    ///
2153    /// ```no_run
2154    /// # #[cfg(feature = "macos_15_0")]
2155    /// # async fn example(recording: screencapturekit::async_api::AsyncSCRecordingOutput) {
2156    /// use futures_util::StreamExt;
2157    ///
2158    /// recording
2159    ///     .events()
2160    ///     .for_each(|event| {
2161    ///         println!("recording event: {event:?}");
2162    ///         std::future::ready(())
2163    ///     })
2164    ///     .await;
2165    /// # }
2166    /// ```
2167    #[must_use]
2168    pub fn events(&self) -> RecordingEventStream<'_> {
2169        RecordingEventStream {
2170            state: &self.state,
2171            waiter: Arc::new(()),
2172        }
2173    }
2174
2175    /// Check if the recording has finished
2176    #[must_use]
2177    pub fn is_finished(&self) -> bool {
2178        self.state.lock().map_or(true, |s| s.finished)
2179    }
2180
2181    /// Get any pending events without waiting
2182    #[must_use]
2183    pub fn try_next(&self) -> Option<RecordingEvent> {
2184        self.state.lock().ok()?.events.pop_front()
2185    }
2186}
2187
2188#[cfg(test)]
2189mod tests {
2190    use super::*;
2191
2192    #[test]
2193    fn dropped_sample_future_unregisters_its_waker() {
2194        let state = Arc::new(Mutex::new(AsyncSampleIteratorState {
2195            buffer: std::collections::VecDeque::new(),
2196            waiters: Vec::new(),
2197            closed: false,
2198            stopped: false,
2199            capacity: 1,
2200            senders: 1,
2201            stop_error: None,
2202        }));
2203        let mut future = Box::pin(NextSample {
2204            state: &state,
2205            waiter: Arc::new(()),
2206        });
2207        let waker = Waker::noop();
2208        let mut context = Context::from_waker(waker);
2209
2210        assert!(future.as_mut().poll(&mut context).is_pending());
2211        assert_eq!(state.lock().unwrap().waiters.len(), 1);
2212        drop(future);
2213        assert!(state.lock().unwrap().waiters.is_empty());
2214    }
2215
2216    fn idle_state() -> Arc<Mutex<AsyncSampleIteratorState>> {
2217        Arc::new(Mutex::new(AsyncSampleIteratorState {
2218            buffer: std::collections::VecDeque::new(),
2219            waiters: Vec::new(),
2220            closed: false,
2221            stopped: false,
2222            capacity: 1,
2223            senders: 1,
2224            stop_error: None,
2225        }))
2226    }
2227
2228    #[test]
2229    fn failed_start_closes_the_queue_but_a_retry_reopens_it() {
2230        let state = idle_state();
2231        close_sample_state(
2232            &state,
2233            Some(SCError::CaptureStartFailed("denied".to_string())),
2234            false,
2235        );
2236        assert!(state.lock().unwrap().closed);
2237
2238        reopen_sample_state(&state);
2239
2240        let (closed, stale_error) = {
2241            let state = state.lock().unwrap();
2242            (state.closed, state.stop_error.is_some())
2243        };
2244        assert!(!closed, "a retry must not see a permanently closed queue");
2245        assert!(!stale_error, "the stale error must not survive");
2246    }
2247
2248    #[test]
2249    fn reopen_refuses_once_stop_capture_has_succeeded() {
2250        let state = idle_state();
2251        close_sample_state(&state, None, true);
2252
2253        reopen_sample_state(&state);
2254
2255        let closed = state.lock().unwrap().closed;
2256        assert!(closed, "a stopped SCStream cannot be restarted");
2257    }
2258}