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::num::NonZeroUsize;
13use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
14use std::sync::{Arc, Mutex, RwLock};
15
16use crate::error::{SCError, SCResult};
17use crate::stream::delegate_trait::SCStreamDelegateTrait;
18use crate::utils::completion::{is_timeout_error, UnitCompletion};
19use crate::utils::panic_safe::catch_user_panic;
20use crate::{
21    dispatch_queue::DispatchQueue,
22    ffi,
23    stream::{
24        configuration::SCStreamConfiguration, content_filter::SCContentFilter,
25        output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType,
26    },
27};
28
29/// Per-stream handler entry.
30///
31/// The handler is behind an `Arc` so a callback can clone it out of the
32/// registry, release the lock, and only then run user code — see
33/// [`sample_handler`].
34struct HandlerEntry {
35    id: usize,
36    of_type: SCStreamOutputType,
37    handler: Arc<dyn SCStreamOutputTrait>,
38}
39
40/// The native dispatch queue established for one output type.
41///
42/// `ScreenCaptureKit` is given a single bridge-side `SCStreamOutput` object per
43/// stream, so there is exactly one native registration — and therefore one
44/// queue — per output type, no matter how many Rust handlers are attached.
45#[derive(Clone, Copy, PartialEq, Eq)]
46enum OutputQueue {
47    /// The dedicated user-interactive queue the bridge creates.
48    BridgeDefault,
49    /// A caller-supplied [`DispatchQueue`], identified by its raw pointer.
50    Custom(usize),
51}
52
53/// Per-stream context holding output handlers and an optional delegate.
54///
55/// Allocated on the heap via `Box::into_raw` and passed through FFI as an
56/// opaque context pointer. Callbacks cast it back to `&StreamContext` for
57/// direct, O(1) access to the owning stream's state.
58///
59/// `handlers` and `delegate` are stored behind `RwLock`s rather than
60/// `Mutex`es so concurrent callbacks from `ScreenCaptureKit`'s independent
61/// dispatch queues (e.g. screen + audio) can dispatch in parallel. The locks
62/// are held only long enough to clone out the `Arc`s that a callback needs;
63/// user code always runs unlocked.
64struct StreamContext {
65    handlers: RwLock<Vec<HandlerEntry>>,
66    delegate: RwLock<Option<Arc<dyn SCStreamDelegateTrait>>>,
67    /// Queue established by the first successful registration for each output
68    /// type, cleared when the last handler of that type is removed.
69    output_queues: RwLock<Vec<(SCStreamOutputType, OutputQueue)>>,
70    output_mutation: Mutex<()>,
71    capturing: Arc<AtomicBool>,
72    /// Set when a `start_capture` completion timed out, leaving `capturing`
73    /// latched without a confirmed outcome. The next start reissues instead of
74    /// short-circuiting to `Ok(())` on the strength of a start that may never
75    /// have landed.
76    start_unconfirmed: Arc<AtomicBool>,
77    #[cfg(feature = "macos_15_0")]
78    recording_outputs: AtomicUsize,
79    ref_count: AtomicUsize,
80}
81
82impl StreamContext {
83    fn new(delegate: Option<Arc<dyn SCStreamDelegateTrait>>) -> *mut Self {
84        let ctx = Box::new(Self {
85            handlers: RwLock::new(Vec::new()),
86            delegate: RwLock::new(delegate),
87            output_queues: RwLock::new(Vec::new()),
88            output_mutation: Mutex::new(()),
89            capturing: Arc::new(AtomicBool::new(false)),
90            start_unconfirmed: Arc::new(AtomicBool::new(false)),
91            #[cfg(feature = "macos_15_0")]
92            recording_outputs: AtomicUsize::new(0),
93            ref_count: AtomicUsize::new(1),
94        });
95        Box::into_raw(ctx)
96    }
97
98    /// Increment the reference count.
99    ///
100    /// # Safety
101    ///
102    /// `ptr` must point to a valid, live `StreamContext`.
103    unsafe fn retain(ptr: *mut Self) {
104        unsafe { &*ptr }.ref_count.fetch_add(1, Ordering::Relaxed);
105    }
106
107    /// Decrement the reference count, freeing the context if it reaches zero.
108    ///
109    /// # Safety
110    ///
111    /// `ptr` must point to a valid, live `StreamContext`. After this call,
112    /// `ptr` must not be used if the context was freed.
113    unsafe fn release(ptr: *mut Self) {
114        if ptr.is_null() {
115            return;
116        }
117        let prev = unsafe { &*ptr }.ref_count.fetch_sub(1, Ordering::Release);
118        if prev == 1 {
119            // The Acquire fence is required (NOT redundant — it pairs with
120            // the Release stores from other threads' `fetch_sub` calls
121            // and any other writes to `*ptr` they performed). It guarantees
122            // that the freeing thread sees all happened-before writes from
123            // every other thread that previously held a reference. This is
124            // the canonical Arc-style refcount drop pattern (see
125            // `std::sync::Arc::drop`); removing the fence is unsound on
126            // weakly-ordered architectures (e.g. AArch64).
127            std::sync::atomic::fence(Ordering::Acquire);
128            drop(unsafe { Box::from_raw(ptr) });
129        }
130    }
131
132    /// Clone the delegate out from under the lock so user code runs unlocked.
133    ///
134    /// Holding the lock across a delegate call would deadlock any delegate that
135    /// touches the stream, and would serialise otherwise-independent callbacks.
136    fn delegate_snapshot(&self) -> Option<Arc<dyn SCStreamDelegateTrait>> {
137        self.delegate
138            .read()
139            .unwrap_or_else(std::sync::PoisonError::into_inner)
140            .clone()
141    }
142
143    /// Clone the handlers matching `of_type` out from under the lock.
144    ///
145    /// A handler removed concurrently with a callback already in flight may
146    /// still receive that one sample — the `Arc` keeps it alive for the
147    /// duration — but never receives one afterwards.
148    fn handler_snapshot(&self, of_type: SCStreamOutputType) -> Vec<Arc<dyn SCStreamOutputTrait>> {
149        self.handlers
150            .read()
151            .unwrap_or_else(std::sync::PoisonError::into_inner)
152            .iter()
153            .filter(|e| e.of_type == of_type)
154            .map(|e| Arc::clone(&e.handler))
155            .collect()
156    }
157
158    #[cfg(feature = "macos_15_0")]
159    fn has_handlers(&self) -> bool {
160        !self
161            .handlers
162            .read()
163            .unwrap_or_else(std::sync::PoisonError::into_inner)
164            .is_empty()
165    }
166}
167
168/// Compile-time assertion: `StreamContext` is `Send + Sync`.
169///
170/// `SCStream` carries `unsafe impl Send + Sync` (lines below); that impl is
171/// only sound if the underlying `StreamContext` is itself `Send + Sync`.
172/// Without this static check, a future refactor that adds a `!Send` or
173/// `!Sync` field (or removes the `Send`/`Sync` bound from a trait it holds
174/// in `Box<dyn …>`) would silently invalidate the unsafe impl with no
175/// compiler error. This `const _` forces a compile error in that case.
176const _: fn() = || {
177    fn assert_send_sync<T: Send + Sync>() {}
178    assert_send_sync::<StreamContext>();
179};
180
181/// Monotonically increasing handler ID generator (process-wide).
182static NEXT_HANDLER_ID: AtomicUsize = AtomicUsize::new(1);
183
184/// Discriminant the Swift bridge uses for each output type.
185const fn native_output_type(of_type: SCStreamOutputType) -> i32 {
186    match of_type {
187        SCStreamOutputType::Screen => 0,
188        SCStreamOutputType::Audio => 1,
189        SCStreamOutputType::Microphone => 2,
190    }
191}
192
193// C trampoline handed to Swift so the bridge objects (delegate wrapper and
194// output handler) can each take a +1 reference on the `StreamContext` for the
195// duration of their own lifetime. This keeps the context alive while any
196// callback can still be dispatched on it.
197extern "C" fn context_retain_cb(context: *mut c_void) {
198    if !context.is_null() {
199        unsafe { StreamContext::retain(context.cast::<StreamContext>()) };
200    }
201}
202
203// C trampoline handed to Swift, invoked from each bridge object's `deinit` to
204// drop the +1 reference taken in `context_retain_cb`. `StreamContext::release`
205// null-checks internally.
206extern "C" fn context_release_cb(context: *mut c_void) {
207    catch_user_panic("StreamContext::release", || unsafe {
208        StreamContext::release(context.cast::<StreamContext>());
209    });
210}
211
212// C callback for stream errors — dispatches to per-stream delegate via context pointer.
213//
214// Safety: this function is called from Swift. A Rust panic unwinding across
215// the C ABI is undefined behavior, so all user-visible code (delegate trait
216// methods) is wrapped in `catch_unwind`. The `delegate` lock is taken with
217// `unwrap_or_else` poisoning recovery so a panic in one callback cannot
218// permanently break the stream by poisoning the lock.
219extern "C" fn delegate_error_callback(context: *mut c_void, error_code: i32, msg: *const i8) {
220    if context.is_null() {
221        return;
222    }
223    // SAFETY: `context` is the +1-retained StreamContext pointer the Swift
224    // bridge stored via context_retain_cb; it outlives this callback.
225    let ctx = unsafe { &*(context.cast::<StreamContext>()) };
226    ctx.capturing.store(false, Ordering::Release);
227
228    let message = if msg.is_null() {
229        "Unknown error".to_string()
230    } else {
231        // Best-effort: if Swift sent a non-UTF-8 buffer, fall back to a
232        // placeholder rather than panicking.
233        unsafe { CStr::from_ptr(msg) }
234            .to_str()
235            .unwrap_or("Unknown error")
236            .to_string()
237    };
238
239    let error = if error_code != 0 {
240        crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
241            || SCError::StreamError(format!("{message} (code: {error_code})")),
242            |code| SCError::SCStreamError {
243                code,
244                message: Some(message.clone()),
245            },
246        )
247    } else {
248        SCError::StreamError(message)
249    };
250
251    let Some(delegate) = ctx.delegate_snapshot() else {
252        eprintln!("SCStream error: {error}");
253        return;
254    };
255
256    // ScreenCaptureKit reports stops only through `stream(_:didStopWithError:)`,
257    // so we dispatch the single canonical `did_stop_with_error` callback.
258    // Wrap user code in catch_unwind so a panic never propagates into Swift.
259    catch_user_panic("delegate.did_stop_with_error", || {
260        delegate.did_stop_with_error(error);
261    });
262}
263
264// C callback for the remaining `SCStreamDelegate` lifecycle events. The event
265// codes are defined alongside the trampoline in Stream.swift; the two lists
266// must stay in sync.
267extern "C" fn delegate_event_callback(context: *mut c_void, event: i32) {
268    if context.is_null() {
269        return;
270    }
271    // SAFETY: `context` is the +1-retained StreamContext pointer the Swift
272    // bridge stored via context_retain_cb; it outlives this callback.
273    let ctx = unsafe { &*(context.cast::<StreamContext>()) };
274
275    let Some(delegate) = ctx.delegate_snapshot() else {
276        return;
277    };
278
279    catch_user_panic("delegate lifecycle event", || match event {
280        0 => delegate.stream_did_become_active(),
281        1 => delegate.stream_did_become_inactive(),
282        2 => delegate.output_video_effect_did_start_for_stream(),
283        3 => delegate.output_video_effect_did_stop_for_stream(),
284        other => eprintln!("SCStream: unknown delegate event code {other}"),
285    });
286}
287
288// C callback for sample buffers — dispatches to per-stream handlers via context pointer.
289//
290// Safety: this function is called from Swift on a dispatch queue. A Rust
291// panic across the C ABI is UB; every user handler invocation is wrapped in
292// `catch_unwind`. The handler `Arc`s are cloned out under a short read lock
293// and dispatched with the lock released, so a handler is free to call back
294// into `add_output_handler` / `remove_output_handler` (which need the write
295// lock) without deadlocking, and a slow handler never blocks registration.
296// The `passRetained` `CMSampleBuffer` reference Swift hands us is consumed
297// exactly once: each non-final matching handler receives a freshly retained
298// clone, and the final matching handler consumes the original.
299extern "C" fn sample_handler(context: *mut c_void, sample_buffer: *const c_void, output_type: i32) {
300    if sample_buffer.is_null() {
301        return;
302    }
303    if context.is_null() {
304        unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
305        return;
306    }
307    // SAFETY: `context` is the +1-retained StreamContext pointer the Swift
308    // bridge stored via context_retain_cb; it outlives this callback.
309    let ctx = unsafe { &*(context.cast::<StreamContext>()) };
310
311    let output_type_enum = match output_type {
312        0 => SCStreamOutputType::Screen,
313        1 => SCStreamOutputType::Audio,
314        2 => SCStreamOutputType::Microphone,
315        _ => {
316            eprintln!("Unknown output type: {output_type}");
317            unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
318            return;
319        }
320    };
321
322    let matching = ctx.handler_snapshot(output_type_enum);
323
324    if matching.is_empty() {
325        unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
326        return;
327    }
328
329    let last = matching.len() - 1;
330    for (index, handler) in matching.iter().enumerate() {
331        // Retain for every handler except the last; the last handler consumes
332        // the original `passRetained` reference Swift gave us.
333        if index != last {
334            unsafe { crate::cm::ffi::cm_sample_buffer_retain(sample_buffer.cast_mut()) };
335        }
336
337        let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(sample_buffer.cast_mut()) };
338
339        // Wrap user code in catch_unwind so panics never propagate into Swift.
340        // If the handler panics, `buffer` is dropped on unwind, which calls
341        // `cm_sample_buffer_release` and balances the retain we just did
342        // (or, for the last handler, balances the original `passRetained`).
343        // The retain/release accounting is preserved either way.
344        catch_user_panic("output handler", || {
345            handler.did_output_sample_buffer(buffer, output_type_enum);
346        });
347    }
348}
349
350/// Stable, non-owning identity for an [`SCStream`].
351///
352/// This is the address of the underlying native stream, stored as an opaque
353/// value. It does not keep the stream alive and must never be dereferenced.
354/// Compare it with [`SCStream::identity`] while the stream is still live.
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
356pub struct StreamIdentity(NonZeroUsize);
357
358impl StreamIdentity {
359    pub(crate) fn from_ptr(ptr: *const c_void) -> Option<Self> {
360        NonZeroUsize::new(ptr as usize).map(Self)
361    }
362
363    /// Whether this identity belongs to `stream`.
364    #[must_use]
365    pub fn matches(self, stream: &SCStream) -> bool {
366        self == stream.identity()
367    }
368}
369
370/// `SCStream` is a lightweight wrapper around the Swift `SCStream` instance.
371/// It provides direct FFI access to `ScreenCaptureKit` functionality.
372///
373/// This is the primary and only implementation of `SCStream` in v1.0+.
374/// All `ScreenCaptureKit` operations go through Swift FFI bindings.
375///
376/// # Examples
377///
378/// ```no_run
379/// use screencapturekit::prelude::*;
380///
381/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
382/// // Get shareable content
383/// let content = SCShareableContent::get()?;
384/// let display = &content.displays()[0];
385///
386/// // Create filter and configuration
387/// let filter = SCContentFilter::create()
388///     .with_display(display)
389///     .with_excluding_windows(&[])
390///     .build()?;
391/// let config = SCStreamConfiguration::new()
392///     .with_width(1920)
393///     .with_height(1080);
394///
395/// // Create and start stream
396/// let mut stream = SCStream::new(&filter, &config)?;
397/// stream.start_capture()?;
398///
399/// // ... capture frames ...
400///
401/// stream.stop_capture()?;
402/// # Ok(())
403/// # }
404/// ```
405pub struct SCStream {
406    ptr: *const c_void,
407    identity: StreamIdentity,
408    /// Per-stream context holding handlers and delegate (ref-counted).
409    context: *mut StreamContext,
410}
411
412unsafe impl Send for SCStream {}
413unsafe impl Sync for SCStream {}
414
415impl SCStream {
416    /// Create a new stream with a content filter and configuration
417    ///
418    /// # Examples
419    ///
420    /// ```no_run
421    /// use screencapturekit::prelude::*;
422    ///
423    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
424    /// let content = SCShareableContent::get()?;
425    /// let display = &content.displays()[0];
426    /// let filter = SCContentFilter::create()
427    ///     .with_display(display)
428    ///     .with_excluding_windows(&[])
429    ///     .build()?;
430    /// let config = SCStreamConfiguration::new()
431    ///     .with_width(1920)
432    ///     .with_height(1080);
433    ///
434    /// let stream = SCStream::new(&filter, &config)?;
435    /// # Ok(())
436    /// # }
437    /// ```
438    #[allow(clippy::missing_errors_doc)]
439    pub fn new(filter: &SCContentFilter, configuration: &SCStreamConfiguration) -> SCResult<Self> {
440        Self::create(filter, configuration, None)
441    }
442
443    /// Create a new stream with a content filter, configuration, and delegate
444    ///
445    /// The delegate receives callbacks for stream lifecycle events. The key
446    /// one is [`did_stop_with_error`](crate::stream::delegate_trait::SCStreamDelegateTrait::did_stop_with_error),
447    /// invoked when `ScreenCaptureKit` stops the stream with an error (e.g. the
448    /// captured window closes or permission is revoked). A *clean* stop you
449    /// requested via [`stop_capture`](Self::stop_capture) is observed through
450    /// that call's return value, not the delegate.
451    ///
452    /// # Examples
453    ///
454    /// ```no_run
455    /// use screencapturekit::prelude::*;
456    /// use screencapturekit::stream::delegate_trait::StreamCallbacks;
457    ///
458    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
459    /// let content = SCShareableContent::get()?;
460    /// let display = &content.displays()[0];
461    /// let filter = SCContentFilter::create()
462    ///     .with_display(display)
463    ///     .with_excluding_windows(&[])
464    ///     .build()?;
465    /// let config = SCStreamConfiguration::new()
466    ///     .with_width(1920)
467    ///     .with_height(1080);
468    ///
469    /// let delegate = StreamCallbacks::new()
470    ///     .on_error(|e| eprintln!("Stream stopped with error: {}", e));
471    ///
472    /// let stream = SCStream::new_with_delegate(&filter, &config, delegate)?;
473    /// stream.start_capture()?;
474    /// # Ok(())
475    /// # }
476    /// ```
477    #[allow(clippy::missing_errors_doc)]
478    pub fn new_with_delegate(
479        filter: &SCContentFilter,
480        configuration: &SCStreamConfiguration,
481        delegate: impl SCStreamDelegateTrait + 'static,
482    ) -> SCResult<Self> {
483        Self::create(filter, configuration, Some(Arc::new(delegate)))
484    }
485
486    fn create(
487        filter: &SCContentFilter,
488        configuration: &SCStreamConfiguration,
489        delegate: Option<Arc<dyn SCStreamDelegateTrait>>,
490    ) -> SCResult<Self> {
491        let context = StreamContext::new(delegate);
492        let context_ptr = context.cast::<c_void>();
493        let configuration = configuration.clone();
494
495        let ptr = unsafe {
496            ffi::sc_stream_create(
497                filter.as_ptr(),
498                configuration.as_ptr(),
499                context_ptr,
500                delegate_error_callback,
501                sample_handler,
502                context_retain_cb,
503                context_release_cb,
504            )
505        };
506
507        unsafe { Self::adopt(ptr, context) }
508    }
509
510    unsafe fn adopt(ptr: *const c_void, context: *mut StreamContext) -> SCResult<Self> {
511        let Some(identity) = StreamIdentity::from_ptr(ptr) else {
512            unsafe { StreamContext::release(context) };
513            return Err(SCError::null_pointer(
514                "ScreenCaptureKit returned no SCStream",
515            ));
516        };
517
518        // Wire up the remaining delegate callbacks (active / inactive / video
519        // effect start / stop). Registration is unconditional: a delegate can
520        // be present from the start, and the Rust trampoline is a no-op when
521        // there isn't one.
522        unsafe { ffi::sc_stream_set_delegate_event_callback(ptr, delegate_event_callback) };
523
524        Ok(Self {
525            ptr,
526            identity,
527            context,
528        })
529    }
530
531    /// Add an output handler to receive captured frames
532    ///
533    /// # Arguments
534    ///
535    /// * `handler` - The handler to receive callbacks. Can be:
536    ///   - A struct implementing [`SCStreamOutputTrait`]
537    ///   - A closure `|CMSampleBuffer, SCStreamOutputType| { ... }`
538    /// * `of_type` - The type of output to receive (Screen, Audio, or Microphone)
539    ///
540    /// # Returns
541    ///
542    /// Returns the handler ID, which can be used with
543    /// [`remove_output_handler`](Self::remove_output_handler).
544    ///
545    /// # Errors
546    ///
547    /// Returns [`SCError::StreamError`] if `ScreenCaptureKit` rejected the
548    /// registration (e.g. the output type is not enabled by the stream
549    /// configuration), and [`SCError::InvalidConfiguration`] for the requests
550    /// [`add_output_handler_with_queue`](Self::add_output_handler_with_queue)
551    /// refuses.
552    ///
553    /// # Dispatch queue
554    ///
555    /// The handler is invoked on the queue already established for `of_type`,
556    /// or — for the first handler of that type — on a dedicated
557    /// user-interactive serial dispatch queue created by the bridge. This
558    /// intentionally **deviates from
559    /// Apple's `SCStream.addStreamOutput`** API, whose `nil` queue parameter
560    /// means "deliver on the main queue". Main-queue dispatch only works
561    /// when the host process runs a Cocoa runloop, which Rust apps
562    /// generally don't, so the default would otherwise silently drop
563    /// every frame. Use [`add_output_handler_with_queue`](Self::add_output_handler_with_queue)
564    /// and pass an explicit [`DispatchQueue`] (e.g. one wrapping main) if
565    /// you need a different queue — including AppKit/UIKit affinity.
566    ///
567    /// # Examples
568    ///
569    /// Using a struct:
570    /// ```rust,no_run
571    /// use screencapturekit::prelude::*;
572    ///
573    /// struct MyHandler;
574    /// impl SCStreamOutputTrait for MyHandler {
575    ///     fn did_output_sample_buffer(&self, _sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
576    ///         println!("Got frame!");
577    ///     }
578    /// }
579    ///
580    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
581    /// # let content = SCShareableContent::get()?;
582    /// # let display = &content.displays()[0];
583    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
584    /// # let config = SCStreamConfiguration::default();
585    /// let mut stream = SCStream::new(&filter, &config)?;
586    /// stream.add_output_handler(MyHandler, SCStreamOutputType::Screen)?;
587    /// # Ok(())
588    /// # }
589    /// ```
590    ///
591    /// Using a closure:
592    /// ```rust,no_run
593    /// use screencapturekit::prelude::*;
594    ///
595    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
596    /// # let content = SCShareableContent::get()?;
597    /// # let display = &content.displays()[0];
598    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
599    /// # let config = SCStreamConfiguration::default();
600    /// let mut stream = SCStream::new(&filter, &config)?;
601    /// stream.add_output_handler(
602    ///     |_sample, _type| println!("Got frame!"),
603    ///     SCStreamOutputType::Screen
604    /// )?;
605    /// # Ok(())
606    /// # }
607    /// ```
608    ///
609    /// # Sharing state with handlers
610    ///
611    /// The handler bound is `impl SCStreamOutputTrait + 'static`. The
612    /// `'static` is required because the handler is stored inside
613    /// `SCStream` which can outlive any borrowed reference. Combined
614    /// with the trait's `Send + Sync` bound (callbacks run on
615    /// independent dispatch queues, see [`SCStreamOutputTrait`]),
616    /// the canonical pattern for sharing state with a handler is to
617    /// wrap it in `Arc<Mutex<T>>` (or `Arc<AtomicXxx>` for primitives):
618    ///
619    /// ```rust,no_run
620    /// use screencapturekit::prelude::*;
621    /// use std::sync::{Arc, Mutex, atomic::{AtomicUsize, Ordering}};
622    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
623    /// # let content = SCShareableContent::get()?;
624    /// # let display = &content.displays()[0];
625    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
626    /// # let config = SCStreamConfiguration::default();
627    /// let frame_count = Arc::new(AtomicUsize::new(0));
628    /// let count_handler = frame_count.clone();
629    /// let mut stream = SCStream::new(&filter, &config)?;
630    /// stream.add_output_handler(
631    ///     move |_sample, _type| {
632    ///         count_handler.fetch_add(1, Ordering::Relaxed);
633    ///     },
634    ///     SCStreamOutputType::Screen,
635    /// )?;
636    /// // outer scope can still read frame_count any time:
637    /// println!("frames so far: {}", frame_count.load(Ordering::Relaxed));
638    /// # Ok(())
639    /// # }
640    /// ```
641    pub fn add_output_handler(
642        &mut self,
643        handler: impl SCStreamOutputTrait + 'static,
644        of_type: SCStreamOutputType,
645    ) -> Result<usize, SCError> {
646        self.add_output_handler_with_queue(handler, of_type, None)
647    }
648
649    /// Add an output handler with a custom dispatch queue
650    ///
651    /// This allows controlling which thread/queue the handler is called on.
652    ///
653    /// # Arguments
654    ///
655    /// * `handler` - The handler to receive callbacks
656    /// * `of_type` - The type of output to receive
657    /// * `queue` - Optional custom dispatch queue for callbacks
658    ///
659    /// # One queue per output type
660    ///
661    /// The bridge registers a single native `SCStreamOutput` object per output
662    /// type, so `ScreenCaptureKit` delivers **all** handlers of a given type on
663    /// **one** queue — the one established by the first successful registration
664    /// for that type. Consequences:
665    ///
666    /// - Adding a further handler for the same type with `queue: None` is fine:
667    ///   it joins the established queue.
668    /// - Adding a further handler for the same type with a *different* explicit
669    ///   queue is rejected with [`SCError::InvalidConfiguration`], rather than
670    ///   silently delivering on a queue you did not ask for — that would break
671    ///   handlers written around thread affinity.
672    /// - Removing the last handler of a type also tears down the native output,
673    ///   so the next registration for that type is free to pick a new queue.
674    ///
675    /// # Examples
676    ///
677    /// ```rust,no_run
678    /// use screencapturekit::prelude::*;
679    /// use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
680    ///
681    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
682    /// # let content = SCShareableContent::get()?;
683    /// # let display = &content.displays()[0];
684    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
685    /// # let config = SCStreamConfiguration::default();
686    /// let mut stream = SCStream::new(&filter, &config)?;
687    /// let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
688    ///
689    /// stream.add_output_handler_with_queue(
690    ///     |_sample, _type| println!("Got frame on custom queue!"),
691    ///     SCStreamOutputType::Screen,
692    ///     Some(&queue)
693    /// )?;
694    /// # Ok(())
695    /// # }
696    /// ```
697    #[allow(clippy::missing_errors_doc)]
698    pub fn add_output_handler_with_queue(
699        &mut self,
700        handler: impl SCStreamOutputTrait + 'static,
701        of_type: SCStreamOutputType,
702        queue: Option<&DispatchQueue>,
703    ) -> Result<usize, SCError> {
704        #[cfg(not(feature = "macos_15_0"))]
705        if of_type == SCStreamOutputType::Microphone {
706            return Err(SCError::invalid_config(
707                "microphone output requires the macos_15_0 feature",
708            ));
709        }
710
711        let requested = queue.map_or(OutputQueue::BridgeDefault, |q| {
712            OutputQueue::Custom(q.as_ptr() as usize)
713        });
714
715        // SAFETY: self.context is the Box::into_raw StreamContext created in
716        // SCStream::create; it stays valid for the lifetime of self (released
717        // only in Drop, after this method returns).
718        let ctx = unsafe { &*self.context };
719        let _mutation_guard = ctx
720            .output_mutation
721            .lock()
722            .unwrap_or_else(std::sync::PoisonError::into_inner);
723
724        let mut established = ctx
725            .output_queues
726            .write()
727            .unwrap_or_else(std::sync::PoisonError::into_inner);
728
729        if let Some(&(_, existing)) = established.iter().find(|(ty, _)| *ty == of_type) {
730            if queue.is_some() && existing != requested {
731                drop(established);
732                return Err(SCError::invalid_config(format!(
733                    "refusing to add a {of_type:?} handler on a different dispatch queue — \
734                     ScreenCaptureKit delivers every handler of one output type on the queue \
735                     chosen by the first registration. Reuse that queue (pass None) or remove \
736                     the existing {of_type:?} handlers first."
737                )));
738            }
739        }
740
741        let output_type_int = native_output_type(of_type);
742
743        let ok = if let Some(q) = queue {
744            unsafe {
745                ffi::sc_stream_add_stream_output_with_queue(self.ptr, output_type_int, q.as_ptr())
746            }
747        } else {
748            unsafe { ffi::sc_stream_add_stream_output(self.ptr, output_type_int) }
749        };
750
751        if !ok {
752            drop(established);
753            return Err(SCError::StreamError(format!(
754                "failed to register output handler for {of_type:?} \
755                 (ScreenCaptureKit rejected addStreamOutput)"
756            )));
757        }
758
759        if !established.iter().any(|(ty, _)| *ty == of_type) {
760            established.push((of_type, requested));
761        }
762        drop(established);
763
764        let handler_id = NEXT_HANDLER_ID.fetch_add(1, Ordering::Relaxed);
765        ctx.handlers
766            .write()
767            .unwrap_or_else(std::sync::PoisonError::into_inner)
768            .push(HandlerEntry {
769                id: handler_id,
770                of_type,
771                handler: Arc::new(handler),
772            });
773        Ok(handler_id)
774    }
775
776    /// Remove an output handler
777    ///
778    /// # Arguments
779    ///
780    /// * `id` - The handler ID returned from [`add_output_handler`](Self::add_output_handler)
781    /// * `of_type` - The type of output the handler was registered for
782    ///
783    /// # Returns
784    ///
785    /// Returns `Ok(true)` if a handler with this `id` **and** output type was
786    /// found and removed, and `Ok(false)` if there was no such handler.
787    ///
788    /// # Errors
789    ///
790    /// Returns [`SCError::StreamError`] when the handler was removed from this
791    /// stream but `removeStreamOutput` failed on the `ScreenCaptureKit` side.
792    /// The handler stops receiving samples either way; a native teardown
793    /// failure only means `ScreenCaptureKit` keeps delivering samples that the
794    /// bridge then discards.
795    pub fn remove_output_handler(
796        &mut self,
797        id: usize,
798        of_type: SCStreamOutputType,
799    ) -> Result<bool, SCError> {
800        // SAFETY: self.context is the Box::into_raw StreamContext created in
801        // SCStream::create; it stays valid for the lifetime of self.
802        let ctx = unsafe { &*self.context };
803        let mutation_guard = ctx
804            .output_mutation
805            .lock()
806            .unwrap_or_else(std::sync::PoisonError::into_inner);
807
808        let mut handlers = ctx
809            .handlers
810            .write()
811            .unwrap_or_else(std::sync::PoisonError::into_inner);
812        // Match on the output type too: the same id registered for a different
813        // type must not be silently removed by a mismatched call, which would
814        // also detach the wrong native output.
815        let Some(pos) = handlers
816            .iter()
817            .position(|e| e.id == id && e.of_type == of_type)
818        else {
819            return Ok(false);
820        };
821        let removed_handler = handlers.remove(pos);
822
823        // If no more handlers for this output type, tell Swift to remove the output
824        let has_type = handlers.iter().any(|e| e.of_type == of_type);
825        drop(handlers);
826
827        let result = if has_type {
828            Ok(true)
829        } else {
830            let removed = unsafe {
831                ffi::sc_stream_remove_stream_output(self.ptr, native_output_type(of_type))
832            };
833
834            if removed {
835                // The next registration for this type may choose a fresh queue.
836                ctx.output_queues
837                    .write()
838                    .unwrap_or_else(std::sync::PoisonError::into_inner)
839                    .retain(|(ty, _)| *ty != of_type);
840                Ok(true)
841            } else {
842                Err(SCError::StreamError(format!(
843                    "ScreenCaptureKit rejected removeStreamOutput for {of_type:?}; the handler was \
844                     detached but the native output is still registered"
845                )))
846            }
847        };
848
849        drop(mutation_guard);
850        drop(removed_handler);
851        result
852    }
853
854    /// Start capturing screen content
855    ///
856    /// This method blocks until the capture operation completes or fails.
857    ///
858    /// # Errors
859    ///
860    /// Returns `SCError::CaptureStartFailed` if the capture fails to start.
861    pub fn start_capture(&self) -> Result<(), SCError> {
862        let context = unsafe { &*self.context };
863        if !claim_start(&context.capturing, &context.start_unconfirmed) {
864            return Ok(());
865        }
866        let (completion, context) = UnitCompletion::new();
867        unsafe { ffi::sc_stream_start_capture(self.ptr, context, UnitCompletion::callback) };
868        match completion.wait() {
869            Ok(()) => Ok(()),
870            Err(error) => {
871                let context = unsafe { &*self.context };
872                if is_timeout_error(&error) {
873                    // The native start is still outstanding, so clearing
874                    // `capturing` would let a retry double-start. Record that
875                    // the outcome is unconfirmed instead, so the next start
876                    // reissues rather than reporting a success nobody observed.
877                    context.start_unconfirmed.store(true, Ordering::Release);
878                } else {
879                    context.capturing.store(false, Ordering::Release);
880                }
881                Err(SCError::CaptureStartFailed(error))
882            }
883        }
884    }
885
886    /// Stop capturing screen content
887    ///
888    /// This method blocks until the capture operation completes or fails.
889    ///
890    /// # Errors
891    ///
892    /// Returns `SCError::CaptureStopFailed` if the capture fails to stop.
893    pub fn stop_capture(&self) -> Result<(), SCError> {
894        let context = unsafe { &*self.context };
895        if !context.capturing.swap(false, Ordering::AcqRel) {
896            return Ok(());
897        }
898        let (completion, context) = UnitCompletion::new();
899        unsafe { ffi::sc_stream_stop_capture(self.ptr, context, UnitCompletion::callback) };
900        if let Err(error) = completion.wait() {
901            unsafe { &*self.context }
902                .capturing
903                .store(true, Ordering::Release);
904            return Err(SCError::CaptureStopFailed(error));
905        }
906        Ok(())
907    }
908
909    /// Update the stream configuration
910    ///
911    /// This method blocks until the configuration update completes or fails.
912    ///
913    /// # Errors
914    ///
915    /// Returns `SCError::StreamError` if the configuration update fails.
916    #[cfg(feature = "macos_14_0")]
917    pub fn update_configuration(
918        &self,
919        configuration: &SCStreamConfiguration,
920    ) -> Result<(), SCError> {
921        let configuration = configuration.clone();
922        let (completion, context) = UnitCompletion::new();
923        unsafe {
924            ffi::sc_stream_update_configuration(
925                self.ptr,
926                configuration.as_ptr(),
927                context,
928                UnitCompletion::callback,
929            );
930        }
931        completion.wait().map_err(SCError::StreamError)
932    }
933
934    /// Update the content filter
935    ///
936    /// This method blocks until the filter update completes or fails.
937    ///
938    /// # Errors
939    ///
940    /// Returns `SCError::StreamError` if the filter update fails.
941    pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
942        let (completion, context) = UnitCompletion::new();
943        unsafe {
944            ffi::sc_stream_update_content_filter(
945                self.ptr,
946                filter.as_ptr(),
947                context,
948                UnitCompletion::callback,
949            );
950        }
951        completion.wait().map_err(SCError::StreamError)
952    }
953
954    /// Get the synchronization clock for this stream (macOS 13.0+)
955    ///
956    /// Returns the `CMClock` used to synchronize the stream's output.
957    /// This is useful for coordinating multiple streams or synchronizing
958    /// with other media.
959    ///
960    /// Returns `None` if the clock is not available (e.g., stream not started
961    /// or macOS version too old).
962    #[cfg(feature = "macos_13_0")]
963    pub fn synchronization_clock(&self) -> Option<crate::cm::CMClock> {
964        let ptr = unsafe { ffi::sc_stream_get_synchronization_clock(self.ptr) };
965        // SAFETY: the Swift thunk transfers a +1 retained clock reference.
966        #[allow(unused_unsafe)]
967        unsafe {
968            crate::cm::CMClock::from_raw(ptr)
969        }
970    }
971
972    /// Add a recording output to the stream (macOS 15.0+)
973    ///
974    /// Starts recording if the stream is already capturing, otherwise recording
975    /// will start when capture begins. The recording is written to the file URL
976    /// specified in the `SCRecordingOutputConfiguration`.
977    ///
978    /// # Errors
979    ///
980    /// Returns `SCError::StreamError` if adding the recording output fails.
981    #[cfg(feature = "macos_15_0")]
982    pub fn add_recording_output(
983        &self,
984        recording_output: &crate::recording_output::SCRecordingOutput,
985    ) -> Result<(), SCError> {
986        let stream_context = unsafe { &*self.context };
987        let (completion, context) = UnitCompletion::new();
988        unsafe {
989            ffi::sc_stream_add_recording_output(
990                self.ptr,
991                recording_output.as_ptr(),
992                UnitCompletion::callback,
993                context,
994            );
995        }
996        completion.wait().map_err(SCError::StreamError)?;
997        stream_context
998            .recording_outputs
999            .fetch_add(1, Ordering::AcqRel);
1000        Ok(())
1001    }
1002
1003    /// Remove a recording output from the stream (macOS 15.0+)
1004    ///
1005    /// Stops recording if the stream is currently recording.
1006    ///
1007    /// # Errors
1008    ///
1009    /// Returns `SCError::StreamError` if removing the recording output fails.
1010    #[cfg(feature = "macos_15_0")]
1011    pub fn remove_recording_output(
1012        &self,
1013        recording_output: &crate::recording_output::SCRecordingOutput,
1014    ) -> Result<(), SCError> {
1015        let context = unsafe { &*self.context };
1016        if context.capturing.load(Ordering::Acquire)
1017            && !context.has_handlers()
1018            && context.recording_outputs.load(Ordering::Acquire) == 1
1019        {
1020            self.stop_capture()?;
1021            let (completion, completion_context) = UnitCompletion::new();
1022            unsafe {
1023                ffi::sc_recording_output_wait_until_terminal(
1024                    recording_output.as_ptr(),
1025                    completion_context,
1026                    UnitCompletion::callback,
1027                );
1028            }
1029            completion.wait().map_err(SCError::StreamError)?;
1030        }
1031        let (completion, completion_context) = UnitCompletion::new();
1032        unsafe {
1033            ffi::sc_stream_remove_recording_output(
1034                self.ptr,
1035                recording_output.as_ptr(),
1036                UnitCompletion::callback,
1037                completion_context,
1038            );
1039        }
1040        let outcome = completion.wait();
1041        // Swift reports failure only when the native removal itself threw; a
1042        // timeout can therefore only expire while waiting for the movie to
1043        // finalize, by which point the output is already gone. Leaving the
1044        // count inflated there would permanently disable the stop-and-flush
1045        // branch above for whichever outputs remain.
1046        let removed = match &outcome {
1047            Ok(()) => true,
1048            Err(error) => is_timeout_error(error),
1049        };
1050        if removed {
1051            context
1052                .recording_outputs
1053                .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
1054                    Some(count.saturating_sub(1))
1055                })
1056                .ok();
1057        }
1058        outcome.map_err(SCError::StreamError)?;
1059        Ok(())
1060    }
1061
1062    /// Returns the raw pointer to the underlying Swift `SCStream` instance.
1063    #[allow(dead_code)]
1064    pub(crate) fn as_ptr(&self) -> *const c_void {
1065        self.ptr
1066    }
1067
1068    /// Return a stable, non-owning identity for this stream.
1069    #[must_use]
1070    pub fn identity(&self) -> StreamIdentity {
1071        self.identity
1072    }
1073
1074    #[cfg(feature = "async")]
1075    pub(crate) fn capture_state(&self) -> Arc<AtomicBool> {
1076        Arc::clone(&unsafe { &*self.context }.capturing)
1077    }
1078
1079    #[cfg(feature = "async")]
1080    pub(crate) fn start_unconfirmed_state(&self) -> Arc<AtomicBool> {
1081        Arc::clone(&unsafe { &*self.context }.start_unconfirmed)
1082    }
1083}
1084
1085/// Claims the right to issue a native `startCapture`.
1086///
1087/// Returns `false` when the stream is already capturing and the previous start
1088/// was confirmed, so the caller can report success without a redundant FFI
1089/// call. `start_unconfirmed` is always cleared, since a flag left set past a
1090/// successful start would later reissue a start on a live stream.
1091pub(crate) fn claim_start(capturing: &AtomicBool, start_unconfirmed: &AtomicBool) -> bool {
1092    let was_capturing = capturing.swap(true, Ordering::AcqRel);
1093    let unconfirmed = start_unconfirmed.swap(false, Ordering::AcqRel);
1094    !was_capturing || unconfirmed
1095}
1096
1097impl Drop for SCStream {
1098    // Safety / teardown ordering:
1099    //
1100    // `sc_stream_release` drops this handle's claim on the Swift-side
1101    // `StreamState` and releases the `SCStream`. The `StreamState` — and with
1102    // it the stream-output delegate — is discarded only when the *last* handle
1103    // (this stream plus every clone) is gone, so dropping one clone never
1104    // detaches the callbacks of the survivors. We release the `StreamContext`
1105    // afterwards, so the ordering here is release-stream-then-release-context.
1106    //
1107    // In-flight callbacks are safe even though Apple's stop is asynchronous: the
1108    // Swift `StreamDelegateWrapper` and `StreamOutputHandler` objects each hold
1109    // their own +1 reference on the `StreamContext` (taken in `init` via
1110    // `context_retain_cb`, dropped in `deinit` via `context_release_cb`). Each
1111    // callback runs as a method on one of those objects, and ARC keeps that
1112    // object (`self`) alive for the duration of the call, so its context
1113    // reference is also held for the duration of the call. Therefore a callback
1114    // already in flight can never observe a freed context: the final
1115    // `Box::from_raw` only happens once every holder — this Rust `SCStream` and
1116    // both Swift bridge objects — has released its reference.
1117    //
1118    // Refcount accounting: `StreamContext::new` starts at 1; the Swift
1119    // `createStream` adds +1 per bridge object (delegate + output handler) = 3;
1120    // each Rust clone adds +1; each `drop` removes -1; each bridge object's
1121    // `deinit` removes -1, and the context is freed when the total reaches 0.
1122    fn drop(&mut self) {
1123        unsafe { ffi::sc_stream_release(self.ptr) };
1124        unsafe { StreamContext::release(self.context) };
1125    }
1126}
1127
1128impl Clone for SCStream {
1129    /// Clone the stream reference.
1130    ///
1131    /// Cloning an `SCStream` creates a new reference to the same underlying
1132    /// Swift `SCStream` object. The cloned stream shares the same handlers
1133    /// as the original — they receive frames from the same capture session.
1134    ///
1135    /// Both the original and cloned stream share the same capture state, so:
1136    /// - Starting capture on one affects both
1137    /// - Stopping capture on one affects both
1138    /// - Configuration updates affect both
1139    /// - Handlers receive the same frames
1140    /// - Dropping one clone leaves the others fully functional; capture and
1141    ///   delegate callbacks stop only once the last clone is dropped
1142    ///
1143    /// # Examples
1144    ///
1145    /// ```rust,no_run
1146    /// use screencapturekit::prelude::*;
1147    ///
1148    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1149    /// # let content = SCShareableContent::get()?;
1150    /// # let display = &content.displays()[0];
1151    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build()?;
1152    /// # let config = SCStreamConfiguration::default();
1153    /// let mut stream = SCStream::new(&filter, &config)?;
1154    /// stream.add_output_handler(|_, _| println!("Handler 1"), SCStreamOutputType::Screen)?;
1155    ///
1156    /// // Clone shares the same handlers
1157    /// let stream2 = stream.clone();
1158    /// // Both stream and stream2 will receive frames via Handler 1
1159    /// # Ok(())
1160    /// # }
1161    /// ```
1162    fn clone(&self) -> Self {
1163        unsafe { StreamContext::retain(self.context) };
1164
1165        Self {
1166            ptr: unsafe { crate::ffi::sc_stream_retain(self.ptr) },
1167            identity: self.identity,
1168            context: self.context,
1169        }
1170    }
1171}
1172
1173impl fmt::Debug for SCStream {
1174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1175        f.debug_struct("SCStream")
1176            .field("ptr", &self.ptr)
1177            .finish_non_exhaustive()
1178    }
1179}
1180
1181impl fmt::Display for SCStream {
1182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1183        write!(f, "SCStream")
1184    }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189    use super::*;
1190    use std::sync::atomic::AtomicUsize;
1191    use std::sync::Arc;
1192
1193    /// Regression test for #135: multiple concurrent streams must not leak
1194    /// samples across each other.
1195    ///
1196    /// Creates two independent `StreamContexts` with separate handlers and
1197    /// directly invokes each context's handlers. Verifies that each handler
1198    /// only receives calls routed through its own context — not from the
1199    /// other context. With the old global `HANDLER_REGISTRY`, both handlers
1200    /// would have been called for every callback regardless of context.
1201    #[test]
1202    fn test_per_stream_callback_isolation() {
1203        let count_a = Arc::new(AtomicUsize::new(0));
1204        let count_b = Arc::new(AtomicUsize::new(0));
1205
1206        // Create two independent contexts (simulates two SCStream instances)
1207        let ctx_a = StreamContext::new(None);
1208        let ctx_b = StreamContext::new(None);
1209
1210        // Register an audio handler on context A
1211        {
1212            let counter = count_a.clone();
1213            let mut handlers = unsafe { &*ctx_a }
1214                .handlers
1215                .write()
1216                .unwrap_or_else(std::sync::PoisonError::into_inner);
1217            handlers.push(HandlerEntry {
1218                id: 1,
1219                of_type: SCStreamOutputType::Audio,
1220                handler: Arc::new(
1221                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1222                        counter.fetch_add(1, Ordering::Relaxed);
1223                        // Prevent Drop from calling cm_sample_buffer_release on our fake pointer
1224                        std::mem::forget(buf);
1225                    },
1226                ),
1227            });
1228        }
1229
1230        // Register an audio handler on context B
1231        {
1232            let counter = count_b.clone();
1233            let mut handlers = unsafe { &*ctx_b }
1234                .handlers
1235                .write()
1236                .unwrap_or_else(std::sync::PoisonError::into_inner);
1237            handlers.push(HandlerEntry {
1238                id: 2,
1239                of_type: SCStreamOutputType::Audio,
1240                handler: Arc::new(
1241                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1242                        counter.fetch_add(1, Ordering::Relaxed);
1243                        std::mem::forget(buf);
1244                    },
1245                ),
1246            });
1247        }
1248
1249        // Simulate 5 audio callbacks on context A by directly calling matching handlers
1250        for _ in 0..5 {
1251            let handlers = unsafe { &*ctx_a }
1252                .handlers
1253                .write()
1254                .unwrap_or_else(std::sync::PoisonError::into_inner);
1255            for entry in handlers
1256                .iter()
1257                .filter(|e| e.of_type == SCStreamOutputType::Audio)
1258            {
1259                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1260                entry
1261                    .handler
1262                    .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
1263            }
1264        }
1265
1266        // Simulate 3 audio callbacks on context B
1267        for _ in 0..3 {
1268            let handlers = unsafe { &*ctx_b }
1269                .handlers
1270                .write()
1271                .unwrap_or_else(std::sync::PoisonError::into_inner);
1272            for entry in handlers
1273                .iter()
1274                .filter(|e| e.of_type == SCStreamOutputType::Audio)
1275            {
1276                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1277                entry
1278                    .handler
1279                    .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
1280            }
1281        }
1282
1283        // Handler A must have received exactly 5 — not 8
1284        assert_eq!(
1285            count_a.load(Ordering::Relaxed),
1286            5,
1287            "handler A received callbacks meant for B (cross-stream leak)"
1288        );
1289        // Handler B must have received exactly 3 — not 8
1290        assert_eq!(
1291            count_b.load(Ordering::Relaxed),
1292            3,
1293            "handler B received callbacks meant for A (cross-stream leak)"
1294        );
1295
1296        unsafe {
1297            StreamContext::release(ctx_a);
1298            StreamContext::release(ctx_b);
1299        }
1300    }
1301
1302    /// Verify that handlers are filtered by output type within a single context.
1303    #[test]
1304    fn test_handler_output_type_filtering() {
1305        let screen_count = Arc::new(AtomicUsize::new(0));
1306        let audio_count = Arc::new(AtomicUsize::new(0));
1307
1308        let ctx = StreamContext::new(None);
1309
1310        {
1311            let counter = screen_count.clone();
1312            let mut handlers = unsafe { &*ctx }
1313                .handlers
1314                .write()
1315                .unwrap_or_else(std::sync::PoisonError::into_inner);
1316            handlers.push(HandlerEntry {
1317                id: 1,
1318                of_type: SCStreamOutputType::Screen,
1319                handler: Arc::new(
1320                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1321                        counter.fetch_add(1, Ordering::Relaxed);
1322                        std::mem::forget(buf);
1323                    },
1324                ),
1325            });
1326        }
1327        {
1328            let counter = audio_count.clone();
1329            let mut handlers = unsafe { &*ctx }
1330                .handlers
1331                .write()
1332                .unwrap_or_else(std::sync::PoisonError::into_inner);
1333            handlers.push(HandlerEntry {
1334                id: 2,
1335                of_type: SCStreamOutputType::Audio,
1336                handler: Arc::new(
1337                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1338                        counter.fetch_add(1, Ordering::Relaxed);
1339                        std::mem::forget(buf);
1340                    },
1341                ),
1342            });
1343        }
1344
1345        // Send 4 screen callbacks
1346        for _ in 0..4 {
1347            let handlers = unsafe { &*ctx }
1348                .handlers
1349                .write()
1350                .unwrap_or_else(std::sync::PoisonError::into_inner);
1351            for entry in handlers
1352                .iter()
1353                .filter(|e| e.of_type == SCStreamOutputType::Screen)
1354            {
1355                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1356                entry
1357                    .handler
1358                    .did_output_sample_buffer(buf, SCStreamOutputType::Screen);
1359            }
1360        }
1361
1362        // Send 2 audio callbacks
1363        for _ in 0..2 {
1364            let handlers = unsafe { &*ctx }
1365                .handlers
1366                .write()
1367                .unwrap_or_else(std::sync::PoisonError::into_inner);
1368            for entry in handlers
1369                .iter()
1370                .filter(|e| e.of_type == SCStreamOutputType::Audio)
1371            {
1372                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1373                entry
1374                    .handler
1375                    .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
1376            }
1377        }
1378
1379        assert_eq!(screen_count.load(Ordering::Relaxed), 4);
1380        assert_eq!(audio_count.load(Ordering::Relaxed), 2);
1381
1382        unsafe { StreamContext::release(ctx) };
1383    }
1384
1385    /// Verify that `StreamContext` ref counting works correctly.
1386    #[test]
1387    fn test_stream_context_ref_counting() {
1388        let ctx = StreamContext::new(None);
1389
1390        // Initial ref count is 1
1391        assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
1392
1393        // Retain bumps to 2
1394        unsafe { StreamContext::retain(ctx) };
1395        assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 2);
1396
1397        // First release drops to 1 — context still alive
1398        unsafe { StreamContext::release(ctx) };
1399        assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
1400
1401        // Second release drops to 0 — context freed (no crash = success)
1402        unsafe { StreamContext::release(ctx) };
1403    }
1404
1405    /// Regression test: a panic in a user-supplied output handler must NOT
1406    /// poison the handlers `RwLock`, must NOT propagate across the C ABI,
1407    /// and must NOT prevent subsequent callbacks from being dispatched.
1408    ///
1409    /// This validates the C1+C2 fix from the deep review: `catch_unwind`
1410    /// around user dispatch and `RwLock` poisoning recovery via
1411    /// `unwrap_or_else(PoisonError::into_inner)` together prevent one
1412    /// panicking handler from permanently breaking the stream.
1413    #[test]
1414    fn test_panic_in_handler_is_isolated() {
1415        // Set a no-op panic hook so our intentional panic doesn't spam the
1416        // test output. We restore it at the end of the test.
1417        let original_hook = std::panic::take_hook();
1418        std::panic::set_hook(Box::new(|_| {}));
1419
1420        let panicked_count = Arc::new(AtomicUsize::new(0));
1421        let normal_count = Arc::new(AtomicUsize::new(0));
1422
1423        let ctx = StreamContext::new(None);
1424
1425        // Handler 1: always panics
1426        {
1427            let counter = panicked_count.clone();
1428            let mut handlers = unsafe { &*ctx }
1429                .handlers
1430                .write()
1431                .unwrap_or_else(std::sync::PoisonError::into_inner);
1432            handlers.push(HandlerEntry {
1433                id: 1,
1434                of_type: SCStreamOutputType::Audio,
1435                handler: Arc::new(
1436                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1437                        counter.fetch_add(1, Ordering::Relaxed);
1438                        std::mem::forget(buf);
1439                        panic!("intentional test panic");
1440                    },
1441                ),
1442            });
1443        }
1444
1445        // Handler 2: well-behaved, registered AFTER the panicker
1446        {
1447            let counter = normal_count.clone();
1448            let mut handlers = unsafe { &*ctx }
1449                .handlers
1450                .write()
1451                .unwrap_or_else(std::sync::PoisonError::into_inner);
1452            handlers.push(HandlerEntry {
1453                id: 2,
1454                of_type: SCStreamOutputType::Audio,
1455                handler: Arc::new(
1456                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1457                        counter.fetch_add(1, Ordering::Relaxed);
1458                        std::mem::forget(buf);
1459                    },
1460                ),
1461            });
1462        }
1463
1464        // Simulate 5 callbacks. Each iteration, the panicker fires (and
1465        // panics), then the well-behaved handler must still fire on the
1466        // SAME callback because both handlers match the output type. We
1467        // simulate the dispatch path without going through the C callback
1468        // (which would require a real CMSampleBuffer); the key behaviour
1469        // we're verifying is that the lock isn't poisoned and that the
1470        // catch_unwind boundary contains the panic.
1471        for _ in 0..5 {
1472            let handlers = unsafe { &*ctx }
1473                .handlers
1474                .read()
1475                .unwrap_or_else(std::sync::PoisonError::into_inner);
1476            for entry in handlers
1477                .iter()
1478                .filter(|e| e.of_type == SCStreamOutputType::Audio)
1479            {
1480                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1481                catch_user_panic("test handler", || {
1482                    entry
1483                        .handler
1484                        .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
1485                });
1486            }
1487        }
1488
1489        // Both handlers fired 5 times each — the panicker did not stop the
1490        // dispatch loop or poison the lock for subsequent reads.
1491        assert_eq!(
1492            panicked_count.load(Ordering::Relaxed),
1493            5,
1494            "panicking handler stopped firing after first panic"
1495        );
1496        assert_eq!(
1497            normal_count.load(Ordering::Relaxed),
1498            5,
1499            "well-behaved handler stopped firing after panicker poisoned state"
1500        );
1501
1502        // Lock is still acquirable (would otherwise be poisoned).
1503        drop(
1504            unsafe { &*ctx }
1505                .handlers
1506                .write()
1507                .unwrap_or_else(std::sync::PoisonError::into_inner),
1508        );
1509
1510        unsafe { StreamContext::release(ctx) };
1511
1512        // Restore the original panic hook so other tests behave normally.
1513        std::panic::set_hook(original_hook);
1514    }
1515
1516    /// Regression test: dispatching a sample must not hold the handlers lock,
1517    /// so a handler is free to mutate the handler set from inside the
1518    /// callback. Under the previous read-lock-across-dispatch design this
1519    /// deadlocked on the `RwLock` upgrade.
1520    #[test]
1521    fn test_handler_may_take_the_write_lock_from_inside_a_callback() {
1522        struct Reentrant(*mut StreamContext);
1523        // SAFETY: the pointer is only used to take the same locks the real
1524        // callback path takes; the context outlives the handler in this test.
1525        unsafe impl Send for Reentrant {}
1526        unsafe impl Sync for Reentrant {}
1527
1528        impl SCStreamOutputTrait for Reentrant {
1529            fn did_output_sample_buffer(
1530                &self,
1531                buffer: crate::cm::CMSampleBuffer,
1532                _of_type: SCStreamOutputType,
1533            ) {
1534                std::mem::forget(buffer);
1535                // Would deadlock if the dispatch path still held a read lock.
1536                let mut handlers = unsafe { &*self.0 }
1537                    .handlers
1538                    .write()
1539                    .unwrap_or_else(std::sync::PoisonError::into_inner);
1540                handlers.retain(|e| e.id != 1);
1541            }
1542        }
1543
1544        let ctx = StreamContext::new(None);
1545        unsafe { &*ctx }
1546            .handlers
1547            .write()
1548            .unwrap_or_else(std::sync::PoisonError::into_inner)
1549            .push(HandlerEntry {
1550                id: 1,
1551                of_type: SCStreamOutputType::Screen,
1552                handler: Arc::new(Reentrant(ctx)),
1553            });
1554
1555        for handler in unsafe { &*ctx }.handler_snapshot(SCStreamOutputType::Screen) {
1556            let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1557            handler.did_output_sample_buffer(buffer, SCStreamOutputType::Screen);
1558        }
1559
1560        assert!(
1561            unsafe { &*ctx }
1562                .handler_snapshot(SCStreamOutputType::Screen)
1563                .is_empty(),
1564            "handler failed to remove itself from inside its own callback"
1565        );
1566
1567        unsafe { StreamContext::release(ctx) };
1568    }
1569
1570    /// A handler removed while a sample is already being dispatched must stay
1571    /// alive for that call — the snapshot holds an `Arc`, so the removal can
1572    /// never free a handler out from under a running callback.
1573    #[test]
1574    fn test_snapshot_keeps_a_concurrently_removed_handler_alive() {
1575        let ctx = StreamContext::new(None);
1576        let calls = Arc::new(AtomicUsize::new(0));
1577
1578        {
1579            let counter = calls.clone();
1580            unsafe { &*ctx }
1581                .handlers
1582                .write()
1583                .unwrap_or_else(std::sync::PoisonError::into_inner)
1584                .push(HandlerEntry {
1585                    id: 1,
1586                    of_type: SCStreamOutputType::Audio,
1587                    handler: Arc::new(
1588                        move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
1589                            counter.fetch_add(1, Ordering::Relaxed);
1590                            std::mem::forget(buf);
1591                        },
1592                    ),
1593                });
1594        }
1595
1596        let snapshot = unsafe { &*ctx }.handler_snapshot(SCStreamOutputType::Audio);
1597
1598        // Remove the handler *after* the snapshot was taken, mirroring a
1599        // `remove_output_handler` racing an in-flight callback.
1600        unsafe { &*ctx }
1601            .handlers
1602            .write()
1603            .unwrap_or_else(std::sync::PoisonError::into_inner)
1604            .clear();
1605
1606        for handler in &snapshot {
1607            let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
1608            handler.did_output_sample_buffer(buffer, SCStreamOutputType::Audio);
1609        }
1610
1611        assert_eq!(calls.load(Ordering::Relaxed), 1);
1612        // The next dispatch sees the removal.
1613        assert!(unsafe { &*ctx }
1614            .handler_snapshot(SCStreamOutputType::Audio)
1615            .is_empty());
1616
1617        unsafe { StreamContext::release(ctx) };
1618    }
1619
1620    /// The delegate snapshot must clone the `Arc` out rather than dispatch
1621    /// under the lock, so a delegate can register handlers (or otherwise
1622    /// re-enter the stream) from its own callback.
1623    #[test]
1624    fn test_delegate_snapshot_runs_user_code_unlocked() {
1625        struct Counting(Arc<AtomicUsize>);
1626        impl SCStreamDelegateTrait for Counting {
1627            fn stream_did_become_active(&self) {
1628                self.0.fetch_add(1, Ordering::Relaxed);
1629            }
1630        }
1631
1632        let calls = Arc::new(AtomicUsize::new(0));
1633        let ctx = StreamContext::new(Some(Arc::new(Counting(calls.clone()))));
1634
1635        let delegate = unsafe { &*ctx }
1636            .delegate_snapshot()
1637            .expect("delegate should be present");
1638        // The lock is free while user code runs.
1639        assert!(unsafe { &*ctx }.delegate.try_write().is_ok());
1640        delegate.stream_did_become_active();
1641
1642        assert_eq!(calls.load(Ordering::Relaxed), 1);
1643
1644        unsafe { StreamContext::release(ctx) };
1645    }
1646
1647    /// Regression test: the four non-error `SCStreamDelegate` callbacks used
1648    /// to be dead code — the bridge never had a trampoline to reach them.
1649    /// This pins the event-code mapping shared with `Stream.swift`.
1650    #[test]
1651    fn test_delegate_event_callback_routes_each_event_code() {
1652        #[derive(Default)]
1653        struct Recorder {
1654            events: std::sync::Mutex<Vec<&'static str>>,
1655        }
1656        impl Recorder {
1657            fn record(&self, what: &'static str) {
1658                self.events
1659                    .lock()
1660                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1661                    .push(what);
1662            }
1663        }
1664        impl SCStreamDelegateTrait for Arc<Recorder> {
1665            fn stream_did_become_active(&self) {
1666                self.record("active");
1667            }
1668            fn stream_did_become_inactive(&self) {
1669                self.record("inactive");
1670            }
1671            fn output_video_effect_did_start_for_stream(&self) {
1672                self.record("effect_start");
1673            }
1674            fn output_video_effect_did_stop_for_stream(&self) {
1675                self.record("effect_stop");
1676            }
1677        }
1678
1679        let recorder = Arc::new(Recorder::default());
1680        let ctx = StreamContext::new(Some(Arc::new(Arc::clone(&recorder))));
1681
1682        for event in 0..4 {
1683            delegate_event_callback(ctx.cast::<c_void>(), event);
1684        }
1685        // Unknown codes are logged, not dispatched, and must not panic.
1686        delegate_event_callback(ctx.cast::<c_void>(), 99);
1687        // A null context is ignored rather than dereferenced.
1688        delegate_event_callback(std::ptr::null_mut(), 0);
1689
1690        assert_eq!(
1691            *recorder
1692                .events
1693                .lock()
1694                .unwrap_or_else(std::sync::PoisonError::into_inner),
1695            vec!["active", "inactive", "effect_start", "effect_stop"]
1696        );
1697
1698        unsafe { StreamContext::release(ctx) };
1699    }
1700
1701    /// A stream with no delegate must swallow lifecycle events instead of
1702    /// dereferencing a missing one.
1703    #[test]
1704    fn test_delegate_event_callback_without_a_delegate_is_a_noop() {
1705        let ctx = StreamContext::new(None);
1706        for event in 0..4 {
1707            delegate_event_callback(ctx.cast::<c_void>(), event);
1708        }
1709        unsafe { StreamContext::release(ctx) };
1710    }
1711
1712    #[test]
1713    fn test_null_native_stream_is_rejected_and_releases_its_context() {
1714        let ctx = StreamContext::new(None);
1715        unsafe { StreamContext::retain(ctx) };
1716
1717        let result = unsafe { SCStream::adopt(std::ptr::null(), ctx) };
1718
1719        assert!(matches!(result, Err(SCError::NullPointer(_))));
1720        assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
1721        unsafe { StreamContext::release(ctx) };
1722    }
1723
1724    #[test]
1725    fn test_stream_identity_rejects_null() {
1726        assert!(StreamIdentity::from_ptr(std::ptr::null()).is_none());
1727        let value = 0x1000_usize as *const c_void;
1728        assert_eq!(
1729            StreamIdentity::from_ptr(value),
1730            NonZeroUsize::new(0x1000).map(StreamIdentity)
1731        );
1732    }
1733
1734    #[test]
1735    fn test_claim_start_skips_the_native_call_while_capturing() {
1736        let capturing = AtomicBool::new(false);
1737        let unconfirmed = AtomicBool::new(false);
1738
1739        assert!(claim_start(&capturing, &unconfirmed));
1740        assert!(!claim_start(&capturing, &unconfirmed));
1741    }
1742
1743    /// A start whose completion timed out left `capturing` latched without a
1744    /// confirmed outcome, so the retry must reissue rather than report the
1745    /// success nobody observed.
1746    #[test]
1747    fn test_claim_start_reissues_after_an_unconfirmed_start() {
1748        let capturing = AtomicBool::new(true);
1749        let unconfirmed = AtomicBool::new(true);
1750
1751        assert!(claim_start(&capturing, &unconfirmed));
1752        assert!(!claim_start(&capturing, &unconfirmed));
1753    }
1754
1755    /// The flag must not outlive the retry that consumed it, or a later start
1756    /// on a live stream would reissue against `ScreenCaptureKit`.
1757    #[test]
1758    fn test_claim_start_clears_the_flag_even_when_idle() {
1759        let capturing = AtomicBool::new(false);
1760        let unconfirmed = AtomicBool::new(true);
1761
1762        assert!(claim_start(&capturing, &unconfirmed));
1763        assert!(!unconfirmed.load(Ordering::Acquire));
1764        assert!(!claim_start(&capturing, &unconfirmed));
1765    }
1766}