Skip to main content

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}