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