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()?;
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()?;
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::utils::completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
74use std::ffi::c_void;
75use std::future::Future;
76use std::pin::Pin;
77use std::sync::{Arc, Mutex};
78use std::task::{Context, Poll, Waker};
79
80// ============================================================================
81// AsyncSCShareableContent - True async with callback-based FFI
82// ============================================================================
83
84/// Callback from Swift FFI for shareable content
85extern "C" fn shareable_content_callback(
86    content: *const c_void,
87    error: *const i8,
88    user_data: *mut c_void,
89) {
90    if !error.is_null() {
91        let error_msg = unsafe { error_from_cstr(error) };
92        unsafe { AsyncCompletion::<SCShareableContent>::complete_err(user_data, error_msg) };
93    } else if !content.is_null() {
94        let sc = unsafe { SCShareableContent::from_ptr(content) };
95        unsafe { AsyncCompletion::complete_ok(user_data, sc) };
96    } else {
97        unsafe {
98            AsyncCompletion::<SCShareableContent>::complete_err(
99                user_data,
100                "Unknown error".to_string(),
101            );
102        };
103    }
104}
105
106/// Future for async shareable content retrieval
107pub struct AsyncShareableContentFuture {
108    inner: AsyncCompletionFuture<SCShareableContent>,
109}
110
111impl std::fmt::Debug for AsyncShareableContentFuture {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.debug_struct("AsyncShareableContentFuture")
114            .finish_non_exhaustive()
115    }
116}
117
118impl Future for AsyncShareableContentFuture {
119    type Output = Result<SCShareableContent, SCError>;
120
121    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
122        Pin::new(&mut self.inner)
123            .poll(cx)
124            .map(|r| r.map_err(SCError::NoShareableContent))
125    }
126}
127
128/// Async wrapper for `SCShareableContent`
129///
130/// Provides async methods to retrieve displays, windows, and applications
131/// without blocking. **Executor-agnostic** - works with any async runtime.
132#[derive(Debug, Clone, Copy)]
133pub struct AsyncSCShareableContent;
134
135impl AsyncSCShareableContent {
136    /// Asynchronously get the shareable content (displays, windows, applications)
137    ///
138    /// Uses callback-based Swift FFI for true async operation.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if:
143    /// - Screen recording permission is not granted
144    /// - The system fails to retrieve shareable content
145    pub fn get() -> AsyncShareableContentFuture {
146        Self::create().get()
147    }
148
149    /// Create options builder for customizing shareable content retrieval
150    #[must_use]
151    pub fn create() -> AsyncSCShareableContentOptions {
152        AsyncSCShareableContentOptions::default()
153    }
154}
155
156/// Options for async shareable content retrieval
157#[derive(Default, Debug, Clone, PartialEq, Eq)]
158pub struct AsyncSCShareableContentOptions {
159    exclude_desktop_windows: bool,
160    on_screen_windows_only: bool,
161}
162
163impl AsyncSCShareableContentOptions {
164    /// Exclude desktop windows from the shareable content
165    #[must_use]
166    pub fn with_exclude_desktop_windows(mut self, exclude: bool) -> Self {
167        self.exclude_desktop_windows = exclude;
168        self
169    }
170
171    /// Include only on-screen windows in the shareable content
172    #[must_use]
173    pub fn with_on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
174        self.on_screen_windows_only = on_screen_only;
175        self
176    }
177
178    /// Asynchronously get the shareable content with these options
179    pub fn get(self) -> AsyncShareableContentFuture {
180        let (future, context) = AsyncCompletion::create();
181
182        unsafe {
183            crate::ffi::sc_shareable_content_get_with_options(
184                self.exclude_desktop_windows,
185                self.on_screen_windows_only,
186                shareable_content_callback,
187                context,
188            );
189        }
190
191        AsyncShareableContentFuture { inner: future }
192    }
193
194    /// Asynchronously get shareable content with only windows below a reference window
195    ///
196    /// This returns windows that are stacked below the specified reference window
197    /// in the window layering order.
198    ///
199    /// # Arguments
200    ///
201    /// * `reference_window` - The window to use as the reference point
202    pub fn below_window(
203        self,
204        reference_window: &crate::shareable_content::SCWindow,
205    ) -> AsyncShareableContentFuture {
206        let (future, context) = AsyncCompletion::create();
207
208        unsafe {
209            crate::ffi::sc_shareable_content_get_below_window(
210                self.exclude_desktop_windows,
211                reference_window.as_ptr(),
212                shareable_content_callback,
213                context,
214            );
215        }
216
217        AsyncShareableContentFuture { inner: future }
218    }
219
220    /// Asynchronously get shareable content with only windows above a reference window
221    ///
222    /// This returns windows that are stacked above the specified reference window
223    /// in the window layering order.
224    ///
225    /// # Arguments
226    ///
227    /// * `reference_window` - The window to use as the reference point
228    pub fn above_window(
229        self,
230        reference_window: &crate::shareable_content::SCWindow,
231    ) -> AsyncShareableContentFuture {
232        let (future, context) = AsyncCompletion::create();
233
234        unsafe {
235            crate::ffi::sc_shareable_content_get_above_window(
236                self.exclude_desktop_windows,
237                reference_window.as_ptr(),
238                shareable_content_callback,
239                context,
240            );
241        }
242
243        AsyncShareableContentFuture { inner: future }
244    }
245}
246
247impl AsyncSCShareableContent {
248    /// Asynchronously get shareable content for the current process only (macOS 14.4+)
249    ///
250    /// This retrieves content that the current process can capture without
251    /// requiring user authorization via TCC (Transparency, Consent, and Control).
252    ///
253    /// # Examples
254    ///
255    /// ```rust,no_run
256    /// # #[tokio::main]
257    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
258    /// use screencapturekit::async_api::AsyncSCShareableContent;
259    ///
260    /// // Get content capturable without TCC authorization
261    /// let content = AsyncSCShareableContent::current_process().await?;
262    /// println!("Found {} windows for current process", content.windows().len());
263    /// # Ok(())
264    /// # }
265    /// ```
266    #[cfg(feature = "macos_14_4")]
267    pub fn current_process() -> AsyncShareableContentFuture {
268        let (future, context) = AsyncCompletion::create();
269
270        unsafe {
271            crate::ffi::sc_shareable_content_get_current_process_displays(
272                shareable_content_callback,
273                context,
274            );
275        }
276
277        AsyncShareableContentFuture { inner: future }
278    }
279}
280
281// ============================================================================
282// AsyncSCStream - Async stream with integrated frame iteration
283// ============================================================================
284
285/// Async iterator over sample buffers
286struct AsyncSampleIteratorState {
287    buffer: std::collections::VecDeque<crate::cm::CMSampleBuffer>,
288    waker: Option<Waker>,
289    closed: bool,
290    capacity: usize,
291}
292
293/// Internal sender for async sample iterator
294struct AsyncSampleSender {
295    inner: Arc<Mutex<AsyncSampleIteratorState>>,
296}
297
298impl crate::stream::output_trait::SCStreamOutputTrait for AsyncSampleSender {
299    fn did_output_sample_buffer(
300        &self,
301        sample_buffer: crate::cm::CMSampleBuffer,
302        _of_type: crate::stream::output_type::SCStreamOutputType,
303    ) {
304        let Ok(mut state) = self.inner.lock() else {
305            return;
306        };
307
308        // Drop oldest if at capacity
309        if state.buffer.len() >= state.capacity {
310            state.buffer.pop_front();
311        }
312
313        state.buffer.push_back(sample_buffer);
314
315        if let Some(waker) = state.waker.take() {
316            waker.wake();
317        }
318    }
319}
320
321impl Drop for AsyncSampleSender {
322    fn drop(&mut self) {
323        if let Ok(mut state) = self.inner.lock() {
324            state.closed = true;
325            if let Some(waker) = state.waker.take() {
326                waker.wake();
327            }
328        }
329    }
330}
331
332/// Future for getting the next sample buffer
333pub struct NextSample<'a> {
334    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
335}
336
337impl std::fmt::Debug for NextSample<'_> {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        f.debug_struct("NextSample").finish_non_exhaustive()
340    }
341}
342
343impl Future for NextSample<'_> {
344    type Output = Option<crate::cm::CMSampleBuffer>;
345
346    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
347        let Ok(mut state) = self.state.lock() else {
348            return Poll::Ready(None);
349        };
350
351        if let Some(sample) = state.buffer.pop_front() {
352            return Poll::Ready(Some(sample));
353        }
354
355        if state.closed {
356            Poll::Ready(None)
357        } else {
358            // Avoid the lost-wakeup race: when the same future is polled by
359            // a different task (e.g. moved between tokio::select! arms),
360            // the waker changes. Using `will_wake` avoids needlessly cloning
361            // when the executor reuses the same waker, and the explicit
362            // assignment guarantees the latest waker is the one a future
363            // sample arrival will wake.
364            let waker = cx.waker();
365            match state.waker {
366                Some(ref existing) if existing.will_wake(waker) => {}
367                _ => state.waker = Some(waker.clone()),
368            }
369            Poll::Pending
370        }
371    }
372}
373
374unsafe impl Send for AsyncSampleSender {}
375unsafe impl Sync for AsyncSampleSender {}
376
377/// Async wrapper for `SCStream` with integrated frame iteration
378///
379/// Provides async methods for stream lifecycle and frame iteration.
380/// **Executor-agnostic** - works with any async runtime.
381///
382/// # Examples
383///
384/// ```rust,no_run
385/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
386/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
387/// use screencapturekit::stream::configuration::SCStreamConfiguration;
388/// use screencapturekit::stream::content_filter::SCContentFilter;
389/// use screencapturekit::stream::output_type::SCStreamOutputType;
390///
391/// let content = AsyncSCShareableContent::get().await?;
392/// let display = &content.displays()[0];
393/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
394/// let config = SCStreamConfiguration::new()
395///     .with_width(1920)
396///     .with_height(1080);
397///
398/// let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
399/// stream.start_capture()?;
400///
401/// // Process frames asynchronously
402/// while let Some(frame) = stream.next().await {
403///     println!("Got frame!");
404/// }
405/// # Ok(())
406/// # }
407/// ```
408/// Async wrapper for `SCStream` with integrated frame iteration.
409///
410/// # Back-pressure and frame loss
411///
412/// `AsyncSCStream` buffers samples in a **bounded** internal queue sized
413/// by the `buffer_capacity` argument to [`AsyncSCStream::new`]. When the
414/// queue is full and a new sample arrives from `ScreenCaptureKit`, the
415/// **oldest** queued sample is dropped to make room — the stream is
416/// **lossy by design**.
417///
418/// This is the right policy for real-time UI rendering, screen-share
419/// previews, and live encoding: a slow consumer would rather see the
420/// most recent frame than a stale one. It is the *wrong* policy for
421/// lossless capture (e.g. saving every frame to disk for later
422/// editing) — for that, use the synchronous [`SCStream`](crate::stream::SCStream)
423/// directly, where back-pressure is naturally enforced by Apple's
424/// `queueDepth` setting and your handler's runtime.
425///
426/// To detect when frames are being dropped, watch `buffered_count()`
427/// against `buffer_capacity` over time, or instrument your handler
428/// with a per-frame timestamp delta and compare to your expected
429/// frame interval.
430pub struct AsyncSCStream {
431    stream: crate::stream::SCStream,
432    iterator_state: Arc<Mutex<AsyncSampleIteratorState>>,
433}
434
435impl AsyncSCStream {
436    /// Create a new async stream
437    ///
438    /// # Arguments
439    ///
440    /// * `filter` - Content filter specifying what to capture
441    /// * `config` - Stream configuration
442    /// * `buffer_capacity` - Max frames to buffer (oldest dropped when full)
443    /// * `output_type` - Type of output (Screen, Audio, Microphone)
444    #[must_use]
445    pub fn new(
446        filter: &SCContentFilter,
447        config: &SCStreamConfiguration,
448        buffer_capacity: usize,
449        output_type: crate::stream::output_type::SCStreamOutputType,
450    ) -> Self {
451        let state = Arc::new(Mutex::new(AsyncSampleIteratorState {
452            buffer: std::collections::VecDeque::with_capacity(buffer_capacity),
453            waker: None,
454            closed: false,
455            capacity: buffer_capacity,
456        }));
457
458        let sender = AsyncSampleSender {
459            inner: Arc::clone(&state),
460        };
461
462        let mut stream = crate::stream::SCStream::new(filter, config);
463        stream.add_output_handler(sender, output_type);
464
465        Self {
466            stream,
467            iterator_state: state,
468        }
469    }
470
471    /// Get the next sample buffer asynchronously
472    ///
473    /// Returns `None` when the stream is closed.
474    pub fn next(&self) -> NextSample<'_> {
475        NextSample {
476            state: &self.iterator_state,
477        }
478    }
479
480    /// Try to get a sample without waiting
481    #[must_use]
482    pub fn try_next(&self) -> Option<crate::cm::CMSampleBuffer> {
483        self.iterator_state.lock().ok()?.buffer.pop_front()
484    }
485
486    /// Check if the stream has been closed
487    #[must_use]
488    pub fn is_closed(&self) -> bool {
489        self.iterator_state.lock().map_or(true, |s| s.closed)
490    }
491
492    /// Get the number of buffered samples
493    #[must_use]
494    pub fn buffered_count(&self) -> usize {
495        self.iterator_state.lock().map_or(0, |s| s.buffer.len())
496    }
497
498    /// Clear all buffered samples
499    pub fn clear_buffer(&self) {
500        if let Ok(mut state) = self.iterator_state.lock() {
501            state.buffer.clear();
502        }
503    }
504
505    /// Start capture (synchronous - returns immediately)
506    ///
507    /// # Errors
508    ///
509    /// Returns an error if capture fails to start.
510    pub fn start_capture(&self) -> Result<(), SCError> {
511        self.stream.start_capture()
512    }
513
514    /// Stop capture (synchronous - returns immediately)
515    ///
516    /// # Errors
517    ///
518    /// Returns an error if capture fails to stop.
519    pub fn stop_capture(&self) -> Result<(), SCError> {
520        self.stream.stop_capture()
521    }
522
523    /// Update stream configuration
524    ///
525    /// # Errors
526    ///
527    /// Returns an error if the update fails.
528    pub fn update_configuration(&self, config: &SCStreamConfiguration) -> Result<(), SCError> {
529        self.stream.update_configuration(config)
530    }
531
532    /// Update content filter
533    ///
534    /// # Errors
535    ///
536    /// Returns an error if the update fails.
537    pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
538        self.stream.update_content_filter(filter)
539    }
540
541    /// Get a reference to the underlying stream
542    #[must_use]
543    pub fn inner(&self) -> &crate::stream::SCStream {
544        &self.stream
545    }
546}
547
548impl std::fmt::Debug for AsyncSCStream {
549    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
550        f.debug_struct("AsyncSCStream")
551            .field("stream", &self.stream)
552            .field("buffered_count", &self.buffered_count())
553            .field("is_closed", &self.is_closed())
554            .finish_non_exhaustive()
555    }
556}
557
558// ============================================================================
559// AsyncSCScreenshotManager - Async screenshot capture (macOS 14.0+)
560// ============================================================================
561
562/// Async wrapper for `SCScreenshotManager`
563///
564/// Provides async methods for single-frame screenshot capture.
565/// **Executor-agnostic** - works with any async runtime.
566///
567/// Requires the `macos_14_0` feature flag.
568///
569/// # Examples
570///
571/// ```rust,no_run
572/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
573/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCScreenshotManager};
574/// use screencapturekit::stream::configuration::SCStreamConfiguration;
575/// use screencapturekit::stream::content_filter::SCContentFilter;
576///
577/// let content = AsyncSCShareableContent::get().await?;
578/// let display = &content.displays()[0];
579/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
580/// let config = SCStreamConfiguration::new()
581///     .with_width(1920)
582///     .with_height(1080);
583///
584/// let image = AsyncSCScreenshotManager::capture_image(&filter, &config).await?;
585/// println!("Screenshot: {}x{}", image.width(), image.height());
586/// # Ok(())
587/// # }
588/// ```
589#[cfg(feature = "macos_14_0")]
590#[derive(Debug, Clone, Copy)]
591pub struct AsyncSCScreenshotManager;
592
593/// Callback for async `CGImage` capture
594#[cfg(feature = "macos_14_0")]
595extern "C" fn screenshot_image_callback(
596    image_ptr: *const c_void,
597    error_ptr: *const i8,
598    user_data: *mut c_void,
599) {
600    if !error_ptr.is_null() {
601        let error = unsafe { error_from_cstr(error_ptr) };
602        unsafe {
603            AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(user_data, error);
604        }
605    } else if !image_ptr.is_null() {
606        let image = crate::screenshot_manager::CGImage::from_ptr(image_ptr);
607        unsafe { AsyncCompletion::complete_ok(user_data, image) };
608    } else {
609        unsafe {
610            AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(
611                user_data,
612                "Unknown error".to_string(),
613            );
614        };
615    }
616}
617
618/// Callback for async `CMSampleBuffer` capture
619#[cfg(feature = "macos_14_0")]
620extern "C" fn screenshot_buffer_callback(
621    buffer_ptr: *const c_void,
622    error_ptr: *const i8,
623    user_data: *mut c_void,
624) {
625    if !error_ptr.is_null() {
626        let error = unsafe { error_from_cstr(error_ptr) };
627        unsafe { AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
628    } else if !buffer_ptr.is_null() {
629        let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
630        unsafe { AsyncCompletion::complete_ok(user_data, buffer) };
631    } else {
632        unsafe {
633            AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
634                user_data,
635                "Unknown error".to_string(),
636            );
637        };
638    }
639}
640
641/// Future for async screenshot capture
642#[cfg(feature = "macos_14_0")]
643pub struct AsyncScreenshotFuture<T> {
644    inner: AsyncCompletionFuture<T>,
645}
646
647#[cfg(feature = "macos_14_0")]
648impl<T> std::fmt::Debug for AsyncScreenshotFuture<T> {
649    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
650        f.debug_struct("AsyncScreenshotFuture")
651            .finish_non_exhaustive()
652    }
653}
654
655#[cfg(feature = "macos_14_0")]
656impl<T> Future for AsyncScreenshotFuture<T> {
657    type Output = Result<T, SCError>;
658
659    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
660        Pin::new(&mut self.inner)
661            .poll(cx)
662            .map(|r| r.map_err(SCError::ScreenshotError))
663    }
664}
665
666#[cfg(feature = "macos_14_0")]
667impl AsyncSCScreenshotManager {
668    /// Capture a single screenshot as a `CGImage` asynchronously
669    ///
670    /// # Errors
671    /// Returns an error if:
672    /// - Screen recording permission is not granted
673    /// - The capture fails for any reason
674    pub fn capture_image(
675        content_filter: &crate::stream::content_filter::SCContentFilter,
676        configuration: &SCStreamConfiguration,
677    ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
678        let (future, context) = AsyncCompletion::create();
679
680        unsafe {
681            crate::ffi::sc_screenshot_manager_capture_image(
682                content_filter.as_ptr(),
683                configuration.as_ptr(),
684                screenshot_image_callback,
685                context,
686            );
687        }
688
689        AsyncScreenshotFuture { inner: future }
690    }
691
692    /// Capture a single screenshot as a `CMSampleBuffer` asynchronously
693    ///
694    /// # Errors
695    /// Returns an error if:
696    /// - Screen recording permission is not granted
697    /// - The capture fails for any reason
698    pub fn capture_sample_buffer(
699        content_filter: &crate::stream::content_filter::SCContentFilter,
700        configuration: &SCStreamConfiguration,
701    ) -> AsyncScreenshotFuture<crate::cm::CMSampleBuffer> {
702        let (future, context) = AsyncCompletion::create();
703
704        unsafe {
705            crate::ffi::sc_screenshot_manager_capture_sample_buffer(
706                content_filter.as_ptr(),
707                configuration.as_ptr(),
708                screenshot_buffer_callback,
709                context,
710            );
711        }
712
713        AsyncScreenshotFuture { inner: future }
714    }
715
716    /// Capture a screenshot of a specific screen region asynchronously (macOS 15.2+)
717    ///
718    /// This method captures the content within the specified rectangle,
719    /// which can span multiple displays.
720    ///
721    /// # Arguments
722    /// * `rect` - The rectangle to capture, in screen coordinates (points)
723    ///
724    /// # Errors
725    /// Returns an error if:
726    /// - The system is not macOS 15.2+
727    /// - Screen recording permission is not granted
728    /// - The capture fails for any reason
729    #[cfg(feature = "macos_15_2")]
730    pub fn capture_image_in_rect(
731        rect: crate::cg::CGRect,
732    ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
733        let (future, context) = AsyncCompletion::create();
734
735        unsafe {
736            crate::ffi::sc_screenshot_manager_capture_image_in_rect(
737                rect.x,
738                rect.y,
739                rect.width,
740                rect.height,
741                screenshot_image_callback,
742                context,
743            );
744        }
745
746        AsyncScreenshotFuture { inner: future }
747    }
748
749    /// Capture a screenshot with advanced configuration asynchronously (macOS 26.0+)
750    ///
751    /// This method uses the new `SCScreenshotConfiguration` for more control
752    /// over the screenshot output, including HDR support and file saving.
753    ///
754    /// # Arguments
755    /// * `content_filter` - The content filter specifying what to capture
756    /// * `configuration` - The screenshot configuration
757    ///
758    /// # Errors
759    /// Returns an error if the capture fails
760    #[cfg(feature = "macos_26_0")]
761    pub fn capture_screenshot(
762        content_filter: &crate::stream::content_filter::SCContentFilter,
763        configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
764    ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
765        let (future, context) = AsyncCompletion::create();
766
767        unsafe {
768            crate::ffi::sc_screenshot_manager_capture_screenshot(
769                content_filter.as_ptr(),
770                configuration.as_ptr(),
771                screenshot_output_callback,
772                context,
773            );
774        }
775
776        AsyncScreenshotFuture { inner: future }
777    }
778
779    /// Capture a screenshot of a specific region with advanced configuration asynchronously (macOS 26.0+)
780    ///
781    /// # Arguments
782    /// * `rect` - The rectangle to capture, in screen coordinates (points)
783    /// * `configuration` - The screenshot configuration
784    ///
785    /// # Errors
786    /// Returns an error if the capture fails
787    #[cfg(feature = "macos_26_0")]
788    pub fn capture_screenshot_in_rect(
789        rect: crate::cg::CGRect,
790        configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
791    ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
792        let (future, context) = AsyncCompletion::create();
793
794        unsafe {
795            crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
796                rect.x,
797                rect.y,
798                rect.width,
799                rect.height,
800                configuration.as_ptr(),
801                screenshot_output_callback,
802                context,
803            );
804        }
805
806        AsyncScreenshotFuture { inner: future }
807    }
808}
809
810/// Callback for async `SCScreenshotOutput` capture (macOS 26.0+)
811#[cfg(feature = "macos_26_0")]
812extern "C" fn screenshot_output_callback(
813    output_ptr: *const c_void,
814    error_ptr: *const i8,
815    user_data: *mut c_void,
816) {
817    if !error_ptr.is_null() {
818        let error = unsafe { error_from_cstr(error_ptr) };
819        unsafe {
820            AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
821                user_data, error,
822            );
823        }
824    } else if !output_ptr.is_null() {
825        let output = crate::screenshot_manager::SCScreenshotOutput::from_ptr(output_ptr);
826        unsafe { AsyncCompletion::complete_ok(user_data, output) };
827    } else {
828        unsafe {
829            AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
830                user_data,
831                "Unknown error".to_string(),
832            );
833        };
834    }
835}
836
837// ============================================================================
838// AsyncSCContentSharingPicker - Async content sharing picker (macOS 14.0+)
839// ============================================================================
840
841/// Result from the async picker callback
842#[cfg(feature = "macos_14_0")]
843struct AsyncPickerCallbackResult {
844    code: i32,
845    ptr: *const c_void,
846}
847
848#[cfg(feature = "macos_14_0")]
849unsafe impl Send for AsyncPickerCallbackResult {}
850
851/// Callback for async picker
852#[cfg(feature = "macos_14_0")]
853extern "C" fn async_picker_callback(result_code: i32, ptr: *const c_void, user_data: *mut c_void) {
854    let result = AsyncPickerCallbackResult {
855        code: result_code,
856        ptr,
857    };
858    unsafe { AsyncCompletion::complete_ok(user_data, result) };
859}
860
861/// Future for async picker with full result
862#[cfg(feature = "macos_14_0")]
863pub struct AsyncPickerFuture {
864    inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
865}
866
867#[cfg(feature = "macos_14_0")]
868impl std::fmt::Debug for AsyncPickerFuture {
869    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
870        f.debug_struct("AsyncPickerFuture").finish_non_exhaustive()
871    }
872}
873
874#[cfg(feature = "macos_14_0")]
875impl Future for AsyncPickerFuture {
876    type Output = crate::content_sharing_picker::SCPickerOutcome;
877
878    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
879        use crate::content_sharing_picker::{SCPickerOutcome, SCPickerResult};
880
881        match Pin::new(&mut self.inner).poll(cx) {
882            Poll::Pending => Poll::Pending,
883            Poll::Ready(Ok(result)) => {
884                let outcome = match result.code {
885                    1 if !result.ptr.is_null() => {
886                        SCPickerOutcome::Picked(SCPickerResult::from_ptr(result.ptr))
887                    }
888                    0 => SCPickerOutcome::Cancelled,
889                    _ => SCPickerOutcome::Error("Picker failed".to_string()),
890                };
891                Poll::Ready(outcome)
892            }
893            Poll::Ready(Err(e)) => Poll::Ready(SCPickerOutcome::Error(e)),
894        }
895    }
896}
897
898/// Future for async picker returning filter only
899#[cfg(feature = "macos_14_0")]
900pub struct AsyncPickerFilterFuture {
901    inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
902}
903
904#[cfg(feature = "macos_14_0")]
905impl std::fmt::Debug for AsyncPickerFilterFuture {
906    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
907        f.debug_struct("AsyncPickerFilterFuture")
908            .finish_non_exhaustive()
909    }
910}
911
912#[cfg(feature = "macos_14_0")]
913impl Future for AsyncPickerFilterFuture {
914    type Output = crate::content_sharing_picker::SCPickerFilterOutcome;
915
916    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
917        use crate::content_sharing_picker::SCPickerFilterOutcome;
918
919        match Pin::new(&mut self.inner).poll(cx) {
920            Poll::Pending => Poll::Pending,
921            Poll::Ready(Ok(result)) => {
922                let outcome = match result.code {
923                    1 if !result.ptr.is_null() => {
924                        SCPickerFilterOutcome::Filter(SCContentFilter::from_picker_ptr(result.ptr))
925                    }
926                    0 => SCPickerFilterOutcome::Cancelled,
927                    _ => SCPickerFilterOutcome::Error("Picker failed".to_string()),
928                };
929                Poll::Ready(outcome)
930            }
931            Poll::Ready(Err(e)) => Poll::Ready(SCPickerFilterOutcome::Error(e)),
932        }
933    }
934}
935
936/// Async wrapper for `SCContentSharingPicker` (macOS 14.0+)
937///
938/// Provides async methods to show the system content sharing picker UI.
939/// **Executor-agnostic** - works with any async runtime.
940///
941/// # Examples
942///
943/// ```no_run
944/// use screencapturekit::async_api::AsyncSCContentSharingPicker;
945/// use screencapturekit::content_sharing_picker::*;
946///
947/// async fn pick_content() {
948///     let config = SCContentSharingPickerConfiguration::new();
949///     match AsyncSCContentSharingPicker::show(&config).await {
950///         SCPickerOutcome::Picked(result) => {
951///             let (width, height) = result.pixel_size();
952///             let filter = result.filter();
953///             println!("Selected content: {}x{}", width, height);
954///         }
955///         SCPickerOutcome::Cancelled => println!("User cancelled"),
956///         SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
957///     }
958/// }
959/// ```
960#[cfg(feature = "macos_14_0")]
961#[derive(Debug, Clone, Copy)]
962pub struct AsyncSCContentSharingPicker;
963
964#[cfg(feature = "macos_14_0")]
965impl AsyncSCContentSharingPicker {
966    /// Show the picker UI asynchronously and return `SCPickerResult` with filter and metadata
967    ///
968    /// This is the main API - use when you need content dimensions or want to build custom filters.
969    /// The picker UI will be shown on the main thread, and the future will resolve when the user
970    /// makes a selection or cancels.
971    ///
972    /// # Example
973    /// ```no_run
974    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
975    /// use screencapturekit::content_sharing_picker::*;
976    ///
977    /// async fn example() {
978    ///     let config = SCContentSharingPickerConfiguration::new();
979    ///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
980    ///         let (width, height) = result.pixel_size();
981    ///         let filter = result.filter();
982    ///     }
983    /// }
984    /// ```
985    pub fn show(
986        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
987    ) -> AsyncPickerFuture {
988        let (future, context) = AsyncCompletion::create();
989
990        unsafe {
991            crate::ffi::sc_content_sharing_picker_show_with_result(
992                config.as_ptr(),
993                async_picker_callback,
994                context,
995            );
996        }
997
998        AsyncPickerFuture { inner: future }
999    }
1000
1001    /// Show the picker UI asynchronously and return an `SCContentFilter` directly
1002    ///
1003    /// This is the simple API - use when you just need the filter without metadata.
1004    ///
1005    /// # Example
1006    /// ```no_run
1007    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1008    /// use screencapturekit::content_sharing_picker::*;
1009    ///
1010    /// async fn example() {
1011    ///     let config = SCContentSharingPickerConfiguration::new();
1012    ///     if let SCPickerFilterOutcome::Filter(filter) = AsyncSCContentSharingPicker::show_filter(&config).await {
1013    ///         // Use filter with SCStream
1014    ///     }
1015    /// }
1016    /// ```
1017    pub fn show_filter(
1018        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1019    ) -> AsyncPickerFilterFuture {
1020        let (future, context) = AsyncCompletion::create();
1021
1022        unsafe {
1023            crate::ffi::sc_content_sharing_picker_show(
1024                config.as_ptr(),
1025                async_picker_callback,
1026                context,
1027            );
1028        }
1029
1030        AsyncPickerFilterFuture { inner: future }
1031    }
1032
1033    /// Show the picker UI for an existing stream to change source while capturing
1034    ///
1035    /// Use this when you have an active `SCStream` and want to let the user
1036    /// select a new content source. The result can be used with `stream.update_content_filter()`.
1037    ///
1038    /// # Example
1039    /// ```no_run
1040    /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1041    /// use screencapturekit::content_sharing_picker::*;
1042    /// use screencapturekit::stream::SCStream;
1043    /// use screencapturekit::stream::configuration::SCStreamConfiguration;
1044    /// use screencapturekit::stream::content_filter::SCContentFilter;
1045    /// use screencapturekit::shareable_content::SCShareableContent;
1046    ///
1047    /// async fn example() -> Option<()> {
1048    ///     let content = SCShareableContent::get().ok()?;
1049    ///     let displays = content.displays();
1050    ///     let display = displays.first()?;
1051    ///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
1052    ///     let stream_config = SCStreamConfiguration::new();
1053    ///     let stream = SCStream::new(&filter, &stream_config);
1054    ///
1055    ///     // When stream is active and user wants to change source
1056    ///     let config = SCContentSharingPickerConfiguration::new();
1057    ///     if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show_for_stream(&config, &stream).await {
1058    ///         // Use result.filter() with stream.update_content_filter()
1059    ///         let _ = result.filter();
1060    ///     }
1061    ///     Some(())
1062    /// }
1063    /// ```
1064    pub fn show_for_stream(
1065        config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1066        stream: &crate::stream::SCStream,
1067    ) -> AsyncPickerFuture {
1068        let (future, context) = AsyncCompletion::create();
1069
1070        unsafe {
1071            crate::ffi::sc_content_sharing_picker_show_for_stream(
1072                config.as_ptr(),
1073                stream.as_ptr(),
1074                async_picker_callback,
1075                context,
1076            );
1077        }
1078
1079        AsyncPickerFuture { inner: future }
1080    }
1081}
1082
1083// ============================================================================
1084// AsyncSCRecordingOutput - Async recording with event stream (macOS 15.0+)
1085// ============================================================================
1086
1087/// Recording lifecycle event
1088#[cfg(feature = "macos_15_0")]
1089#[derive(Debug, Clone, PartialEq, Eq)]
1090pub enum RecordingEvent {
1091    /// Recording started successfully
1092    Started,
1093    /// Recording finished successfully
1094    Finished,
1095    /// Recording failed with an error
1096    Failed(String),
1097}
1098
1099#[cfg(feature = "macos_15_0")]
1100struct AsyncRecordingState {
1101    events: std::collections::VecDeque<RecordingEvent>,
1102    waker: Option<Waker>,
1103    finished: bool,
1104}
1105
1106#[cfg(feature = "macos_15_0")]
1107struct AsyncRecordingDelegate {
1108    state: Arc<Mutex<AsyncRecordingState>>,
1109}
1110
1111#[cfg(feature = "macos_15_0")]
1112impl crate::recording_output::SCRecordingOutputDelegate for AsyncRecordingDelegate {
1113    fn recording_did_start(&self) {
1114        if let Ok(mut state) = self.state.lock() {
1115            state.events.push_back(RecordingEvent::Started);
1116            if let Some(waker) = state.waker.take() {
1117                waker.wake();
1118            }
1119        }
1120    }
1121
1122    fn recording_did_fail(&self, error: String) {
1123        if let Ok(mut state) = self.state.lock() {
1124            state.events.push_back(RecordingEvent::Failed(error));
1125            state.finished = true;
1126            if let Some(waker) = state.waker.take() {
1127                waker.wake();
1128            }
1129        }
1130    }
1131
1132    fn recording_did_finish(&self) {
1133        if let Ok(mut state) = self.state.lock() {
1134            state.events.push_back(RecordingEvent::Finished);
1135            state.finished = true;
1136            if let Some(waker) = state.waker.take() {
1137                waker.wake();
1138            }
1139        }
1140    }
1141}
1142
1143/// Future for getting the next recording event
1144#[cfg(feature = "macos_15_0")]
1145pub struct NextRecordingEvent<'a> {
1146    state: &'a Arc<Mutex<AsyncRecordingState>>,
1147}
1148
1149#[cfg(feature = "macos_15_0")]
1150impl std::fmt::Debug for NextRecordingEvent<'_> {
1151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1152        f.debug_struct("NextRecordingEvent").finish_non_exhaustive()
1153    }
1154}
1155
1156#[cfg(feature = "macos_15_0")]
1157impl Future for NextRecordingEvent<'_> {
1158    type Output = Option<RecordingEvent>;
1159
1160    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1161        let Ok(mut state) = self.state.lock() else {
1162            return Poll::Ready(None);
1163        };
1164
1165        if let Some(event) = state.events.pop_front() {
1166            return Poll::Ready(Some(event));
1167        }
1168
1169        if state.finished {
1170            Poll::Ready(None)
1171        } else {
1172            // Avoid the lost-wakeup race — see NextSample::poll above.
1173            let waker = cx.waker();
1174            match state.waker {
1175                Some(ref existing) if existing.will_wake(waker) => {}
1176                _ => state.waker = Some(waker.clone()),
1177            }
1178            Poll::Pending
1179        }
1180    }
1181}
1182
1183/// Async wrapper for `SCRecordingOutput` with event stream (macOS 15.0+)
1184///
1185/// Provides async iteration over recording lifecycle events.
1186/// **Executor-agnostic** - works with any async runtime.
1187///
1188/// # Examples
1189///
1190/// ```no_run
1191/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCRecordingOutput, RecordingEvent};
1192/// use screencapturekit::recording_output::SCRecordingOutputConfiguration;
1193/// use screencapturekit::stream::{SCStream, configuration::SCStreamConfiguration, content_filter::SCContentFilter};
1194/// use std::path::Path;
1195///
1196/// async fn record_screen() -> Option<()> {
1197///     let content = AsyncSCShareableContent::get().await.ok()?;
1198///     let displays = content.displays();
1199///     let display = displays.first()?;
1200///     let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
1201///     let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
1202///
1203///     let rec_config = SCRecordingOutputConfiguration::new()
1204///         .with_output_url(Path::new("/tmp/recording.mp4"));
1205///
1206///     let (recording, events) = AsyncSCRecordingOutput::new(&rec_config)?;
1207///
1208///     let mut stream = SCStream::new(&filter, &config);
1209///     stream.add_recording_output(&recording).ok()?;
1210///     stream.start_capture().ok()?;
1211///
1212///     // Wait for recording events
1213///     while let Some(event) = events.next().await {
1214///         match event {
1215///             RecordingEvent::Started => println!("Recording started!"),
1216///             RecordingEvent::Finished => {
1217///                 println!("Recording finished!");
1218///                 break;
1219///             }
1220///             RecordingEvent::Failed(e) => {
1221///                 eprintln!("Recording failed: {}", e);
1222///                 break;
1223///             }
1224///         }
1225///     }
1226///
1227///     Some(())
1228/// }
1229/// ```
1230#[cfg(feature = "macos_15_0")]
1231pub struct AsyncSCRecordingOutput {
1232    state: Arc<Mutex<AsyncRecordingState>>,
1233}
1234
1235#[cfg(feature = "macos_15_0")]
1236impl std::fmt::Debug for AsyncSCRecordingOutput {
1237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1238        f.debug_struct("AsyncSCRecordingOutput")
1239            .finish_non_exhaustive()
1240    }
1241}
1242
1243#[cfg(feature = "macos_15_0")]
1244impl AsyncSCRecordingOutput {
1245    /// Create a new async recording output
1246    ///
1247    /// Returns a tuple of (`SCRecordingOutput`, `AsyncSCRecordingOutput`).
1248    /// The `SCRecordingOutput` should be added to an `SCStream`, while
1249    /// the `AsyncSCRecordingOutput` provides async event iteration.
1250    ///
1251    /// # Errors
1252    ///
1253    /// Returns `None` if the recording output cannot be created (requires macOS 15.0+).
1254    #[must_use]
1255    pub fn new(
1256        config: &crate::recording_output::SCRecordingOutputConfiguration,
1257    ) -> Option<(crate::recording_output::SCRecordingOutput, Self)> {
1258        let state = Arc::new(Mutex::new(AsyncRecordingState {
1259            events: std::collections::VecDeque::new(),
1260            waker: None,
1261            finished: false,
1262        }));
1263
1264        let delegate = AsyncRecordingDelegate {
1265            state: Arc::clone(&state),
1266        };
1267
1268        let recording =
1269            crate::recording_output::SCRecordingOutput::new_with_delegate(config, delegate)?;
1270
1271        Some((recording, Self { state }))
1272    }
1273
1274    /// Get the next recording event asynchronously
1275    ///
1276    /// Returns `None` when the recording has finished or failed.
1277    pub fn next(&self) -> NextRecordingEvent<'_> {
1278        NextRecordingEvent { state: &self.state }
1279    }
1280
1281    /// Check if the recording has finished
1282    #[must_use]
1283    pub fn is_finished(&self) -> bool {
1284        self.state.lock().map_or(true, |s| s.finished)
1285    }
1286
1287    /// Get any pending events without waiting
1288    #[must_use]
1289    pub fn try_next(&self) -> Option<RecordingEvent> {
1290        self.state.lock().ok()?.events.pop_front()
1291    }
1292}