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}