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