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 state.waker = Some(cx.waker().clone());
359 Poll::Pending
360 }
361 }
362}
363
364unsafe impl Send for AsyncSampleSender {}
365unsafe impl Sync for AsyncSampleSender {}
366
367/// Async wrapper for `SCStream` with integrated frame iteration
368///
369/// Provides async methods for stream lifecycle and frame iteration.
370/// **Executor-agnostic** - works with any async runtime.
371///
372/// # Examples
373///
374/// ```rust,no_run
375/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
376/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCStream};
377/// use screencapturekit::stream::configuration::SCStreamConfiguration;
378/// use screencapturekit::stream::content_filter::SCContentFilter;
379/// use screencapturekit::stream::output_type::SCStreamOutputType;
380///
381/// let content = AsyncSCShareableContent::get().await?;
382/// let display = &content.displays()[0];
383/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
384/// let config = SCStreamConfiguration::new()
385/// .with_width(1920)
386/// .with_height(1080);
387///
388/// let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
389/// stream.start_capture()?;
390///
391/// // Process frames asynchronously
392/// while let Some(frame) = stream.next().await {
393/// println!("Got frame!");
394/// }
395/// # Ok(())
396/// # }
397/// ```
398pub struct AsyncSCStream {
399 stream: crate::stream::SCStream,
400 iterator_state: Arc<Mutex<AsyncSampleIteratorState>>,
401}
402
403impl AsyncSCStream {
404 /// Create a new async stream
405 ///
406 /// # Arguments
407 ///
408 /// * `filter` - Content filter specifying what to capture
409 /// * `config` - Stream configuration
410 /// * `buffer_capacity` - Max frames to buffer (oldest dropped when full)
411 /// * `output_type` - Type of output (Screen, Audio, Microphone)
412 #[must_use]
413 pub fn new(
414 filter: &SCContentFilter,
415 config: &SCStreamConfiguration,
416 buffer_capacity: usize,
417 output_type: crate::stream::output_type::SCStreamOutputType,
418 ) -> Self {
419 let state = Arc::new(Mutex::new(AsyncSampleIteratorState {
420 buffer: std::collections::VecDeque::with_capacity(buffer_capacity),
421 waker: None,
422 closed: false,
423 capacity: buffer_capacity,
424 }));
425
426 let sender = AsyncSampleSender {
427 inner: Arc::clone(&state),
428 };
429
430 let mut stream = crate::stream::SCStream::new(filter, config);
431 stream.add_output_handler(sender, output_type);
432
433 Self {
434 stream,
435 iterator_state: state,
436 }
437 }
438
439 /// Get the next sample buffer asynchronously
440 ///
441 /// Returns `None` when the stream is closed.
442 pub fn next(&self) -> NextSample<'_> {
443 NextSample {
444 state: &self.iterator_state,
445 }
446 }
447
448 /// Try to get a sample without waiting
449 #[must_use]
450 pub fn try_next(&self) -> Option<crate::cm::CMSampleBuffer> {
451 self.iterator_state.lock().ok()?.buffer.pop_front()
452 }
453
454 /// Check if the stream has been closed
455 #[must_use]
456 pub fn is_closed(&self) -> bool {
457 self.iterator_state.lock().map(|s| s.closed).unwrap_or(true)
458 }
459
460 /// Get the number of buffered samples
461 #[must_use]
462 pub fn buffered_count(&self) -> usize {
463 self.iterator_state
464 .lock()
465 .map(|s| s.buffer.len())
466 .unwrap_or(0)
467 }
468
469 /// Clear all buffered samples
470 pub fn clear_buffer(&self) {
471 if let Ok(mut state) = self.iterator_state.lock() {
472 state.buffer.clear();
473 }
474 }
475
476 /// Start capture (synchronous - returns immediately)
477 ///
478 /// # Errors
479 ///
480 /// Returns an error if capture fails to start.
481 pub fn start_capture(&self) -> Result<(), SCError> {
482 self.stream.start_capture()
483 }
484
485 /// Stop capture (synchronous - returns immediately)
486 ///
487 /// # Errors
488 ///
489 /// Returns an error if capture fails to stop.
490 pub fn stop_capture(&self) -> Result<(), SCError> {
491 self.stream.stop_capture()
492 }
493
494 /// Update stream configuration
495 ///
496 /// # Errors
497 ///
498 /// Returns an error if the update fails.
499 pub fn update_configuration(&self, config: &SCStreamConfiguration) -> Result<(), SCError> {
500 self.stream.update_configuration(config)
501 }
502
503 /// Update content filter
504 ///
505 /// # Errors
506 ///
507 /// Returns an error if the update fails.
508 pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
509 self.stream.update_content_filter(filter)
510 }
511
512 /// Get a reference to the underlying stream
513 #[must_use]
514 pub fn inner(&self) -> &crate::stream::SCStream {
515 &self.stream
516 }
517}
518
519impl std::fmt::Debug for AsyncSCStream {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 f.debug_struct("AsyncSCStream")
522 .field("stream", &self.stream)
523 .field("buffered_count", &self.buffered_count())
524 .field("is_closed", &self.is_closed())
525 .finish_non_exhaustive()
526 }
527}
528
529// ============================================================================
530// AsyncSCScreenshotManager - Async screenshot capture (macOS 14.0+)
531// ============================================================================
532
533/// Async wrapper for `SCScreenshotManager`
534///
535/// Provides async methods for single-frame screenshot capture.
536/// **Executor-agnostic** - works with any async runtime.
537///
538/// Requires the `macos_14_0` feature flag.
539///
540/// # Examples
541///
542/// ```rust,no_run
543/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
544/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCScreenshotManager};
545/// use screencapturekit::stream::configuration::SCStreamConfiguration;
546/// use screencapturekit::stream::content_filter::SCContentFilter;
547///
548/// let content = AsyncSCShareableContent::get().await?;
549/// let display = &content.displays()[0];
550/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
551/// let config = SCStreamConfiguration::new()
552/// .with_width(1920)
553/// .with_height(1080);
554///
555/// let image = AsyncSCScreenshotManager::capture_image(&filter, &config).await?;
556/// println!("Screenshot: {}x{}", image.width(), image.height());
557/// # Ok(())
558/// # }
559/// ```
560#[cfg(feature = "macos_14_0")]
561#[derive(Debug, Clone, Copy)]
562pub struct AsyncSCScreenshotManager;
563
564/// Callback for async `CGImage` capture
565#[cfg(feature = "macos_14_0")]
566extern "C" fn screenshot_image_callback(
567 image_ptr: *const c_void,
568 error_ptr: *const i8,
569 user_data: *mut c_void,
570) {
571 if !error_ptr.is_null() {
572 let error = unsafe { error_from_cstr(error_ptr) };
573 unsafe {
574 AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(user_data, error);
575 }
576 } else if !image_ptr.is_null() {
577 let image = crate::screenshot_manager::CGImage::from_ptr(image_ptr);
578 unsafe { AsyncCompletion::complete_ok(user_data, image) };
579 } else {
580 unsafe {
581 AsyncCompletion::<crate::screenshot_manager::CGImage>::complete_err(
582 user_data,
583 "Unknown error".to_string(),
584 );
585 };
586 }
587}
588
589/// Callback for async `CMSampleBuffer` capture
590#[cfg(feature = "macos_14_0")]
591extern "C" fn screenshot_buffer_callback(
592 buffer_ptr: *const c_void,
593 error_ptr: *const i8,
594 user_data: *mut c_void,
595) {
596 if !error_ptr.is_null() {
597 let error = unsafe { error_from_cstr(error_ptr) };
598 unsafe { AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(user_data, error) };
599 } else if !buffer_ptr.is_null() {
600 let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(buffer_ptr.cast_mut()) };
601 unsafe { AsyncCompletion::complete_ok(user_data, buffer) };
602 } else {
603 unsafe {
604 AsyncCompletion::<crate::cm::CMSampleBuffer>::complete_err(
605 user_data,
606 "Unknown error".to_string(),
607 );
608 };
609 }
610}
611
612/// Future for async screenshot capture
613#[cfg(feature = "macos_14_0")]
614pub struct AsyncScreenshotFuture<T> {
615 inner: AsyncCompletionFuture<T>,
616}
617
618#[cfg(feature = "macos_14_0")]
619impl<T> std::fmt::Debug for AsyncScreenshotFuture<T> {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 f.debug_struct("AsyncScreenshotFuture")
622 .finish_non_exhaustive()
623 }
624}
625
626#[cfg(feature = "macos_14_0")]
627impl<T> Future for AsyncScreenshotFuture<T> {
628 type Output = Result<T, SCError>;
629
630 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
631 Pin::new(&mut self.inner)
632 .poll(cx)
633 .map(|r| r.map_err(SCError::ScreenshotError))
634 }
635}
636
637#[cfg(feature = "macos_14_0")]
638impl AsyncSCScreenshotManager {
639 /// Capture a single screenshot as a `CGImage` asynchronously
640 ///
641 /// # Errors
642 /// Returns an error if:
643 /// - Screen recording permission is not granted
644 /// - The capture fails for any reason
645 pub fn capture_image(
646 content_filter: &crate::stream::content_filter::SCContentFilter,
647 configuration: &SCStreamConfiguration,
648 ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
649 let (future, context) = AsyncCompletion::create();
650
651 unsafe {
652 crate::ffi::sc_screenshot_manager_capture_image(
653 content_filter.as_ptr(),
654 configuration.as_ptr(),
655 screenshot_image_callback,
656 context,
657 );
658 }
659
660 AsyncScreenshotFuture { inner: future }
661 }
662
663 /// Capture a single screenshot as a `CMSampleBuffer` asynchronously
664 ///
665 /// # Errors
666 /// Returns an error if:
667 /// - Screen recording permission is not granted
668 /// - The capture fails for any reason
669 pub fn capture_sample_buffer(
670 content_filter: &crate::stream::content_filter::SCContentFilter,
671 configuration: &SCStreamConfiguration,
672 ) -> AsyncScreenshotFuture<crate::cm::CMSampleBuffer> {
673 let (future, context) = AsyncCompletion::create();
674
675 unsafe {
676 crate::ffi::sc_screenshot_manager_capture_sample_buffer(
677 content_filter.as_ptr(),
678 configuration.as_ptr(),
679 screenshot_buffer_callback,
680 context,
681 );
682 }
683
684 AsyncScreenshotFuture { inner: future }
685 }
686
687 /// Capture a screenshot of a specific screen region asynchronously (macOS 15.2+)
688 ///
689 /// This method captures the content within the specified rectangle,
690 /// which can span multiple displays.
691 ///
692 /// # Arguments
693 /// * `rect` - The rectangle to capture, in screen coordinates (points)
694 ///
695 /// # Errors
696 /// Returns an error if:
697 /// - The system is not macOS 15.2+
698 /// - Screen recording permission is not granted
699 /// - The capture fails for any reason
700 #[cfg(feature = "macos_15_2")]
701 pub fn capture_image_in_rect(
702 rect: crate::cg::CGRect,
703 ) -> AsyncScreenshotFuture<crate::screenshot_manager::CGImage> {
704 let (future, context) = AsyncCompletion::create();
705
706 unsafe {
707 crate::ffi::sc_screenshot_manager_capture_image_in_rect(
708 rect.x,
709 rect.y,
710 rect.width,
711 rect.height,
712 screenshot_image_callback,
713 context,
714 );
715 }
716
717 AsyncScreenshotFuture { inner: future }
718 }
719
720 /// Capture a screenshot with advanced configuration asynchronously (macOS 26.0+)
721 ///
722 /// This method uses the new `SCScreenshotConfiguration` for more control
723 /// over the screenshot output, including HDR support and file saving.
724 ///
725 /// # Arguments
726 /// * `content_filter` - The content filter specifying what to capture
727 /// * `configuration` - The screenshot configuration
728 ///
729 /// # Errors
730 /// Returns an error if the capture fails
731 #[cfg(feature = "macos_26_0")]
732 pub fn capture_screenshot(
733 content_filter: &crate::stream::content_filter::SCContentFilter,
734 configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
735 ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
736 let (future, context) = AsyncCompletion::create();
737
738 unsafe {
739 crate::ffi::sc_screenshot_manager_capture_screenshot(
740 content_filter.as_ptr(),
741 configuration.as_ptr(),
742 screenshot_output_callback,
743 context,
744 );
745 }
746
747 AsyncScreenshotFuture { inner: future }
748 }
749
750 /// Capture a screenshot of a specific region with advanced configuration asynchronously (macOS 26.0+)
751 ///
752 /// # Arguments
753 /// * `rect` - The rectangle to capture, in screen coordinates (points)
754 /// * `configuration` - The screenshot configuration
755 ///
756 /// # Errors
757 /// Returns an error if the capture fails
758 #[cfg(feature = "macos_26_0")]
759 pub fn capture_screenshot_in_rect(
760 rect: crate::cg::CGRect,
761 configuration: &crate::screenshot_manager::SCScreenshotConfiguration,
762 ) -> AsyncScreenshotFuture<crate::screenshot_manager::SCScreenshotOutput> {
763 let (future, context) = AsyncCompletion::create();
764
765 unsafe {
766 crate::ffi::sc_screenshot_manager_capture_screenshot_in_rect(
767 rect.x,
768 rect.y,
769 rect.width,
770 rect.height,
771 configuration.as_ptr(),
772 screenshot_output_callback,
773 context,
774 );
775 }
776
777 AsyncScreenshotFuture { inner: future }
778 }
779}
780
781/// Callback for async `SCScreenshotOutput` capture (macOS 26.0+)
782#[cfg(feature = "macos_26_0")]
783extern "C" fn screenshot_output_callback(
784 output_ptr: *const c_void,
785 error_ptr: *const i8,
786 user_data: *mut c_void,
787) {
788 if !error_ptr.is_null() {
789 let error = unsafe { error_from_cstr(error_ptr) };
790 unsafe {
791 AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
792 user_data, error,
793 );
794 }
795 } else if !output_ptr.is_null() {
796 let output = crate::screenshot_manager::SCScreenshotOutput::from_ptr(output_ptr);
797 unsafe { AsyncCompletion::complete_ok(user_data, output) };
798 } else {
799 unsafe {
800 AsyncCompletion::<crate::screenshot_manager::SCScreenshotOutput>::complete_err(
801 user_data,
802 "Unknown error".to_string(),
803 );
804 };
805 }
806}
807
808// ============================================================================
809// AsyncSCContentSharingPicker - Async content sharing picker (macOS 14.0+)
810// ============================================================================
811
812/// Result from the async picker callback
813#[cfg(feature = "macos_14_0")]
814struct AsyncPickerCallbackResult {
815 code: i32,
816 ptr: *const c_void,
817}
818
819#[cfg(feature = "macos_14_0")]
820unsafe impl Send for AsyncPickerCallbackResult {}
821
822/// Callback for async picker
823#[cfg(feature = "macos_14_0")]
824extern "C" fn async_picker_callback(result_code: i32, ptr: *const c_void, user_data: *mut c_void) {
825 let result = AsyncPickerCallbackResult {
826 code: result_code,
827 ptr,
828 };
829 unsafe { AsyncCompletion::complete_ok(user_data, result) };
830}
831
832/// Future for async picker with full result
833#[cfg(feature = "macos_14_0")]
834pub struct AsyncPickerFuture {
835 inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
836}
837
838#[cfg(feature = "macos_14_0")]
839impl std::fmt::Debug for AsyncPickerFuture {
840 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
841 f.debug_struct("AsyncPickerFuture").finish_non_exhaustive()
842 }
843}
844
845#[cfg(feature = "macos_14_0")]
846impl Future for AsyncPickerFuture {
847 type Output = crate::content_sharing_picker::SCPickerOutcome;
848
849 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
850 use crate::content_sharing_picker::{SCPickerOutcome, SCPickerResult};
851
852 match Pin::new(&mut self.inner).poll(cx) {
853 Poll::Pending => Poll::Pending,
854 Poll::Ready(Ok(result)) => {
855 let outcome = match result.code {
856 1 if !result.ptr.is_null() => {
857 SCPickerOutcome::Picked(SCPickerResult::from_ptr(result.ptr))
858 }
859 0 => SCPickerOutcome::Cancelled,
860 _ => SCPickerOutcome::Error("Picker failed".to_string()),
861 };
862 Poll::Ready(outcome)
863 }
864 Poll::Ready(Err(e)) => Poll::Ready(SCPickerOutcome::Error(e)),
865 }
866 }
867}
868
869/// Future for async picker returning filter only
870#[cfg(feature = "macos_14_0")]
871pub struct AsyncPickerFilterFuture {
872 inner: AsyncCompletionFuture<AsyncPickerCallbackResult>,
873}
874
875#[cfg(feature = "macos_14_0")]
876impl std::fmt::Debug for AsyncPickerFilterFuture {
877 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
878 f.debug_struct("AsyncPickerFilterFuture")
879 .finish_non_exhaustive()
880 }
881}
882
883#[cfg(feature = "macos_14_0")]
884impl Future for AsyncPickerFilterFuture {
885 type Output = crate::content_sharing_picker::SCPickerFilterOutcome;
886
887 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
888 use crate::content_sharing_picker::SCPickerFilterOutcome;
889
890 match Pin::new(&mut self.inner).poll(cx) {
891 Poll::Pending => Poll::Pending,
892 Poll::Ready(Ok(result)) => {
893 let outcome = match result.code {
894 1 if !result.ptr.is_null() => {
895 SCPickerFilterOutcome::Filter(SCContentFilter::from_picker_ptr(result.ptr))
896 }
897 0 => SCPickerFilterOutcome::Cancelled,
898 _ => SCPickerFilterOutcome::Error("Picker failed".to_string()),
899 };
900 Poll::Ready(outcome)
901 }
902 Poll::Ready(Err(e)) => Poll::Ready(SCPickerFilterOutcome::Error(e)),
903 }
904 }
905}
906
907/// Async wrapper for `SCContentSharingPicker` (macOS 14.0+)
908///
909/// Provides async methods to show the system content sharing picker UI.
910/// **Executor-agnostic** - works with any async runtime.
911///
912/// # Examples
913///
914/// ```no_run
915/// use screencapturekit::async_api::AsyncSCContentSharingPicker;
916/// use screencapturekit::content_sharing_picker::*;
917///
918/// async fn pick_content() {
919/// let config = SCContentSharingPickerConfiguration::new();
920/// match AsyncSCContentSharingPicker::show(&config).await {
921/// SCPickerOutcome::Picked(result) => {
922/// let (width, height) = result.pixel_size();
923/// let filter = result.filter();
924/// println!("Selected content: {}x{}", width, height);
925/// }
926/// SCPickerOutcome::Cancelled => println!("User cancelled"),
927/// SCPickerOutcome::Error(e) => eprintln!("Error: {}", e),
928/// }
929/// }
930/// ```
931#[cfg(feature = "macos_14_0")]
932#[derive(Debug, Clone, Copy)]
933pub struct AsyncSCContentSharingPicker;
934
935#[cfg(feature = "macos_14_0")]
936impl AsyncSCContentSharingPicker {
937 /// Show the picker UI asynchronously and return `SCPickerResult` with filter and metadata
938 ///
939 /// This is the main API - use when you need content dimensions or want to build custom filters.
940 /// The picker UI will be shown on the main thread, and the future will resolve when the user
941 /// makes a selection or cancels.
942 ///
943 /// # Example
944 /// ```no_run
945 /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
946 /// use screencapturekit::content_sharing_picker::*;
947 ///
948 /// async fn example() {
949 /// let config = SCContentSharingPickerConfiguration::new();
950 /// if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show(&config).await {
951 /// let (width, height) = result.pixel_size();
952 /// let filter = result.filter();
953 /// }
954 /// }
955 /// ```
956 pub fn show(
957 config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
958 ) -> AsyncPickerFuture {
959 let (future, context) = AsyncCompletion::create();
960
961 unsafe {
962 crate::ffi::sc_content_sharing_picker_show_with_result(
963 config.as_ptr(),
964 async_picker_callback,
965 context,
966 );
967 }
968
969 AsyncPickerFuture { inner: future }
970 }
971
972 /// Show the picker UI asynchronously and return an `SCContentFilter` directly
973 ///
974 /// This is the simple API - use when you just need the filter without metadata.
975 ///
976 /// # Example
977 /// ```no_run
978 /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
979 /// use screencapturekit::content_sharing_picker::*;
980 ///
981 /// async fn example() {
982 /// let config = SCContentSharingPickerConfiguration::new();
983 /// if let SCPickerFilterOutcome::Filter(filter) = AsyncSCContentSharingPicker::show_filter(&config).await {
984 /// // Use filter with SCStream
985 /// }
986 /// }
987 /// ```
988 pub fn show_filter(
989 config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
990 ) -> AsyncPickerFilterFuture {
991 let (future, context) = AsyncCompletion::create();
992
993 unsafe {
994 crate::ffi::sc_content_sharing_picker_show(
995 config.as_ptr(),
996 async_picker_callback,
997 context,
998 );
999 }
1000
1001 AsyncPickerFilterFuture { inner: future }
1002 }
1003
1004 /// Show the picker UI for an existing stream to change source while capturing
1005 ///
1006 /// Use this when you have an active `SCStream` and want to let the user
1007 /// select a new content source. The result can be used with `stream.update_content_filter()`.
1008 ///
1009 /// # Example
1010 /// ```no_run
1011 /// use screencapturekit::async_api::AsyncSCContentSharingPicker;
1012 /// use screencapturekit::content_sharing_picker::*;
1013 /// use screencapturekit::stream::SCStream;
1014 /// use screencapturekit::stream::configuration::SCStreamConfiguration;
1015 /// use screencapturekit::stream::content_filter::SCContentFilter;
1016 /// use screencapturekit::shareable_content::SCShareableContent;
1017 ///
1018 /// async fn example() -> Option<()> {
1019 /// let content = SCShareableContent::get().ok()?;
1020 /// let displays = content.displays();
1021 /// let display = displays.first()?;
1022 /// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
1023 /// let stream_config = SCStreamConfiguration::new();
1024 /// let stream = SCStream::new(&filter, &stream_config);
1025 ///
1026 /// // When stream is active and user wants to change source
1027 /// let config = SCContentSharingPickerConfiguration::new();
1028 /// if let SCPickerOutcome::Picked(result) = AsyncSCContentSharingPicker::show_for_stream(&config, &stream).await {
1029 /// // Use result.filter() with stream.update_content_filter()
1030 /// let _ = result.filter();
1031 /// }
1032 /// Some(())
1033 /// }
1034 /// ```
1035 pub fn show_for_stream(
1036 config: &crate::content_sharing_picker::SCContentSharingPickerConfiguration,
1037 stream: &crate::stream::SCStream,
1038 ) -> AsyncPickerFuture {
1039 let (future, context) = AsyncCompletion::create();
1040
1041 unsafe {
1042 crate::ffi::sc_content_sharing_picker_show_for_stream(
1043 config.as_ptr(),
1044 stream.as_ptr(),
1045 async_picker_callback,
1046 context,
1047 );
1048 }
1049
1050 AsyncPickerFuture { inner: future }
1051 }
1052}
1053
1054// ============================================================================
1055// AsyncSCRecordingOutput - Async recording with event stream (macOS 15.0+)
1056// ============================================================================
1057
1058/// Recording lifecycle event
1059#[cfg(feature = "macos_15_0")]
1060#[derive(Debug, Clone, PartialEq, Eq)]
1061pub enum RecordingEvent {
1062 /// Recording started successfully
1063 Started,
1064 /// Recording finished successfully
1065 Finished,
1066 /// Recording failed with an error
1067 Failed(String),
1068}
1069
1070#[cfg(feature = "macos_15_0")]
1071struct AsyncRecordingState {
1072 events: std::collections::VecDeque<RecordingEvent>,
1073 waker: Option<Waker>,
1074 finished: bool,
1075}
1076
1077#[cfg(feature = "macos_15_0")]
1078struct AsyncRecordingDelegate {
1079 state: Arc<Mutex<AsyncRecordingState>>,
1080}
1081
1082#[cfg(feature = "macos_15_0")]
1083impl crate::recording_output::SCRecordingOutputDelegate for AsyncRecordingDelegate {
1084 fn recording_did_start(&self) {
1085 if let Ok(mut state) = self.state.lock() {
1086 state.events.push_back(RecordingEvent::Started);
1087 if let Some(waker) = state.waker.take() {
1088 waker.wake();
1089 }
1090 }
1091 }
1092
1093 fn recording_did_fail(&self, error: String) {
1094 if let Ok(mut state) = self.state.lock() {
1095 state.events.push_back(RecordingEvent::Failed(error));
1096 state.finished = true;
1097 if let Some(waker) = state.waker.take() {
1098 waker.wake();
1099 }
1100 }
1101 }
1102
1103 fn recording_did_finish(&self) {
1104 if let Ok(mut state) = self.state.lock() {
1105 state.events.push_back(RecordingEvent::Finished);
1106 state.finished = true;
1107 if let Some(waker) = state.waker.take() {
1108 waker.wake();
1109 }
1110 }
1111 }
1112}
1113
1114/// Future for getting the next recording event
1115#[cfg(feature = "macos_15_0")]
1116pub struct NextRecordingEvent<'a> {
1117 state: &'a Arc<Mutex<AsyncRecordingState>>,
1118}
1119
1120#[cfg(feature = "macos_15_0")]
1121impl std::fmt::Debug for NextRecordingEvent<'_> {
1122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1123 f.debug_struct("NextRecordingEvent").finish_non_exhaustive()
1124 }
1125}
1126
1127#[cfg(feature = "macos_15_0")]
1128impl Future for NextRecordingEvent<'_> {
1129 type Output = Option<RecordingEvent>;
1130
1131 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1132 let Ok(mut state) = self.state.lock() else {
1133 return Poll::Ready(None);
1134 };
1135
1136 if let Some(event) = state.events.pop_front() {
1137 return Poll::Ready(Some(event));
1138 }
1139
1140 if state.finished {
1141 Poll::Ready(None)
1142 } else {
1143 state.waker = Some(cx.waker().clone());
1144 Poll::Pending
1145 }
1146 }
1147}
1148
1149/// Async wrapper for `SCRecordingOutput` with event stream (macOS 15.0+)
1150///
1151/// Provides async iteration over recording lifecycle events.
1152/// **Executor-agnostic** - works with any async runtime.
1153///
1154/// # Examples
1155///
1156/// ```no_run
1157/// use screencapturekit::async_api::{AsyncSCShareableContent, AsyncSCRecordingOutput, RecordingEvent};
1158/// use screencapturekit::recording_output::SCRecordingOutputConfiguration;
1159/// use screencapturekit::stream::{SCStream, configuration::SCStreamConfiguration, content_filter::SCContentFilter};
1160/// use std::path::Path;
1161///
1162/// async fn record_screen() -> Option<()> {
1163/// let content = AsyncSCShareableContent::get().await.ok()?;
1164/// let displays = content.displays();
1165/// let display = displays.first()?;
1166/// let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
1167/// let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
1168///
1169/// let rec_config = SCRecordingOutputConfiguration::new()
1170/// .with_output_url(Path::new("/tmp/recording.mp4"));
1171///
1172/// let (recording, events) = AsyncSCRecordingOutput::new(&rec_config)?;
1173///
1174/// let mut stream = SCStream::new(&filter, &config);
1175/// stream.add_recording_output(&recording).ok()?;
1176/// stream.start_capture().ok()?;
1177///
1178/// // Wait for recording events
1179/// while let Some(event) = events.next().await {
1180/// match event {
1181/// RecordingEvent::Started => println!("Recording started!"),
1182/// RecordingEvent::Finished => {
1183/// println!("Recording finished!");
1184/// break;
1185/// }
1186/// RecordingEvent::Failed(e) => {
1187/// eprintln!("Recording failed: {}", e);
1188/// break;
1189/// }
1190/// }
1191/// }
1192///
1193/// Some(())
1194/// }
1195/// ```
1196#[cfg(feature = "macos_15_0")]
1197pub struct AsyncSCRecordingOutput {
1198 state: Arc<Mutex<AsyncRecordingState>>,
1199}
1200
1201#[cfg(feature = "macos_15_0")]
1202impl std::fmt::Debug for AsyncSCRecordingOutput {
1203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1204 f.debug_struct("AsyncSCRecordingOutput")
1205 .finish_non_exhaustive()
1206 }
1207}
1208
1209#[cfg(feature = "macos_15_0")]
1210impl AsyncSCRecordingOutput {
1211 /// Create a new async recording output
1212 ///
1213 /// Returns a tuple of (`SCRecordingOutput`, `AsyncSCRecordingOutput`).
1214 /// The `SCRecordingOutput` should be added to an `SCStream`, while
1215 /// the `AsyncSCRecordingOutput` provides async event iteration.
1216 ///
1217 /// # Errors
1218 ///
1219 /// Returns `None` if the recording output cannot be created (requires macOS 15.0+).
1220 #[must_use]
1221 pub fn new(
1222 config: &crate::recording_output::SCRecordingOutputConfiguration,
1223 ) -> Option<(crate::recording_output::SCRecordingOutput, Self)> {
1224 let state = Arc::new(Mutex::new(AsyncRecordingState {
1225 events: std::collections::VecDeque::new(),
1226 waker: None,
1227 finished: false,
1228 }));
1229
1230 let delegate = AsyncRecordingDelegate {
1231 state: Arc::clone(&state),
1232 };
1233
1234 let recording =
1235 crate::recording_output::SCRecordingOutput::new_with_delegate(config, delegate)?;
1236
1237 Some((recording, Self { state }))
1238 }
1239
1240 /// Get the next recording event asynchronously
1241 ///
1242 /// Returns `None` when the recording has finished or failed.
1243 pub fn next(&self) -> NextRecordingEvent<'_> {
1244 NextRecordingEvent { state: &self.state }
1245 }
1246
1247 /// Check if the recording has finished
1248 #[must_use]
1249 pub fn is_finished(&self) -> bool {
1250 self.state.lock().map(|s| s.finished).unwrap_or(true)
1251 }
1252
1253 /// Get any pending events without waiting
1254 #[must_use]
1255 pub fn try_next(&self) -> Option<RecordingEvent> {
1256 self.state.lock().ok()?.events.pop_front()
1257 }
1258}