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}