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