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