5252//! let config = SCStreamConfiguration::new().with_width(1920).with_height(1080);
5353//!
5454//! let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
55- //! stream.start_capture()?;
55+ //! stream.start_capture().await ?;
5656//!
5757//! // Process frames asynchronously
5858//! for _ in 0..100 {
6161//! }
6262//! }
6363//!
64- //! stream.stop_capture()?;
64+ //! stream.stop_capture().await ?;
6565//! # Ok(())
6666//! # }
6767//! ```
@@ -399,6 +399,67 @@ impl Future for NextSample<'_> {
399399unsafe impl Send for AsyncSampleSender { }
400400unsafe impl Sync for AsyncSampleSender { }
401401
402+ // ----------------------------------------------------------------------------
403+ // Stream lifecycle control futures (start / stop / update)
404+ // ----------------------------------------------------------------------------
405+
406+ /// FFI completion callback for [`AsyncSCStream`] lifecycle operations.
407+ ///
408+ /// Translates the Swift `(context, success, message)` completion into the
409+ /// waker-based [`AsyncCompletion`] machinery, so awaiting a control future
410+ /// resumes the task via its [`Waker`] instead of parking a thread. This is the
411+ /// same primitive used by the content / screenshot / picker futures.
412+ extern "C" fn stream_control_callback ( context : * mut c_void , success : bool , msg : * const i8 ) {
413+ crate :: utils:: panic_safe:: catch_user_panic ( "stream_control_callback" , move || {
414+ if success {
415+ // SAFETY: `context` is the one-shot completion pointer from
416+ // `AsyncCompletion::<()>::create()`; Swift invokes this callback
417+ // exactly once, after which the pointer is consumed.
418+ unsafe { AsyncCompletion :: < ( ) > :: complete_ok ( context, ( ) ) } ;
419+ } else {
420+ let error = unsafe { error_from_cstr ( msg) } ;
421+ // SAFETY: see above — one-shot completion pointer, fired once.
422+ unsafe { AsyncCompletion :: < ( ) > :: complete_err ( context, error) } ;
423+ }
424+ } ) ;
425+ }
426+
427+ /// Future for an [`AsyncSCStream`] lifecycle operation — `start_capture`,
428+ /// `stop_capture`, `update_configuration`, or `update_content_filter`.
429+ ///
430+ /// Resolves once `ScreenCaptureKit` acknowledges the operation. Awaiting it
431+ /// **never blocks the executor thread**: the task is parked via its [`Waker`]
432+ /// and resumed from the Swift completion callback. This mirrors the underlying
433+ /// Swift `Task { try await … }` entry points, keeping the async surface
434+ /// consistent end to end.
435+ ///
436+ /// The operation is kicked off eagerly when the method is called (a "hot"
437+ /// future), matching the rest of this module — e.g.
438+ /// [`AsyncSCShareableContent::get`]. Dropping the future without awaiting is
439+ /// safe; it simply means success/failure is not observed.
440+ #[ must_use = "the operation starts eagerly, but you must .await the future to observe success or failure" ]
441+ pub struct StreamControlFuture {
442+ inner : AsyncCompletionFuture < ( ) > ,
443+ map_err : fn ( String ) -> SCError ,
444+ }
445+
446+ impl std:: fmt:: Debug for StreamControlFuture {
447+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
448+ f. debug_struct ( "StreamControlFuture" ) . finish_non_exhaustive ( )
449+ }
450+ }
451+
452+ impl Future for StreamControlFuture {
453+ type Output = Result < ( ) , SCError > ;
454+
455+ fn poll ( mut self : Pin < & mut Self > , cx : & mut Context < ' _ > ) -> Poll < Self :: Output > {
456+ let map_err = self . map_err ;
457+ Pin :: new ( & mut self . inner )
458+ . poll ( cx)
459+ . map ( |r| r. map_err ( map_err) )
460+ }
461+ }
462+
402463/// Async wrapper for `SCStream` with integrated frame iteration
403464///
404465/// Provides async methods for stream lifecycle and frame iteration.
@@ -421,7 +482,7 @@ unsafe impl Sync for AsyncSampleSender {}
421482/// .with_height(1080);
422483///
423484/// let stream = AsyncSCStream::new(&filter, &config, 30, SCStreamOutputType::Screen);
424- /// stream.start_capture()?;
485+ /// stream.start_capture().await ?;
425486///
426487/// // Process frames asynchronously
427488/// while let Some(frame) = stream.next().await {
@@ -527,40 +588,113 @@ impl AsyncSCStream {
527588 }
528589 }
529590
530- /// Start capture (synchronous - returns immediately)
591+ /// Start capture asynchronously.
592+ ///
593+ /// Resolves when `ScreenCaptureKit` confirms the stream has started.
594+ /// Unlike [`SCStream::start_capture`](crate::stream::SCStream::start_capture),
595+ /// awaiting this **does not block the executor thread** — the task is parked
596+ /// via its [`Waker`] and resumed from the Swift completion callback.
597+ ///
598+ /// The capture is initiated eagerly when this method is called; `.await`
599+ /// observes the completion (or error).
531600 ///
532601 /// # Errors
533602 ///
534- /// Returns an error if capture fails to start.
535- pub fn start_capture ( & self ) -> Result < ( ) , SCError > {
536- self . stream . start_capture ( )
603+ /// The awaited result is `Err(SCError::CaptureStartFailed)` if the stream
604+ /// fails to start.
605+ pub fn start_capture ( & self ) -> StreamControlFuture {
606+ let ( future, context) = AsyncCompletion :: < ( ) > :: create ( ) ;
607+ // SAFETY: `self.stream.as_ptr()` is a valid, live `SCStream` pointer for
608+ // the duration of this call; `context` is the one-shot completion
609+ // pointer from `AsyncCompletion::create()`, invoked exactly once.
610+ unsafe {
611+ crate :: ffi:: sc_stream_start_capture (
612+ self . stream . as_ptr ( ) ,
613+ context,
614+ stream_control_callback,
615+ ) ;
616+ }
617+ StreamControlFuture {
618+ inner : future,
619+ map_err : SCError :: CaptureStartFailed ,
620+ }
537621 }
538622
539- /// Stop capture (synchronous - returns immediately)
623+ /// Stop capture asynchronously.
624+ ///
625+ /// Resolves when `ScreenCaptureKit` confirms the stream has stopped. Awaiting
626+ /// this **does not block the executor thread**.
540627 ///
541628 /// # Errors
542629 ///
543- /// Returns an error if capture fails to stop.
544- pub fn stop_capture ( & self ) -> Result < ( ) , SCError > {
545- self . stream . stop_capture ( )
630+ /// The awaited result is `Err(SCError::CaptureStopFailed)` if the stream
631+ /// fails to stop.
632+ pub fn stop_capture ( & self ) -> StreamControlFuture {
633+ let ( future, context) = AsyncCompletion :: < ( ) > :: create ( ) ;
634+ // SAFETY: see `start_capture` — live stream pointer, one-shot context.
635+ unsafe {
636+ crate :: ffi:: sc_stream_stop_capture (
637+ self . stream . as_ptr ( ) ,
638+ context,
639+ stream_control_callback,
640+ ) ;
641+ }
642+ StreamControlFuture {
643+ inner : future,
644+ map_err : SCError :: CaptureStopFailed ,
645+ }
546646 }
547647
548- /// Update stream configuration
648+ /// Update stream configuration asynchronously.
649+ ///
650+ /// Resolves when the reconfiguration completes. Awaiting this **does not
651+ /// block the executor thread**.
549652 ///
550653 /// # Errors
551654 ///
552- /// Returns an error if the update fails.
553- pub fn update_configuration ( & self , config : & SCStreamConfiguration ) -> Result < ( ) , SCError > {
554- self . stream . update_configuration ( config)
655+ /// The awaited result is `Err(SCError::StreamError)` if the update fails.
656+ pub fn update_configuration ( & self , config : & SCStreamConfiguration ) -> StreamControlFuture {
657+ let ( future, context) = AsyncCompletion :: < ( ) > :: create ( ) ;
658+ // SAFETY: `self.stream.as_ptr()` and `config.as_ptr()` are valid for the
659+ // duration of this call; `context` is the one-shot completion pointer.
660+ unsafe {
661+ crate :: ffi:: sc_stream_update_configuration (
662+ self . stream . as_ptr ( ) ,
663+ config. as_ptr ( ) ,
664+ context,
665+ stream_control_callback,
666+ ) ;
667+ }
668+ StreamControlFuture {
669+ inner : future,
670+ map_err : SCError :: StreamError ,
671+ }
555672 }
556673
557- /// Update content filter
674+ /// Update content filter asynchronously.
675+ ///
676+ /// Resolves when the filter swap completes. Awaiting this **does not block
677+ /// the executor thread**.
558678 ///
559679 /// # Errors
560680 ///
561- /// Returns an error if the update fails.
562- pub fn update_content_filter ( & self , filter : & SCContentFilter ) -> Result < ( ) , SCError > {
563- self . stream . update_content_filter ( filter)
681+ /// The awaited result is `Err(SCError::StreamError)` if the update fails.
682+ pub fn update_content_filter ( & self , filter : & SCContentFilter ) -> StreamControlFuture {
683+ let ( future, context) = AsyncCompletion :: < ( ) > :: create ( ) ;
684+ // SAFETY: `self.stream.as_ptr()` and `filter.as_ptr()` are valid for the
685+ // duration of this call; `context` is the one-shot completion pointer.
686+ unsafe {
687+ crate :: ffi:: sc_stream_update_content_filter (
688+ self . stream . as_ptr ( ) ,
689+ filter. as_ptr ( ) ,
690+ context,
691+ stream_control_callback,
692+ ) ;
693+ }
694+ StreamControlFuture {
695+ inner : future,
696+ map_err : SCError :: StreamError ,
697+ }
564698 }
565699
566700 /// Get a reference to the underlying stream
0 commit comments