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