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//! # Runtime Agnostic Design
7//!
8//! This async API uses only `std` types and works with **any** async runtime:
9//! - Uses callback-based Swift FFI for true async operations
10//! - Uses `std::sync::{Arc, Mutex}` for synchronization
11//! - Uses `std::task::{Poll, Waker}` for async primitives
12//! - Uses `std::future::Future` trait
13//!
14//! # Examples
15//!
16//! ```rust,no_run
17//! # #[tokio::main]
18//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! use screencapturekit::async_api::AsyncSCShareableContent;
20//!
21//! let content = AsyncSCShareableContent::get().await?;
22//! println!("Found {} displays", content.displays().len());
23//! # Ok(())
24//! # }
25//! ```
26
27use crate::error::SCError;
28use crate::shareable_content::SCShareableContent;
29use crate::stream::configuration::SCStreamConfiguration;
30use crate::stream::content_filter::SCContentFilter;
31use crate::utils::sync_completion::{error_from_cstr, AsyncCompletion, AsyncCompletionFuture};
32use std::ffi::c_void;
33use std::future::Future;
34use std::pin::Pin;
35use std::sync::{Arc, Mutex};
36use std::task::{Context, Poll, Waker};
37
38// ============================================================================
39// AsyncSCShareableContent - True async with callback-based FFI
40// ============================================================================
41
42/// Callback from Swift FFI for shareable content
43extern "C" fn shareable_content_callback(
44    content: *const c_void,
45    error: *const i8,
46    user_data: *mut c_void,
47) {
48    if !error.is_null() {
49        let error_msg = unsafe { error_from_cstr(error) };
50        unsafe { AsyncCompletion::<SCShareableContent>::complete_err(user_data, error_msg) };
51    } else if !content.is_null() {
52        let sc = unsafe { SCShareableContent::from_ptr(content) };
53        unsafe { AsyncCompletion::complete_ok(user_data, sc) };
54    } else {
55        unsafe {
56            AsyncCompletion::<SCShareableContent>::complete_err(
57                user_data,
58                "Unknown error".to_string(),
59            );
60        };
61    }
62}
63
64/// Future for async shareable content retrieval
65pub struct AsyncShareableContentFuture {
66    inner: AsyncCompletionFuture<SCShareableContent>,
67}
68
69impl Future for AsyncShareableContentFuture {
70    type Output = Result<SCShareableContent, SCError>;
71
72    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
73        Pin::new(&mut self.inner)
74            .poll(cx)
75            .map(|r| r.map_err(SCError::NoShareableContent))
76    }
77}
78
79/// Async wrapper for `SCShareableContent`
80///
81/// Provides async methods to retrieve displays, windows, and applications
82/// without blocking. **Executor-agnostic** - works with any async runtime.
83pub struct AsyncSCShareableContent;
84
85impl AsyncSCShareableContent {
86    /// Asynchronously get the shareable content (displays, windows, applications)
87    ///
88    /// Uses callback-based Swift FFI for true async operation.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if:
93    /// - Screen recording permission is not granted
94    /// - The system fails to retrieve shareable content
95    pub fn get() -> AsyncShareableContentFuture {
96        Self::with_options().get_async()
97    }
98
99    /// Create options builder for customizing shareable content retrieval
100    #[must_use]
101    pub fn with_options() -> AsyncSCShareableContentOptions {
102        AsyncSCShareableContentOptions::default()
103    }
104}
105
106/// Options for async shareable content retrieval
107#[derive(Default, Debug, Clone, PartialEq, Eq)]
108pub struct AsyncSCShareableContentOptions {
109    exclude_desktop_windows: bool,
110    on_screen_windows_only: bool,
111}
112
113impl AsyncSCShareableContentOptions {
114    /// Exclude desktop windows from the shareable content
115    #[must_use]
116    pub fn exclude_desktop_windows(mut self, exclude: bool) -> Self {
117        self.exclude_desktop_windows = exclude;
118        self
119    }
120
121    /// Include only on-screen windows in the shareable content
122    #[must_use]
123    pub fn on_screen_windows_only(mut self, on_screen_only: bool) -> Self {
124        self.on_screen_windows_only = on_screen_only;
125        self
126    }
127
128    /// Asynchronously get the shareable content with these options
129    pub fn get_async(self) -> AsyncShareableContentFuture {
130        let (future, context) = AsyncCompletion::create();
131
132        unsafe {
133            crate::ffi::sc_shareable_content_get_with_options(
134                self.exclude_desktop_windows,
135                self.on_screen_windows_only,
136                shareable_content_callback,
137                context,
138            );
139        }
140
141        AsyncShareableContentFuture { inner: future }
142    }
143}
144
145// ============================================================================
146// AsyncSCStream - Async stream with integrated frame iteration
147// ============================================================================
148
149/// Async iterator over sample buffers
150struct AsyncSampleIteratorState {
151    buffer: std::collections::VecDeque<crate::cm::CMSampleBuffer>,
152    waker: Option<Waker>,
153    closed: bool,
154    capacity: usize,
155}
156
157/// Internal sender for async sample iterator
158struct AsyncSampleSender {
159    inner: Arc<Mutex<AsyncSampleIteratorState>>,
160}
161
162impl crate::stream::output_trait::SCStreamOutputTrait for AsyncSampleSender {
163    fn did_output_sample_buffer(
164        &self,
165        sample_buffer: crate::cm::CMSampleBuffer,
166        _of_type: crate::stream::output_type::SCStreamOutputType,
167    ) {
168        let Ok(mut state) = self.inner.lock() else {
169            return;
170        };
171
172        // Drop oldest if at capacity
173        if state.buffer.len() >= state.capacity {
174            state.buffer.pop_front();
175        }
176
177        state.buffer.push_back(sample_buffer);
178
179        if let Some(waker) = state.waker.take() {
180            waker.wake();
181        }
182    }
183}
184
185impl Drop for AsyncSampleSender {
186    fn drop(&mut self) {
187        if let Ok(mut state) = self.inner.lock() {
188            state.closed = true;
189            if let Some(waker) = state.waker.take() {
190                waker.wake();
191            }
192        }
193    }
194}
195
196/// Future for getting the next sample buffer
197pub struct NextSample<'a> {
198    state: &'a Arc<Mutex<AsyncSampleIteratorState>>,
199}
200
201impl Future for NextSample<'_> {
202    type Output = Option<crate::cm::CMSampleBuffer>;
203
204    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
205        let Ok(mut state) = self.state.lock() else {
206            return Poll::Ready(None);
207        };
208
209        if let Some(sample) = state.buffer.pop_front() {
210            return Poll::Ready(Some(sample));
211        }
212
213        if state.closed {
214            Poll::Ready(None)
215        } else {
216            state.waker = Some(cx.waker().clone());
217            Poll::Pending
218        }
219    }
220}
221
222unsafe impl Send for AsyncSampleSender {}
223unsafe impl Sync for AsyncSampleSender {}
224
225/// Async wrapper for `SCStream` with integrated frame iteration
226///
227/// Provides async methods for stream lifecycle and frame iteration.
228/// **Executor-agnostic** - works with any async runtime.
229///
230/// # Examples
231///
232/// ```rust,no_run
233/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
234/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
235/// use screencapturekit::stream::configuration::SCStreamConfiguration;
236/// use screencapturekit::stream::content_filter::SCContentFilter;
237/// use screencapturekit::stream::output_type::SCStreamOutputType;
238///
239/// let content = AsyncSCShareableContent::get().await?;
240/// let display = &content.displays()[0];
241/// let filter = SCContentFilter::builder().display(display).exclude_windows(&[]).build();
242/// let mut config = SCStreamConfiguration::default();
243/// config.set_width(1920);
244/// config.set_height(1080);
245///
246/// let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
247/// stream.start_capture()?;
248///
249/// // Process frames asynchronously
250/// while let Some(frame) = stream.next().await {
251///     println!("Got frame!");
252/// }
253/// # Ok(())
254/// # }
255/// ```
256pub struct AsyncSCStream {
257    stream: crate::stream::SCStream,
258    iterator_state: Arc<Mutex<AsyncSampleIteratorState>>,
259}
260
261impl AsyncSCStream {
262    /// Create a new async stream
263    ///
264    /// # Arguments
265    ///
266    /// * `filter` - Content filter specifying what to capture
267    /// * `config` - Stream configuration
268    /// * `buffer_capacity` - Max frames to buffer (oldest dropped when full)
269    /// * `output_type` - Type of output (Screen, Audio, Microphone)
270    #[must_use]
271    pub fn new(
272        filter: &SCContentFilter,
273        config: &SCStreamConfiguration,
274        buffer_capacity: usize,
275        output_type: crate::stream::output_type::SCStreamOutputType,
276    ) -> Self {
277        let state = Arc::new(Mutex::new(AsyncSampleIteratorState {
278            buffer: std::collections::VecDeque::with_capacity(buffer_capacity),
279            waker: None,
280            closed: false,
281            capacity: buffer_capacity,
282        }));
283
284        let sender = AsyncSampleSender {
285            inner: Arc::clone(&state),
286        };
287
288        let mut stream = crate::stream::SCStream::new(filter, config);
289        stream.add_output_handler(sender, output_type);
290
291        Self {
292            stream,
293            iterator_state: state,
294        }
295    }
296
297    /// Get the next sample buffer asynchronously
298    ///
299    /// Returns `None` when the stream is closed.
300    pub fn next(&self) -> NextSample<'_> {
301        NextSample {
302            state: &self.iterator_state,
303        }
304    }
305
306    /// Try to get a sample without waiting
307    #[must_use]
308    pub fn try_next(&self) -> Option<crate::cm::CMSampleBuffer> {
309        self.iterator_state.lock().ok()?.buffer.pop_front()
310    }
311
312    /// Check if the stream has been closed
313    #[must_use]
314    pub fn is_closed(&self) -> bool {
315        self.iterator_state.lock().map(|s| s.closed).unwrap_or(true)
316    }
317
318    /// Get the number of buffered samples
319    #[must_use]
320    pub fn buffered_count(&self) -> usize {
321        self.iterator_state
322            .lock()
323            .map(|s| s.buffer.len())
324            .unwrap_or(0)
325    }
326
327    /// Clear all buffered samples
328    pub fn clear_buffer(&self) {
329        if let Ok(mut state) = self.iterator_state.lock() {
330            state.buffer.clear();
331        }
332    }
333
334    /// Start capture (synchronous - returns immediately)
335    ///
336    /// # Errors
337    ///
338    /// Returns an error if capture fails to start.
339    pub fn start_capture(&self) -> Result<(), SCError> {
340        self.stream.start_capture()
341    }
342
343    /// Stop capture (synchronous - returns immediately)
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if capture fails to stop.
348    pub fn stop_capture(&self) -> Result<(), SCError> {
349        self.stream.stop_capture()
350    }
351
352    /// Update stream configuration
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if the update fails.
357    pub fn update_configuration(&self, config: &SCStreamConfiguration) -> Result<(), SCError> {
358        self.stream.update_configuration(config)
359    }
360
361    /// Update content filter
362    ///
363    /// # Errors
364    ///
365    /// Returns an error if the update fails.
366    pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
367        self.stream.update_content_filter(filter)
368    }
369
370    /// Get a reference to the underlying stream
371    #[must_use]
372    pub fn inner(&self) -> &crate::stream::SCStream {
373        &self.stream
374    }
375}
376
377// ============================================================================
378// AsyncSCScreenshotManager - Async screenshot capture (macOS 14.0+)
379// ============================================================================
380
381/// Async wrapper for `SCScreenshotManager`
382///
383/// Provides async methods for single-frame screenshot capture.
384/// **Executor-agnostic** - works with any async runtime.
385///
386/// Requires the `macos_14_0` feature flag.
387///
388/// # Examples
389///
390/// ```rust,no_run
391/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
392/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCScreenshotManager};
393/// use screencapturekit::stream::configuration::SCStreamConfiguration;
394/// use screencapturekit::stream::content_filter::SCContentFilter;
395///
396/// let content = AsyncSCShareableContent::get().await?;
397/// let display = &content.displays()[0];
398/// let filter = SCContentFilter::builder().display(display).exclude_windows(&[]).build();
399/// let mut config = SCStreamConfiguration::default();
400/// config.set_width(1920);
401/// config.set_height(1080);
402///
403/// let image = AsyncSCScreenshotManager::capture_image(&filter, &config).await?;
404/// println!("Screenshot: {}x{}", image.width(), image.height());
405/// # Ok(())
406/// # }
407/// ```
408#[cfg(feature = "macos_14_0")]
409pub struct AsyncSCScreenshotManager;
410
411/// Callback for async `CGImage` capture
412#[cfg(feature = "macos_14_0")]
413extern "C" fn screenshot_image_callback(
414    image_ptr: *const c_void,
415    error_ptr: *const i8,
416    user_data: *mut c_void,
417) {
418    if !error_ptr.is_null() {
419        let error = unsafe { error_from_cstr(error_ptr) };
420        unsafe {
421            AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(user_data, error);
422        }
423    } else if !image_ptr.is_null() {
424        let image = crate::screenshot_manager::CGImage::from_ptr(image_ptr);
425        unsafe { AsyncCompletion::complete_ok(user_data, image) };
426    } else {
427        unsafe {
428            AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(
429                user_data,
430                "Unknown error".to_string(),
431            );
432        };
433    }
434}
435
436/// Callback for async `CMSampleBuffer` capture
437#[cfg(feature = "macos_14_0")]
438extern "C" fn screenshot_buffer_callback(
439    buffer_ptr: *const c_void,
440    error_ptr: *const i8,
441    user_data: *mut c_void,
442) {
443    if !error_ptr.is_null() {
444        let error = unsafe { error_from_cstr(error_ptr) };
445        unsafe { AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
446    } else if !buffer_ptr.is_null() {
447        let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
448        unsafe { AsyncCompletion::complete_ok(user_data, buffer) };
449    } else {
450        unsafe {
451            AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
452                user_data,
453                "Unknown error".to_string(),
454            );
455        };
456    }
457}
458
459/// Future for async screenshot capture
460#[cfg(feature = "macos_14_0")]
461pub struct AsyncScreenshotFuture<T> {
462    inner: AsyncCompletionFuture<T>,
463}
464
465#[cfg(feature = "macos_14_0")]
466impl<T> Future for AsyncScreenshotFuture<T> {
467    type Output = Result<T, SCError>;
468
469    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
470        Pin::new(&mut self.inner)
471            .poll(cx)
472            .map(|r| r.map_err(SCError::ScreenshotError))
473    }
474}
475
476#[cfg(feature = "macos_14_0")]
477impl AsyncSCScreenshotManager {
478    /// Capture a single screenshot as a `CGImage` asynchronously
479    ///
480    /// # Errors
481    /// Returns an error if:
482    /// - Screen recording permission is not granted
483    /// - The capture fails for any reason
484    pub fn capture_image(
485        content_filter: &crate::stream::content_filter::SCContentFilter,
486        configuration: &SCStreamConfiguration,
487    ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
488        let (future, context) = AsyncCompletion::create();
489
490        unsafe {
491            crate::ffi::sc_screenshot_manager_capture_image(
492                content_filter.as_ptr(),
493                configuration.as_ptr(),
494                screenshot_image_callback,
495                context,
496            );
497        }
498
499        AsyncScreenshotFuture { inner: future }
500    }
501
502    /// Capture a single screenshot as a `CMSampleBuffer` asynchronously
503    ///
504    /// # Errors
505    /// Returns an error if:
506    /// - Screen recording permission is not granted
507    /// - The capture fails for any reason
508    pub fn capture_sample_buffer(
509        content_filter: &crate::stream::content_filter::SCContentFilter,
510        configuration: &SCStreamConfiguration,
511    ) -> AsyncScreenshotFuture<crate::cm::CMSampleBuffer> {
512        let (future, context) = AsyncCompletion::create();
513
514        unsafe {
515            crate::ffi::sc_screenshot_manager_capture_sample_buffer(
516                content_filter.as_ptr(),
517                configuration.as_ptr(),
518                screenshot_buffer_callback,
519                context,
520            );
521        }
522
523        AsyncScreenshotFuture { inner: future }
524    }
525}