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}