screencapturekit/stream/sc_stream.rs
1//! Swift FFI based `SCStream` implementation
2//!
3//! This is the primary (and only) implementation in v1.0+.
4//! All `ScreenCaptureKit` operations use direct Swift FFI bindings.
5//!
6//! Each stream owns a heap-allocated `StreamContext` that holds its output
7//! handlers and delegate. The context pointer is passed through FFI so that
8//! callbacks route directly to the owning stream — no global registries.
9
10use std::ffi::{c_void, CStr};
11use std::fmt;
12use std::sync::atomic::{AtomicUsize, Ordering};
13use std::sync::RwLock;
14
15use crate::error::SCError;
16use crate::stream::delegate_trait::SCStreamDelegateTrait;
17use crate::utils::completion::UnitCompletion;
18use crate::utils::panic_safe::catch_user_panic;
19use crate::{
20 dispatch_queue::DispatchQueue,
21 ffi,
22 stream::{
23 configuration::SCStreamConfiguration, content_filter::SCContentFilter,
24 output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType,
25 },
26};
27
28/// Per-stream handler entry.
29struct HandlerEntry {
30 id: usize,
31 of_type: SCStreamOutputType,
32 handler: Box<dyn SCStreamOutputTrait>,
33}
34
35/// Per-stream context holding output handlers and an optional delegate.
36///
37/// Allocated on the heap via `Box::into_raw` and passed through FFI as an
38/// opaque context pointer. Callbacks cast it back to `&StreamContext` for
39/// direct, O(1) access to the owning stream's state.
40///
41/// `handlers` and `delegate` are stored behind `RwLock`s rather than
42/// `Mutex`es so concurrent callbacks from `ScreenCaptureKit`'s independent
43/// dispatch queues (e.g. screen + audio) can dispatch in parallel. Slow
44/// user handlers no longer serialise across output types.
45struct StreamContext {
46 handlers: RwLock<Vec<HandlerEntry>>,
47 delegate: RwLock<Option<Box<dyn SCStreamDelegateTrait>>>,
48 ref_count: AtomicUsize,
49}
50
51impl StreamContext {
52 fn new() -> *mut Self {
53 let ctx = Box::new(Self {
54 handlers: RwLock::new(Vec::new()),
55 delegate: RwLock::new(None),
56 ref_count: AtomicUsize::new(1),
57 });
58 Box::into_raw(ctx)
59 }
60
61 fn new_with_delegate(delegate: Box<dyn SCStreamDelegateTrait>) -> *mut Self {
62 let ctx = Box::new(Self {
63 handlers: RwLock::new(Vec::new()),
64 delegate: RwLock::new(Some(delegate)),
65 ref_count: AtomicUsize::new(1),
66 });
67 Box::into_raw(ctx)
68 }
69
70 /// Increment the reference count.
71 ///
72 /// # Safety
73 ///
74 /// `ptr` must point to a valid, live `StreamContext`.
75 unsafe fn retain(ptr: *mut Self) {
76 unsafe { &*ptr }.ref_count.fetch_add(1, Ordering::Relaxed);
77 }
78
79 /// Decrement the reference count, freeing the context if it reaches zero.
80 ///
81 /// # Safety
82 ///
83 /// `ptr` must point to a valid, live `StreamContext`. After this call,
84 /// `ptr` must not be used if the context was freed.
85 unsafe fn release(ptr: *mut Self) {
86 if ptr.is_null() {
87 return;
88 }
89 let prev = unsafe { &*ptr }.ref_count.fetch_sub(1, Ordering::Release);
90 if prev == 1 {
91 // The Acquire fence is required (NOT redundant — it pairs with
92 // the Release stores from other threads' `fetch_sub` calls
93 // and any other writes to `*ptr` they performed). It guarantees
94 // that the freeing thread sees all happened-before writes from
95 // every other thread that previously held a reference. This is
96 // the canonical Arc-style refcount drop pattern (see
97 // `std::sync::Arc::drop`); removing the fence is unsound on
98 // weakly-ordered architectures (e.g. AArch64).
99 std::sync::atomic::fence(Ordering::Acquire);
100 drop(unsafe { Box::from_raw(ptr) });
101 }
102 }
103}
104
105/// Compile-time assertion: `StreamContext` is `Send + Sync`.
106///
107/// `SCStream` carries `unsafe impl Send + Sync` (lines below); that impl is
108/// only sound if the underlying `StreamContext` is itself `Send + Sync`.
109/// Without this static check, a future refactor that adds a `!Send` or
110/// `!Sync` field (or removes the `Send`/`Sync` bound from a trait it holds
111/// in `Box<dyn …>`) would silently invalidate the unsafe impl with no
112/// compiler error. This `const _` forces a compile error in that case.
113const _: fn() = || {
114 fn assert_send_sync<T: Send + Sync>() {}
115 assert_send_sync::<StreamContext>();
116};
117
118/// Monotonically increasing handler ID generator (process-wide).
119static NEXT_HANDLER_ID: AtomicUsize = AtomicUsize::new(1);
120
121// C callback for stream errors — dispatches to per-stream delegate via context pointer.
122//
123// Safety: this function is called from Swift. A Rust panic unwinding across
124// the C ABI is undefined behavior, so all user-visible code (delegate trait
125// methods) is wrapped in `catch_unwind`. The `delegate` lock is taken with
126// `unwrap_or_else` poisoning recovery so a panic in one callback cannot
127// permanently break the stream by poisoning the lock.
128extern "C" fn delegate_error_callback(context: *mut c_void, error_code: i32, msg: *const i8) {
129 if context.is_null() {
130 return;
131 }
132 let ctx = unsafe { &*(context.cast::<StreamContext>()) };
133
134 let message = if msg.is_null() {
135 "Unknown error".to_string()
136 } else {
137 // Best-effort: if Swift sent a non-UTF-8 buffer, fall back to a
138 // placeholder rather than panicking.
139 unsafe { CStr::from_ptr(msg) }
140 .to_str()
141 .unwrap_or("Unknown error")
142 .to_string()
143 };
144
145 let error = if error_code != 0 {
146 crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
147 || SCError::StreamError(format!("{message} (code: {error_code})")),
148 |code| SCError::SCStreamError {
149 code,
150 message: Some(message.clone()),
151 },
152 )
153 } else {
154 SCError::StreamError(message.clone())
155 };
156
157 // Take a read lock and dispatch under it. Multiple delegate callbacks
158 // (e.g. error + activity) from independent queues can run concurrently.
159 // Recover from poisoning in case a previous callback panicked outside
160 // catch_unwind (defense in depth).
161 let delegate_guard = ctx
162 .delegate
163 .read()
164 .unwrap_or_else(std::sync::PoisonError::into_inner);
165
166 if let Some(ref delegate) = *delegate_guard {
167 // Wrap user code in catch_unwind so panics never propagate into Swift.
168 catch_user_panic("delegate.did_stop_with_error", || {
169 delegate.did_stop_with_error(error);
170 });
171 catch_user_panic("delegate.stream_did_stop", || {
172 delegate.stream_did_stop(Some(message));
173 });
174 return;
175 }
176
177 drop(delegate_guard);
178 // Fallback to logging if no delegate registered
179 eprintln!("SCStream error: {error}");
180}
181
182// C callback for sample buffers — dispatches to per-stream handlers via context pointer.
183//
184// Safety: this function is called from Swift on a dispatch queue. A Rust
185// panic across the C ABI is UB; every user handler invocation is wrapped in
186// `catch_unwind`. The `handlers` lock is a read lock so independent dispatch
187// queues (screen, audio, microphone) can dispatch in parallel — a slow
188// handler on one queue cannot block callbacks on another. The `passRetained`
189// `CMSampleBuffer` reference Swift hands us is consumed exactly once: each
190// non-final matching handler receives a freshly retained clone, and the
191// final matching handler consumes the original.
192extern "C" fn sample_handler(context: *mut c_void, sample_buffer: *const c_void, output_type: i32) {
193 if context.is_null() {
194 unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
195 return;
196 }
197 let ctx = unsafe { &*(context.cast::<StreamContext>()) };
198
199 let output_type_enum = match output_type {
200 0 => SCStreamOutputType::Screen,
201 1 => SCStreamOutputType::Audio,
202 2 => SCStreamOutputType::Microphone,
203 _ => {
204 eprintln!("Unknown output type: {output_type}");
205 unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
206 return;
207 }
208 };
209
210 // Read lock allows concurrent dispatch from independent dispatch queues.
211 // Recover from poisoning in case a previous panic somehow escaped
212 // catch_unwind (defense in depth).
213 let handlers = ctx
214 .handlers
215 .read()
216 .unwrap_or_else(std::sync::PoisonError::into_inner);
217
218 let mut matching = handlers
219 .iter()
220 .filter(|e| e.of_type == output_type_enum)
221 .peekable();
222
223 if matching.peek().is_none() {
224 // Drop the lock before releasing the buffer, in case the release
225 // path ever takes any locks of its own.
226 drop(handlers);
227 unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
228 return;
229 }
230
231 while let Some(entry) = matching.next() {
232 // Retain for every handler except the last; the last handler consumes
233 // the original `passRetained` reference Swift gave us. `peek()` after
234 // `next()` reports the next matching entry (or None if `entry` was
235 // the last matching one).
236 let is_last = matching.peek().is_none();
237 if !is_last {
238 unsafe { crate::cm::ffi::cm_sample_buffer_retain(sample_buffer.cast_mut()) };
239 }
240
241 let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(sample_buffer.cast_mut()) };
242
243 // Wrap user code in catch_unwind so panics never propagate into Swift.
244 // If the handler panics, `buffer` is dropped on unwind, which calls
245 // `cm_sample_buffer_release` and balances the retain we just did
246 // (or, for the last handler, balances the original `passRetained`).
247 // The retain/release accounting is preserved either way.
248 catch_user_panic("output handler", || {
249 entry
250 .handler
251 .did_output_sample_buffer(buffer, output_type_enum);
252 });
253 }
254}
255
256/// `SCStream` is a lightweight wrapper around the Swift `SCStream` instance.
257/// It provides direct FFI access to `ScreenCaptureKit` functionality.
258///
259/// This is the primary and only implementation of `SCStream` in v1.0+.
260/// All `ScreenCaptureKit` operations go through Swift FFI bindings.
261///
262/// # Examples
263///
264/// ```no_run
265/// use screencapturekit::prelude::*;
266///
267/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
268/// // Get shareable content
269/// let content = SCShareableContent::get()?;
270/// let display = &content.displays()[0];
271///
272/// // Create filter and configuration
273/// let filter = SCContentFilter::create()
274/// .with_display(display)
275/// .with_excluding_windows(&[])
276/// .build();
277/// let config = SCStreamConfiguration::new()
278/// .with_width(1920)
279/// .with_height(1080);
280///
281/// // Create and start stream
282/// let mut stream = SCStream::new(&filter, &config);
283/// stream.start_capture()?;
284///
285/// // ... capture frames ...
286///
287/// stream.stop_capture()?;
288/// # Ok(())
289/// # }
290/// ```
291pub struct SCStream {
292 ptr: *const c_void,
293 /// Per-stream context holding handlers and delegate (ref-counted).
294 context: *mut StreamContext,
295}
296
297unsafe impl Send for SCStream {}
298unsafe impl Sync for SCStream {}
299
300impl SCStream {
301 /// Create a new stream with a content filter and configuration
302 ///
303 /// # Examples
304 ///
305 /// ```no_run
306 /// use screencapturekit::prelude::*;
307 ///
308 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
309 /// let content = SCShareableContent::get()?;
310 /// let display = &content.displays()[0];
311 /// let filter = SCContentFilter::create()
312 /// .with_display(display)
313 /// .with_excluding_windows(&[])
314 /// .build();
315 /// let config = SCStreamConfiguration::new()
316 /// .with_width(1920)
317 /// .with_height(1080);
318 ///
319 /// let stream = SCStream::new(&filter, &config);
320 /// # Ok(())
321 /// # }
322 /// ```
323 pub fn new(filter: &SCContentFilter, configuration: &SCStreamConfiguration) -> Self {
324 let context = StreamContext::new();
325 let context_ptr = context.cast::<c_void>();
326
327 let ptr = unsafe {
328 ffi::sc_stream_create(
329 filter.as_ptr(),
330 configuration.as_ptr(),
331 context_ptr,
332 delegate_error_callback,
333 sample_handler,
334 )
335 };
336
337 Self { ptr, context }
338 }
339
340 /// Create a new stream with a content filter, configuration, and delegate
341 ///
342 /// The delegate receives callbacks for stream lifecycle events:
343 /// - `did_stop_with_error` - Called when the stream stops due to an error
344 /// - `stream_did_stop` - Called when the stream stops (with optional error message)
345 ///
346 /// # Examples
347 ///
348 /// ```no_run
349 /// use screencapturekit::prelude::*;
350 /// use screencapturekit::stream::delegate_trait::StreamCallbacks;
351 ///
352 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
353 /// let content = SCShareableContent::get()?;
354 /// let display = &content.displays()[0];
355 /// let filter = SCContentFilter::create()
356 /// .with_display(display)
357 /// .with_excluding_windows(&[])
358 /// .build();
359 /// let config = SCStreamConfiguration::new()
360 /// .with_width(1920)
361 /// .with_height(1080);
362 ///
363 /// let delegate = StreamCallbacks::new()
364 /// .on_error(|e| eprintln!("Stream error: {}", e))
365 /// .on_stop(|err| {
366 /// if let Some(msg) = err {
367 /// eprintln!("Stream stopped with error: {}", msg);
368 /// }
369 /// });
370 ///
371 /// let stream = SCStream::new_with_delegate(&filter, &config, delegate);
372 /// stream.start_capture()?;
373 /// # Ok(())
374 /// # }
375 /// ```
376 pub fn new_with_delegate(
377 filter: &SCContentFilter,
378 configuration: &SCStreamConfiguration,
379 delegate: impl SCStreamDelegateTrait + 'static,
380 ) -> Self {
381 let context = StreamContext::new_with_delegate(Box::new(delegate));
382 let context_ptr = context.cast::<c_void>();
383
384 let ptr = unsafe {
385 ffi::sc_stream_create(
386 filter.as_ptr(),
387 configuration.as_ptr(),
388 context_ptr,
389 delegate_error_callback,
390 sample_handler,
391 )
392 };
393
394 Self { ptr, context }
395 }
396
397 /// Add an output handler to receive captured frames
398 ///
399 /// # Arguments
400 ///
401 /// * `handler` - The handler to receive callbacks. Can be:
402 /// - A struct implementing [`SCStreamOutputTrait`]
403 /// - A closure `|CMSampleBuffer, SCStreamOutputType| { ... }`
404 /// * `of_type` - The type of output to receive (Screen, Audio, or Microphone)
405 ///
406 /// # Returns
407 ///
408 /// Returns `Some(handler_id)` on success, `None` on failure.
409 /// The handler ID can be used with [`remove_output_handler`](Self::remove_output_handler).
410 ///
411 /// # Dispatch queue
412 ///
413 /// The handler is invoked on a dedicated user-interactive serial dispatch
414 /// queue created by the bridge. This intentionally **deviates from
415 /// Apple's `SCStream.addStreamOutput`** API, whose `nil` queue parameter
416 /// means "deliver on the main queue". Main-queue dispatch only works
417 /// when the host process runs a Cocoa runloop, which Rust apps
418 /// generally don't, so the default would otherwise silently drop
419 /// every frame. Use [`add_output_handler_with_queue`](Self::add_output_handler_with_queue)
420 /// and pass an explicit [`DispatchQueue`] (e.g. one wrapping main) if
421 /// you need a different queue — including AppKit/UIKit affinity.
422 ///
423 /// # Examples
424 ///
425 /// Using a struct:
426 /// ```rust,no_run
427 /// use screencapturekit::prelude::*;
428 ///
429 /// struct MyHandler;
430 /// impl SCStreamOutputTrait for MyHandler {
431 /// fn did_output_sample_buffer(&self, _sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
432 /// println!("Got frame!");
433 /// }
434 /// }
435 ///
436 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
437 /// # let content = SCShareableContent::get()?;
438 /// # let display = &content.displays()[0];
439 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
440 /// # let config = SCStreamConfiguration::default();
441 /// let mut stream = SCStream::new(&filter, &config);
442 /// stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);
443 /// # Ok(())
444 /// # }
445 /// ```
446 ///
447 /// Using a closure:
448 /// ```rust,no_run
449 /// use screencapturekit::prelude::*;
450 ///
451 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
452 /// # let content = SCShareableContent::get()?;
453 /// # let display = &content.displays()[0];
454 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
455 /// # let config = SCStreamConfiguration::default();
456 /// let mut stream = SCStream::new(&filter, &config);
457 /// stream.add_output_handler(
458 /// |_sample, _type| println!("Got frame!"),
459 /// SCStreamOutputType::Screen
460 /// );
461 /// # Ok(())
462 /// # }
463 /// ```
464 ///
465 /// # Sharing state with handlers
466 ///
467 /// The handler bound is `impl SCStreamOutputTrait + 'static`. The
468 /// `'static` is required because the handler is stored inside
469 /// `SCStream` which can outlive any borrowed reference. Combined
470 /// with the trait's `Send + Sync` bound (callbacks run on
471 /// independent dispatch queues, see
472 /// [`SCStreamOutputTrait`](crate::stream::output_trait::SCStreamOutputTrait)),
473 /// the canonical pattern for sharing state with a handler is to
474 /// wrap it in `Arc<Mutex<T>>` (or `Arc<AtomicXxx>` for primitives):
475 ///
476 /// ```rust,no_run
477 /// use screencapturekit::prelude::*;
478 /// use std::sync::{Arc, Mutex, atomic::{AtomicUsize, Ordering}};
479 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
480 /// # let content = SCShareableContent::get()?;
481 /// # let display = &content.displays()[0];
482 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
483 /// # let config = SCStreamConfiguration::default();
484 /// let frame_count = Arc::new(AtomicUsize::new(0));
485 /// let count_handler = frame_count.clone();
486 /// let mut stream = SCStream::new(&filter, &config);
487 /// stream.add_output_handler(
488 /// move |_sample, _type| {
489 /// count_handler.fetch_add(1, Ordering::Relaxed);
490 /// },
491 /// SCStreamOutputType::Screen,
492 /// );
493 /// // outer scope can still read frame_count any time:
494 /// println!("frames so far: {}", frame_count.load(Ordering::Relaxed));
495 /// # Ok(())
496 /// # }
497 /// ```
498 pub fn add_output_handler(
499 &mut self,
500 handler: impl SCStreamOutputTrait + 'static,
501 of_type: SCStreamOutputType,
502 ) -> Option<usize> {
503 self.add_output_handler_with_queue(handler, of_type, None)
504 }
505
506 /// Add an output handler with a custom dispatch queue
507 ///
508 /// This allows controlling which thread/queue the handler is called on.
509 ///
510 /// # Arguments
511 ///
512 /// * `handler` - The handler to receive callbacks
513 /// * `of_type` - The type of output to receive
514 /// * `queue` - Optional custom dispatch queue for callbacks
515 ///
516 /// # Examples
517 ///
518 /// ```rust,no_run
519 /// use screencapturekit::prelude::*;
520 /// use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
521 ///
522 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
523 /// # let content = SCShareableContent::get()?;
524 /// # let display = &content.displays()[0];
525 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
526 /// # let config = SCStreamConfiguration::default();
527 /// let mut stream = SCStream::new(&filter, &config);
528 /// let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
529 ///
530 /// stream.add_output_handler_with_queue(
531 /// |_sample, _type| println!("Got frame on custom queue!"),
532 /// SCStreamOutputType::Screen,
533 /// Some(&queue)
534 /// );
535 /// # Ok(())
536 /// # }
537 /// ```
538 pub fn add_output_handler_with_queue(
539 &mut self,
540 handler: impl SCStreamOutputTrait + 'static,
541 of_type: SCStreamOutputType,
542 queue: Option<&DispatchQueue>,
543 ) -> Option<usize> {
544 let handler_id = NEXT_HANDLER_ID.fetch_add(1, Ordering::Relaxed);
545
546 // Convert output type to int for Swift
547 let output_type_int = match of_type {
548 SCStreamOutputType::Screen => 0,
549 SCStreamOutputType::Audio => 1,
550 SCStreamOutputType::Microphone => 2,
551 };
552
553 let ok = if let Some(q) = queue {
554 unsafe {
555 ffi::sc_stream_add_stream_output_with_queue(self.ptr, output_type_int, q.as_ptr())
556 }
557 } else {
558 unsafe { ffi::sc_stream_add_stream_output(self.ptr, output_type_int) }
559 };
560
561 if ok {
562 unsafe { &*self.context }
563 .handlers
564 .write()
565 .unwrap_or_else(std::sync::PoisonError::into_inner)
566 .push(HandlerEntry {
567 id: handler_id,
568 of_type,
569 handler: Box::new(handler),
570 });
571 Some(handler_id)
572 } else {
573 None
574 }
575 }
576
577 /// Remove an output handler
578 ///
579 /// # Arguments
580 ///
581 /// * `id` - The handler ID returned from [`add_output_handler`](Self::add_output_handler)
582 /// * `of_type` - The type of output the handler was registered for
583 ///
584 /// # Returns
585 ///
586 /// Returns `true` if the handler was found and removed, `false` otherwise.
587 pub fn remove_output_handler(&mut self, id: usize, of_type: SCStreamOutputType) -> bool {
588 let mut handlers = unsafe { &*self.context }
589 .handlers
590 .write()
591 .unwrap_or_else(std::sync::PoisonError::into_inner);
592 let Some(pos) = handlers.iter().position(|e| e.id == id) else {
593 return false;
594 };
595 handlers.remove(pos);
596
597 // If no more handlers for this output type, tell Swift to remove the output
598 let has_type = handlers.iter().any(|e| e.of_type == of_type);
599 drop(handlers);
600
601 if !has_type {
602 let output_type_int = match of_type {
603 SCStreamOutputType::Screen => 0,
604 SCStreamOutputType::Audio => 1,
605 SCStreamOutputType::Microphone => 2,
606 };
607 unsafe { ffi::sc_stream_remove_stream_output(self.ptr, output_type_int) };
608 }
609
610 true
611 }
612
613 /// Start capturing screen content
614 ///
615 /// This method blocks until the capture operation completes or fails.
616 ///
617 /// # Errors
618 ///
619 /// Returns `SCError::CaptureStartFailed` if the capture fails to start.
620 pub fn start_capture(&self) -> Result<(), SCError> {
621 let (completion, context) = UnitCompletion::new();
622 unsafe { ffi::sc_stream_start_capture(self.ptr, context, UnitCompletion::callback) };
623 completion.wait().map_err(SCError::CaptureStartFailed)
624 }
625
626 /// Stop capturing screen content
627 ///
628 /// This method blocks until the capture operation completes or fails.
629 ///
630 /// # Errors
631 ///
632 /// Returns `SCError::CaptureStopFailed` if the capture fails to stop.
633 pub fn stop_capture(&self) -> Result<(), SCError> {
634 let (completion, context) = UnitCompletion::new();
635 unsafe { ffi::sc_stream_stop_capture(self.ptr, context, UnitCompletion::callback) };
636 completion.wait().map_err(SCError::CaptureStopFailed)
637 }
638
639 /// Update the stream configuration
640 ///
641 /// This method blocks until the configuration update completes or fails.
642 ///
643 /// # Errors
644 ///
645 /// Returns `SCError::StreamError` if the configuration update fails.
646 pub fn update_configuration(
647 &self,
648 configuration: &SCStreamConfiguration,
649 ) -> Result<(), SCError> {
650 let (completion, context) = UnitCompletion::new();
651 unsafe {
652 ffi::sc_stream_update_configuration(
653 self.ptr,
654 configuration.as_ptr(),
655 context,
656 UnitCompletion::callback,
657 );
658 }
659 completion.wait().map_err(SCError::StreamError)
660 }
661
662 /// Update the content filter
663 ///
664 /// This method blocks until the filter update completes or fails.
665 ///
666 /// # Errors
667 ///
668 /// Returns `SCError::StreamError` if the filter update fails.
669 pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
670 let (completion, context) = UnitCompletion::new();
671 unsafe {
672 ffi::sc_stream_update_content_filter(
673 self.ptr,
674 filter.as_ptr(),
675 context,
676 UnitCompletion::callback,
677 );
678 }
679 completion.wait().map_err(SCError::StreamError)
680 }
681
682 /// Get the synchronization clock for this stream (macOS 13.0+)
683 ///
684 /// Returns the `CMClock` used to synchronize the stream's output.
685 /// This is useful for coordinating multiple streams or synchronizing
686 /// with other media.
687 ///
688 /// Returns `None` if the clock is not available (e.g., stream not started
689 /// or macOS version too old).
690 #[cfg(feature = "macos_13_0")]
691 pub fn synchronization_clock(&self) -> Option<crate::cm::CMClock> {
692 let ptr = unsafe { ffi::sc_stream_get_synchronization_clock(self.ptr) };
693 if ptr.is_null() {
694 None
695 } else {
696 Some(crate::cm::CMClock::from_ptr(ptr))
697 }
698 }
699
700 /// Add a recording output to the stream (macOS 15.0+)
701 ///
702 /// Starts recording if the stream is already capturing, otherwise recording
703 /// will start when capture begins. The recording is written to the file URL
704 /// specified in the `SCRecordingOutputConfiguration`.
705 ///
706 /// # Errors
707 ///
708 /// Returns `SCError::StreamError` if adding the recording output fails.
709 #[cfg(feature = "macos_15_0")]
710 pub fn add_recording_output(
711 &self,
712 recording_output: &crate::recording_output::SCRecordingOutput,
713 ) -> Result<(), SCError> {
714 let (completion, context) = UnitCompletion::new();
715 unsafe {
716 ffi::sc_stream_add_recording_output(
717 self.ptr,
718 recording_output.as_ptr(),
719 UnitCompletion::callback,
720 context,
721 );
722 }
723 completion.wait().map_err(SCError::StreamError)
724 }
725
726 /// Remove a recording output from the stream (macOS 15.0+)
727 ///
728 /// Stops recording if the stream is currently recording.
729 ///
730 /// # Errors
731 ///
732 /// Returns `SCError::StreamError` if removing the recording output fails.
733 #[cfg(feature = "macos_15_0")]
734 pub fn remove_recording_output(
735 &self,
736 recording_output: &crate::recording_output::SCRecordingOutput,
737 ) -> Result<(), SCError> {
738 let (completion, context) = UnitCompletion::new();
739 unsafe {
740 ffi::sc_stream_remove_recording_output(
741 self.ptr,
742 recording_output.as_ptr(),
743 UnitCompletion::callback,
744 context,
745 );
746 }
747 completion.wait().map_err(SCError::StreamError)
748 }
749
750 /// Returns the raw pointer to the underlying Swift `SCStream` instance.
751 #[allow(dead_code)]
752 pub(crate) fn as_ptr(&self) -> *const c_void {
753 self.ptr
754 }
755}
756
757impl Drop for SCStream {
758 fn drop(&mut self) {
759 if !self.ptr.is_null() {
760 unsafe { ffi::sc_stream_release(self.ptr) };
761 }
762 unsafe { StreamContext::release(self.context) };
763 }
764}
765
766impl Clone for SCStream {
767 /// Clone the stream reference.
768 ///
769 /// Cloning an `SCStream` creates a new reference to the same underlying
770 /// Swift `SCStream` object. The cloned stream shares the same handlers
771 /// as the original — they receive frames from the same capture session.
772 ///
773 /// Both the original and cloned stream share the same capture state, so:
774 /// - Starting capture on one affects both
775 /// - Stopping capture on one affects both
776 /// - Configuration updates affect both
777 /// - Handlers receive the same frames
778 ///
779 /// # Examples
780 ///
781 /// ```rust,no_run
782 /// use screencapturekit::prelude::*;
783 ///
784 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
785 /// # let content = SCShareableContent::get()?;
786 /// # let display = &content.displays()[0];
787 /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
788 /// # let config = SCStreamConfiguration::default();
789 /// let mut stream = SCStream::new(&filter, &config);
790 /// stream.add_output_handler(|_, _| println!("Handler 1"), SCStreamOutputType::Screen);
791 ///
792 /// // Clone shares the same handlers
793 /// let stream2 = stream.clone();
794 /// // Both stream and stream2 will receive frames via Handler 1
795 /// # Ok(())
796 /// # }
797 /// ```
798 fn clone(&self) -> Self {
799 unsafe { StreamContext::retain(self.context) };
800
801 Self {
802 ptr: unsafe { crate::ffi::sc_stream_retain(self.ptr) },
803 context: self.context,
804 }
805 }
806}
807
808impl fmt::Debug for SCStream {
809 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
810 f.debug_struct("SCStream")
811 .field("ptr", &self.ptr)
812 .finish_non_exhaustive()
813 }
814}
815
816impl fmt::Display for SCStream {
817 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
818 write!(f, "SCStream")
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use std::sync::atomic::AtomicUsize;
826 use std::sync::Arc;
827
828 /// Regression test for #135: multiple concurrent streams must not leak
829 /// samples across each other.
830 ///
831 /// Creates two independent `StreamContexts` with separate handlers and
832 /// directly invokes each context's handlers. Verifies that each handler
833 /// only receives calls routed through its own context — not from the
834 /// other context. With the old global `HANDLER_REGISTRY`, both handlers
835 /// would have been called for every callback regardless of context.
836 #[test]
837 fn test_per_stream_callback_isolation() {
838 let count_a = Arc::new(AtomicUsize::new(0));
839 let count_b = Arc::new(AtomicUsize::new(0));
840
841 // Create two independent contexts (simulates two SCStream instances)
842 let ctx_a = StreamContext::new();
843 let ctx_b = StreamContext::new();
844
845 // Register an audio handler on context A
846 {
847 let counter = count_a.clone();
848 let mut handlers = unsafe { &*ctx_a }
849 .handlers
850 .write()
851 .unwrap_or_else(std::sync::PoisonError::into_inner);
852 handlers.push(HandlerEntry {
853 id: 1,
854 of_type: SCStreamOutputType::Audio,
855 handler: Box::new(
856 move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
857 counter.fetch_add(1, Ordering::Relaxed);
858 // Prevent Drop from calling cm_sample_buffer_release on our fake pointer
859 std::mem::forget(buf);
860 },
861 ),
862 });
863 }
864
865 // Register an audio handler on context B
866 {
867 let counter = count_b.clone();
868 let mut handlers = unsafe { &*ctx_b }
869 .handlers
870 .write()
871 .unwrap_or_else(std::sync::PoisonError::into_inner);
872 handlers.push(HandlerEntry {
873 id: 2,
874 of_type: SCStreamOutputType::Audio,
875 handler: Box::new(
876 move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
877 counter.fetch_add(1, Ordering::Relaxed);
878 std::mem::forget(buf);
879 },
880 ),
881 });
882 }
883
884 // Simulate 5 audio callbacks on context A by directly calling matching handlers
885 for _ in 0..5 {
886 let handlers = unsafe { &*ctx_a }
887 .handlers
888 .write()
889 .unwrap_or_else(std::sync::PoisonError::into_inner);
890 for entry in handlers
891 .iter()
892 .filter(|e| e.of_type == SCStreamOutputType::Audio)
893 {
894 let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
895 entry
896 .handler
897 .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
898 }
899 }
900
901 // Simulate 3 audio callbacks on context B
902 for _ in 0..3 {
903 let handlers = unsafe { &*ctx_b }
904 .handlers
905 .write()
906 .unwrap_or_else(std::sync::PoisonError::into_inner);
907 for entry in handlers
908 .iter()
909 .filter(|e| e.of_type == SCStreamOutputType::Audio)
910 {
911 let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
912 entry
913 .handler
914 .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
915 }
916 }
917
918 // Handler A must have received exactly 5 — not 8
919 assert_eq!(
920 count_a.load(Ordering::Relaxed),
921 5,
922 "handler A received callbacks meant for B (cross-stream leak)"
923 );
924 // Handler B must have received exactly 3 — not 8
925 assert_eq!(
926 count_b.load(Ordering::Relaxed),
927 3,
928 "handler B received callbacks meant for A (cross-stream leak)"
929 );
930
931 unsafe {
932 StreamContext::release(ctx_a);
933 StreamContext::release(ctx_b);
934 }
935 }
936
937 /// Verify that handlers are filtered by output type within a single context.
938 #[test]
939 fn test_handler_output_type_filtering() {
940 let screen_count = Arc::new(AtomicUsize::new(0));
941 let audio_count = Arc::new(AtomicUsize::new(0));
942
943 let ctx = StreamContext::new();
944
945 {
946 let counter = screen_count.clone();
947 let mut handlers = unsafe { &*ctx }
948 .handlers
949 .write()
950 .unwrap_or_else(std::sync::PoisonError::into_inner);
951 handlers.push(HandlerEntry {
952 id: 1,
953 of_type: SCStreamOutputType::Screen,
954 handler: Box::new(
955 move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
956 counter.fetch_add(1, Ordering::Relaxed);
957 std::mem::forget(buf);
958 },
959 ),
960 });
961 }
962 {
963 let counter = audio_count.clone();
964 let mut handlers = unsafe { &*ctx }
965 .handlers
966 .write()
967 .unwrap_or_else(std::sync::PoisonError::into_inner);
968 handlers.push(HandlerEntry {
969 id: 2,
970 of_type: SCStreamOutputType::Audio,
971 handler: Box::new(
972 move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
973 counter.fetch_add(1, Ordering::Relaxed);
974 std::mem::forget(buf);
975 },
976 ),
977 });
978 }
979
980 // Send 4 screen callbacks
981 for _ in 0..4 {
982 let handlers = unsafe { &*ctx }
983 .handlers
984 .write()
985 .unwrap_or_else(std::sync::PoisonError::into_inner);
986 for entry in handlers
987 .iter()
988 .filter(|e| e.of_type == SCStreamOutputType::Screen)
989 {
990 let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
991 entry
992 .handler
993 .did_output_sample_buffer(buf, SCStreamOutputType::Screen);
994 }
995 }
996
997 // Send 2 audio callbacks
998 for _ in 0..2 {
999 let handlers = unsafe { &*ctx }
1000 .handlers
1001 .write()
1002 .unwrap_or_else(std::sync::PoisonError::into_inner);
1003 for entry in handlers
1004 .iter()
1005 .filter(|e| e.of_type == SCStreamOutputType::Audio)
1006 {
1007 let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1008 entry
1009 .handler
1010 .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
1011 }
1012 }
1013
1014 assert_eq!(screen_count.load(Ordering::Relaxed), 4);
1015 assert_eq!(audio_count.load(Ordering::Relaxed), 2);
1016
1017 unsafe { StreamContext::release(ctx) };
1018 }
1019
1020 /// Verify that `StreamContext` ref counting works correctly.
1021 #[test]
1022 fn test_stream_context_ref_counting() {
1023 let ctx = StreamContext::new();
1024
1025 // Initial ref count is 1
1026 assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
1027
1028 // Retain bumps to 2
1029 unsafe { StreamContext::retain(ctx) };
1030 assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 2);
1031
1032 // First release drops to 1 — context still alive
1033 unsafe { StreamContext::release(ctx) };
1034 assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
1035
1036 // Second release drops to 0 — context freed (no crash = success)
1037 unsafe { StreamContext::release(ctx) };
1038 }
1039
1040 /// Regression test: a panic in a user-supplied output handler must NOT
1041 /// poison the handlers `RwLock`, must NOT propagate across the C ABI,
1042 /// and must NOT prevent subsequent callbacks from being dispatched.
1043 ///
1044 /// This validates the C1+C2 fix from the deep review: `catch_unwind`
1045 /// around user dispatch and `RwLock` poisoning recovery via
1046 /// `unwrap_or_else(PoisonError::into_inner)` together prevent one
1047 /// panicking handler from permanently breaking the stream.
1048 #[test]
1049 fn test_panic_in_handler_is_isolated() {
1050 // Set a no-op panic hook so our intentional panic doesn't spam the
1051 // test output. We restore it at the end of the test.
1052 let original_hook = std::panic::take_hook();
1053 std::panic::set_hook(Box::new(|_| {}));
1054
1055 let panicked_count = Arc::new(AtomicUsize::new(0));
1056 let normal_count = Arc::new(AtomicUsize::new(0));
1057
1058 let ctx = StreamContext::new();
1059
1060 // Handler 1: always panics
1061 {
1062 let counter = panicked_count.clone();
1063 let mut handlers = unsafe { &*ctx }
1064 .handlers
1065 .write()
1066 .unwrap_or_else(std::sync::PoisonError::into_inner);
1067 handlers.push(HandlerEntry {
1068 id: 1,
1069 of_type: SCStreamOutputType::Audio,
1070 handler: Box::new(
1071 move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1072 counter.fetch_add(1, Ordering::Relaxed);
1073 std::mem::forget(buf);
1074 panic!("intentional test panic");
1075 },
1076 ),
1077 });
1078 }
1079
1080 // Handler 2: well-behaved, registered AFTER the panicker
1081 {
1082 let counter = normal_count.clone();
1083 let mut handlers = unsafe { &*ctx }
1084 .handlers
1085 .write()
1086 .unwrap_or_else(std::sync::PoisonError::into_inner);
1087 handlers.push(HandlerEntry {
1088 id: 2,
1089 of_type: SCStreamOutputType::Audio,
1090 handler: Box::new(
1091 move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1092 counter.fetch_add(1, Ordering::Relaxed);
1093 std::mem::forget(buf);
1094 },
1095 ),
1096 });
1097 }
1098
1099 // Simulate 5 callbacks. Each iteration, the panicker fires (and
1100 // panics), then the well-behaved handler must still fire on the
1101 // SAME callback because both handlers match the output type. We
1102 // simulate the dispatch path without going through the C callback
1103 // (which would require a real CMSampleBuffer); the key behaviour
1104 // we're verifying is that the lock isn't poisoned and that the
1105 // catch_unwind boundary contains the panic.
1106 for _ in 0..5 {
1107 let handlers = unsafe { &*ctx }
1108 .handlers
1109 .read()
1110 .unwrap_or_else(std::sync::PoisonError::into_inner);
1111 for entry in handlers
1112 .iter()
1113 .filter(|e| e.of_type == SCStreamOutputType::Audio)
1114 {
1115 let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1116 catch_user_panic("test handler", || {
1117 entry
1118 .handler
1119 .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
1120 });
1121 }
1122 }
1123
1124 // Both handlers fired 5 times each — the panicker did not stop the
1125 // dispatch loop or poison the lock for subsequent reads.
1126 assert_eq!(
1127 panicked_count.load(Ordering::Relaxed),
1128 5,
1129 "panicking handler stopped firing after first panic"
1130 );
1131 assert_eq!(
1132 normal_count.load(Ordering::Relaxed),
1133 5,
1134 "well-behaved handler stopped firing after panicker poisoned state"
1135 );
1136
1137 // Lock is still acquirable (would otherwise be poisoned).
1138 drop(
1139 unsafe { &*ctx }
1140 .handlers
1141 .write()
1142 .unwrap_or_else(std::sync::PoisonError::into_inner),
1143 );
1144
1145 unsafe { StreamContext::release(ctx) };
1146
1147 // Restore the original panic hook so other tests behave normally.
1148 std::panic::set_hook(original_hook);
1149 }
1150}