Skip to main content

screencapturekit/stream/
delegate_trait.rs

1//! Delegate trait for stream lifecycle events
2//!
3//! Defines the interface for receiving stream state change notifications.
4//!
5//! Use [`SCStream::new_with_delegate`](crate::stream::SCStream::new_with_delegate)
6//! to create a stream with a delegate that receives error callbacks.
7
8use crate::error::SCError;
9
10/// Trait for handling stream lifecycle events
11///
12/// Implement this trait to receive notifications about stream state changes,
13/// errors, and video effects.
14///
15/// # Examples
16///
17/// ## Using a struct
18///
19/// ```
20/// use screencapturekit::stream::delegate_trait::SCStreamDelegateTrait;
21/// use screencapturekit::error::SCError;
22///
23/// struct MyDelegate;
24///
25/// impl SCStreamDelegateTrait for MyDelegate {
26///     fn did_stop_with_error(&self, error: SCError) {
27///         eprintln!("Stream stopped with error: {}", error);
28///     }
29/// }
30/// ```
31///
32/// ## Using closures
33///
34/// Use [`StreamCallbacks`] to create a delegate from closures:
35///
36/// ```rust,no_run
37/// use screencapturekit::prelude::*;
38/// use screencapturekit::stream::delegate_trait::StreamCallbacks;
39///
40/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
41/// # let content = SCShareableContent::get()?;
42/// # let display = &content.displays()[0];
43/// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
44/// # let config = SCStreamConfiguration::default();
45///
46/// let delegate = StreamCallbacks::new()
47///     .on_stop(|error| {
48///         if let Some(e) = error {
49///             eprintln!("Stream stopped with error: {}", e);
50///         }
51///     })
52///     .on_error(|error| eprintln!("Error: {}", error));
53///
54/// let stream = SCStream::new_with_delegate(&filter, &config, delegate);
55/// # Ok(())
56/// # }
57/// ```
58pub trait SCStreamDelegateTrait: Send + Sync {
59    /// Called when video effects start (macOS 14.0+)
60    ///
61    /// Notifies when the stream's overlay video effect (presenter overlay) has started.
62    fn output_video_effect_did_start_for_stream(&self) {}
63
64    /// Called when video effects stop (macOS 14.0+)
65    ///
66    /// Notifies when the stream's overlay video effect (presenter overlay) has stopped.
67    fn output_video_effect_did_stop_for_stream(&self) {}
68
69    /// Called when the stream becomes active (macOS 15.2+)
70    ///
71    /// Notifies the first time any window that was being shared in the stream
72    /// is re-opened after all the windows being shared were closed.
73    /// When all the windows being shared are closed, the client will receive
74    /// `stream_did_become_inactive`.
75    fn stream_did_become_active(&self) {}
76
77    /// Called when the stream becomes inactive (macOS 15.2+)
78    ///
79    /// Notifies when all the windows that are currently being shared are exited.
80    /// This callback occurs for all content filter types.
81    fn stream_did_become_inactive(&self) {}
82
83    /// Called when the stream stops with an error.
84    ///
85    /// This is the canonical stop notification and mirrors Apple's
86    /// `stream(_:didStopWithError:)` — the *only* way `ScreenCaptureKit`
87    /// reports a stop to the delegate. It fires when the stream stops
88    /// unexpectedly (the captured window/display goes away, screen-recording
89    /// permission is revoked, the system tears the stream down, …).
90    ///
91    /// A *clean* stop that you requested via
92    /// [`SCStream::stop_capture`](crate::stream::SCStream::stop_capture) is
93    /// **not** reported here — observe it through that method's return value.
94    fn did_stop_with_error(&self, _error: SCError) {}
95
96    /// Called when stream stops.
97    ///
98    /// # Parameters
99    ///
100    /// - `error`: Optional error message if the stream stopped due to an error
101    #[deprecated(
102        note = "ScreenCaptureKit reports stops only via `did_stop_with_error`; the stream \
103                engine no longer invokes this method. Implement `did_stop_with_error` instead."
104    )]
105    fn stream_did_stop(&self, _error: Option<String>) {}
106}
107
108/// A simple error handler wrapper for closures
109///
110/// Allows using a closure as a stream delegate that only handles errors.
111///
112/// # Examples
113///
114/// ```rust,no_run
115/// use screencapturekit::prelude::*;
116/// use screencapturekit::stream::delegate_trait::ErrorHandler;
117///
118/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
119/// # let content = SCShareableContent::get()?;
120/// # let display = &content.displays()[0];
121/// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
122/// # let config = SCStreamConfiguration::default();
123///
124/// let error_handler = ErrorHandler::new(|error| {
125///     eprintln!("Stream error: {}", error);
126/// });
127///
128/// let stream = SCStream::new_with_delegate(&filter, &config, error_handler);
129/// # Ok(())
130/// # }
131/// ```
132pub struct ErrorHandler<F>
133where
134    F: Fn(SCError) + Send + Sync + 'static,
135{
136    handler: F,
137}
138
139impl<F> std::fmt::Debug for ErrorHandler<F>
140where
141    F: Fn(SCError) + Send + Sync + 'static,
142{
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("ErrorHandler").finish_non_exhaustive()
145    }
146}
147
148impl<F> ErrorHandler<F>
149where
150    F: Fn(SCError) + Send + Sync + 'static,
151{
152    /// Create a new error handler from a closure
153    pub fn new(handler: F) -> Self {
154        Self { handler }
155    }
156}
157
158impl<F> SCStreamDelegateTrait for ErrorHandler<F>
159where
160    F: Fn(SCError) + Send + Sync + 'static,
161{
162    fn did_stop_with_error(&self, error: SCError) {
163        (self.handler)(error);
164    }
165}
166
167/// Builder for closure-based stream delegate
168///
169/// Provides a convenient way to create a stream delegate using closures
170/// instead of implementing the [`SCStreamDelegateTrait`] trait.
171///
172/// # Examples
173///
174/// ```rust,no_run
175/// use screencapturekit::prelude::*;
176/// use screencapturekit::stream::delegate_trait::StreamCallbacks;
177///
178/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
179/// # let content = SCShareableContent::get()?;
180/// # let display = &content.displays()[0];
181/// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
182/// # let config = SCStreamConfiguration::default();
183///
184/// // Create delegate with multiple callbacks
185/// let delegate = StreamCallbacks::new()
186///     .on_stop(|error| {
187///         if let Some(e) = error {
188///             eprintln!("Stream stopped with error: {}", e);
189///         } else {
190///             println!("Stream stopped normally");
191///         }
192///     })
193///     .on_error(|error| eprintln!("Stream error: {}", error))
194///     .on_active(|| println!("Stream became active"))
195///     .on_inactive(|| println!("Stream became inactive"));
196///
197/// let stream = SCStream::new_with_delegate(&filter, &config, delegate);
198/// # Ok(())
199/// # }
200/// ```
201#[allow(clippy::struct_field_names)]
202pub struct StreamCallbacks {
203    on_stop: Option<Box<dyn Fn(Option<String>) + Send + Sync + 'static>>,
204    on_error: Option<Box<dyn Fn(SCError) + Send + Sync + 'static>>,
205    on_active: Option<Box<dyn Fn() + Send + Sync + 'static>>,
206    on_inactive: Option<Box<dyn Fn() + Send + Sync + 'static>>,
207    on_video_effect_start: Option<Box<dyn Fn() + Send + Sync + 'static>>,
208    on_video_effect_stop: Option<Box<dyn Fn() + Send + Sync + 'static>>,
209}
210
211impl StreamCallbacks {
212    /// Create a new empty callbacks builder
213    #[must_use]
214    pub fn new() -> Self {
215        Self {
216            on_stop: None,
217            on_error: None,
218            on_active: None,
219            on_inactive: None,
220            on_video_effect_start: None,
221            on_video_effect_stop: None,
222        }
223    }
224
225    /// Set the callback for when the stream stops.
226    ///
227    /// The closure receives `Some(message)` describing the error that stopped
228    /// the stream. Because `ScreenCaptureKit` only reports *error* stops to the
229    /// delegate, this fires alongside [`on_error`](Self::on_error) on an error
230    /// stop; a clean stop you requested via
231    /// [`SCStream::stop_capture`](crate::stream::SCStream::stop_capture) is not
232    /// delivered here. Prefer [`on_error`](Self::on_error) when you want the
233    /// typed [`SCError`].
234    #[must_use]
235    pub fn on_stop<F>(mut self, f: F) -> Self
236    where
237        F: Fn(Option<String>) + Send + Sync + 'static,
238    {
239        self.on_stop = Some(Box::new(f));
240        self
241    }
242
243    /// Set the callback for when the stream encounters an error
244    #[must_use]
245    pub fn on_error<F>(mut self, f: F) -> Self
246    where
247        F: Fn(SCError) + Send + Sync + 'static,
248    {
249        self.on_error = Some(Box::new(f));
250        self
251    }
252
253    /// Set the callback for when the stream becomes active (macOS 15.2+)
254    #[must_use]
255    pub fn on_active<F>(mut self, f: F) -> Self
256    where
257        F: Fn() + Send + Sync + 'static,
258    {
259        self.on_active = Some(Box::new(f));
260        self
261    }
262
263    /// Set the callback for when the stream becomes inactive (macOS 15.2+)
264    #[must_use]
265    pub fn on_inactive<F>(mut self, f: F) -> Self
266    where
267        F: Fn() + Send + Sync + 'static,
268    {
269        self.on_inactive = Some(Box::new(f));
270        self
271    }
272
273    /// Set the callback for when video effects start (macOS 14.0+)
274    #[must_use]
275    pub fn on_video_effect_start<F>(mut self, f: F) -> Self
276    where
277        F: Fn() + Send + Sync + 'static,
278    {
279        self.on_video_effect_start = Some(Box::new(f));
280        self
281    }
282
283    /// Set the callback for when video effects stop (macOS 14.0+)
284    #[must_use]
285    pub fn on_video_effect_stop<F>(mut self, f: F) -> Self
286    where
287        F: Fn() + Send + Sync + 'static,
288    {
289        self.on_video_effect_stop = Some(Box::new(f));
290        self
291    }
292}
293
294impl Default for StreamCallbacks {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300impl std::fmt::Debug for StreamCallbacks {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        f.debug_struct("StreamCallbacks")
303            .field("on_stop", &self.on_stop.is_some())
304            .field("on_error", &self.on_error.is_some())
305            .field("on_active", &self.on_active.is_some())
306            .field("on_inactive", &self.on_inactive.is_some())
307            .field(
308                "on_video_effect_start",
309                &self.on_video_effect_start.is_some(),
310            )
311            .field("on_video_effect_stop", &self.on_video_effect_stop.is_some())
312            .finish()
313    }
314}
315
316impl SCStreamDelegateTrait for StreamCallbacks {
317    // Retained so direct/manual callers (and legacy code) that still invoke
318    // `stream_did_stop` continue to route to `on_stop`. The stream engine no
319    // longer calls this; error stops flow through `did_stop_with_error` below.
320    #[allow(deprecated)]
321    fn stream_did_stop(&self, error: Option<String>) {
322        if let Some(ref f) = self.on_stop {
323            f(error);
324        }
325    }
326
327    fn did_stop_with_error(&self, error: SCError) {
328        // ScreenCaptureKit only reports error stops, so drive both `on_error`
329        // (typed) and `on_stop` (message) from this single engine callback.
330        if let Some(ref f) = self.on_stop {
331            f(Some(error.to_string()));
332        }
333        if let Some(ref f) = self.on_error {
334            f(error);
335        }
336    }
337
338    fn stream_did_become_active(&self) {
339        if let Some(ref f) = self.on_active {
340            f();
341        }
342    }
343
344    fn stream_did_become_inactive(&self) {
345        if let Some(ref f) = self.on_inactive {
346            f();
347        }
348    }
349
350    fn output_video_effect_did_start_for_stream(&self) {
351        if let Some(ref f) = self.on_video_effect_start {
352            f();
353        }
354    }
355
356    fn output_video_effect_did_stop_for_stream(&self) {
357        if let Some(ref f) = self.on_video_effect_stop {
358            f();
359        }
360    }
361}