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::Mutex;
14
15use crate::error::SCError;
16use crate::stream::delegate_trait::SCStreamDelegateTrait;
17use crate::utils::completion::UnitCompletion;
18use crate::{
19    dispatch_queue::DispatchQueue,
20    ffi,
21    stream::{
22        configuration::SCStreamConfiguration, content_filter::SCContentFilter,
23        output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType,
24    },
25};
26
27/// Per-stream handler entry.
28struct HandlerEntry {
29    id: usize,
30    of_type: SCStreamOutputType,
31    handler: Box<dyn SCStreamOutputTrait>,
32}
33
34/// Per-stream context holding output handlers and an optional delegate.
35///
36/// Allocated on the heap via `Box::into_raw` and passed through FFI as an
37/// opaque context pointer. Callbacks cast it back to `&StreamContext` for
38/// direct, O(1) access to the owning stream's state.
39struct StreamContext {
40    handlers: Mutex<Vec<HandlerEntry>>,
41    delegate: Mutex<Option<Box<dyn SCStreamDelegateTrait>>>,
42    ref_count: AtomicUsize,
43}
44
45impl StreamContext {
46    fn new() -> *mut Self {
47        let ctx = Box::new(Self {
48            handlers: Mutex::new(Vec::new()),
49            delegate: Mutex::new(None),
50            ref_count: AtomicUsize::new(1),
51        });
52        Box::into_raw(ctx)
53    }
54
55    fn new_with_delegate(delegate: Box<dyn SCStreamDelegateTrait>) -> *mut Self {
56        let ctx = Box::new(Self {
57            handlers: Mutex::new(Vec::new()),
58            delegate: Mutex::new(Some(delegate)),
59            ref_count: AtomicUsize::new(1),
60        });
61        Box::into_raw(ctx)
62    }
63
64    /// Increment the reference count.
65    ///
66    /// # Safety
67    ///
68    /// `ptr` must point to a valid, live `StreamContext`.
69    unsafe fn retain(ptr: *mut Self) {
70        unsafe { &*ptr }.ref_count.fetch_add(1, Ordering::Relaxed);
71    }
72
73    /// Decrement the reference count, freeing the context if it reaches zero.
74    ///
75    /// # Safety
76    ///
77    /// `ptr` must point to a valid, live `StreamContext`. After this call,
78    /// `ptr` must not be used if the context was freed.
79    unsafe fn release(ptr: *mut Self) {
80        if ptr.is_null() {
81            return;
82        }
83        let prev = unsafe { &*ptr }.ref_count.fetch_sub(1, Ordering::Release);
84        if prev == 1 {
85            std::sync::atomic::fence(Ordering::Acquire);
86            drop(unsafe { Box::from_raw(ptr) });
87        }
88    }
89}
90
91/// Monotonically increasing handler ID generator (process-wide).
92static NEXT_HANDLER_ID: AtomicUsize = AtomicUsize::new(1);
93
94// C callback for stream errors — dispatches to per-stream delegate via context pointer.
95extern "C" fn delegate_error_callback(context: *mut c_void, error_code: i32, msg: *const i8) {
96    if context.is_null() {
97        return;
98    }
99    let ctx = unsafe { &*(context.cast::<StreamContext>()) };
100
101    let message = if msg.is_null() {
102        "Unknown error".to_string()
103    } else {
104        unsafe { CStr::from_ptr(msg) }
105            .to_str()
106            .unwrap_or("Unknown error")
107            .to_string()
108    };
109
110    let error = if error_code != 0 {
111        crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
112            || SCError::StreamError(format!("{message} (code: {error_code})")),
113            |code| SCError::SCStreamError {
114                code,
115                message: Some(message.clone()),
116            },
117        )
118    } else {
119        SCError::StreamError(message.clone())
120    };
121
122    if let Ok(delegate_guard) = ctx.delegate.lock() {
123        if let Some(ref delegate) = *delegate_guard {
124            delegate.did_stop_with_error(error);
125            delegate.stream_did_stop(Some(message));
126            return;
127        }
128    }
129
130    // Fallback to logging if no delegate registered
131    eprintln!("SCStream error: {error}");
132}
133
134// C callback for sample buffers — dispatches to per-stream handlers via context pointer.
135extern "C" fn sample_handler(context: *mut c_void, sample_buffer: *const c_void, output_type: i32) {
136    if context.is_null() {
137        unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
138        return;
139    }
140    let ctx = unsafe { &*(context.cast::<StreamContext>()) };
141
142    let output_type_enum = match output_type {
143        0 => SCStreamOutputType::Screen,
144        1 => SCStreamOutputType::Audio,
145        2 => SCStreamOutputType::Microphone,
146        _ => {
147            eprintln!("Unknown output type: {output_type}");
148            unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
149            return;
150        }
151    };
152
153    // Mutex poisoning is unrecoverable in C callback context; unwrap is appropriate
154    let handlers = ctx.handlers.lock().unwrap();
155
156    // Find handlers matching this output type
157    let matching: Vec<&HandlerEntry> = handlers
158        .iter()
159        .filter(|e| e.of_type == output_type_enum)
160        .collect();
161
162    if matching.is_empty() {
163        // Drop the lock before releasing buffer
164        drop(handlers);
165        unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
166        return;
167    }
168
169    let count = matching.len();
170    for (idx, entry) in matching.iter().enumerate() {
171        let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(sample_buffer.cast_mut()) };
172
173        // Retain for all but the last handler; the last one consumes the
174        // original reference that Swift passed via passRetained.
175        if idx < count - 1 {
176            unsafe { crate::cm::ffi::cm_sample_buffer_retain(sample_buffer.cast_mut()) };
177        }
178
179        entry
180            .handler
181            .did_output_sample_buffer(buffer, output_type_enum);
182    }
183}
184
185/// `SCStream` is a lightweight wrapper around the Swift `SCStream` instance.
186/// It provides direct FFI access to `ScreenCaptureKit` functionality.
187///
188/// This is the primary and only implementation of `SCStream` in v1.0+.
189/// All `ScreenCaptureKit` operations go through Swift FFI bindings.
190///
191/// # Examples
192///
193/// ```no_run
194/// use screencapturekit::prelude::*;
195///
196/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
197/// // Get shareable content
198/// let content = SCShareableContent::get()?;
199/// let display = &content.displays()[0];
200///
201/// // Create filter and configuration
202/// let filter = SCContentFilter::create()
203///     .with_display(display)
204///     .with_excluding_windows(&[])
205///     .build();
206/// let config = SCStreamConfiguration::new()
207///     .with_width(1920)
208///     .with_height(1080);
209///
210/// // Create and start stream
211/// let mut stream = SCStream::new(&filter, &config);
212/// stream.start_capture()?;
213///
214/// // ... capture frames ...
215///
216/// stream.stop_capture()?;
217/// # Ok(())
218/// # }
219/// ```
220pub struct SCStream {
221    ptr: *const c_void,
222    /// Per-stream context holding handlers and delegate (ref-counted).
223    context: *mut StreamContext,
224}
225
226unsafe impl Send for SCStream {}
227unsafe impl Sync for SCStream {}
228
229impl SCStream {
230    /// Create a new stream with a content filter and configuration
231    ///
232    /// # Examples
233    ///
234    /// ```no_run
235    /// use screencapturekit::prelude::*;
236    ///
237    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
238    /// let content = SCShareableContent::get()?;
239    /// let display = &content.displays()[0];
240    /// let filter = SCContentFilter::create()
241    ///     .with_display(display)
242    ///     .with_excluding_windows(&[])
243    ///     .build();
244    /// let config = SCStreamConfiguration::new()
245    ///     .with_width(1920)
246    ///     .with_height(1080);
247    ///
248    /// let stream = SCStream::new(&filter, &config);
249    /// # Ok(())
250    /// # }
251    /// ```
252    pub fn new(filter: &SCContentFilter, configuration: &SCStreamConfiguration) -> Self {
253        let context = StreamContext::new();
254        let context_ptr = context.cast::<c_void>();
255
256        let ptr = unsafe {
257            ffi::sc_stream_create(
258                filter.as_ptr(),
259                configuration.as_ptr(),
260                context_ptr,
261                delegate_error_callback,
262                sample_handler,
263            )
264        };
265
266        Self { ptr, context }
267    }
268
269    /// Create a new stream with a content filter, configuration, and delegate
270    ///
271    /// The delegate receives callbacks for stream lifecycle events:
272    /// - `did_stop_with_error` - Called when the stream stops due to an error
273    /// - `stream_did_stop` - Called when the stream stops (with optional error message)
274    ///
275    /// # Examples
276    ///
277    /// ```no_run
278    /// use screencapturekit::prelude::*;
279    /// use screencapturekit::stream::delegate_trait::StreamCallbacks;
280    ///
281    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
282    /// let content = SCShareableContent::get()?;
283    /// let display = &content.displays()[0];
284    /// let filter = SCContentFilter::create()
285    ///     .with_display(display)
286    ///     .with_excluding_windows(&[])
287    ///     .build();
288    /// let config = SCStreamConfiguration::new()
289    ///     .with_width(1920)
290    ///     .with_height(1080);
291    ///
292    /// let delegate = StreamCallbacks::new()
293    ///     .on_error(|e| eprintln!("Stream error: {}", e))
294    ///     .on_stop(|err| {
295    ///         if let Some(msg) = err {
296    ///             eprintln!("Stream stopped with error: {}", msg);
297    ///         }
298    ///     });
299    ///
300    /// let stream = SCStream::new_with_delegate(&filter, &config, delegate);
301    /// stream.start_capture()?;
302    /// # Ok(())
303    /// # }
304    /// ```
305    pub fn new_with_delegate(
306        filter: &SCContentFilter,
307        configuration: &SCStreamConfiguration,
308        delegate: impl SCStreamDelegateTrait + 'static,
309    ) -> Self {
310        let context = StreamContext::new_with_delegate(Box::new(delegate));
311        let context_ptr = context.cast::<c_void>();
312
313        let ptr = unsafe {
314            ffi::sc_stream_create(
315                filter.as_ptr(),
316                configuration.as_ptr(),
317                context_ptr,
318                delegate_error_callback,
319                sample_handler,
320            )
321        };
322
323        Self { ptr, context }
324    }
325
326    /// Add an output handler to receive captured frames
327    ///
328    /// # Arguments
329    ///
330    /// * `handler` - The handler to receive callbacks. Can be:
331    ///   - A struct implementing [`SCStreamOutputTrait`]
332    ///   - A closure `|CMSampleBuffer, SCStreamOutputType| { ... }`
333    /// * `of_type` - The type of output to receive (Screen, Audio, or Microphone)
334    ///
335    /// # Returns
336    ///
337    /// Returns `Some(handler_id)` on success, `None` on failure.
338    /// The handler ID can be used with [`remove_output_handler`](Self::remove_output_handler).
339    ///
340    /// # Examples
341    ///
342    /// Using a struct:
343    /// ```rust,no_run
344    /// use screencapturekit::prelude::*;
345    ///
346    /// struct MyHandler;
347    /// impl SCStreamOutputTrait for MyHandler {
348    ///     fn did_output_sample_buffer(&self, _sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
349    ///         println!("Got frame!");
350    ///     }
351    /// }
352    ///
353    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
354    /// # let content = SCShareableContent::get()?;
355    /// # let display = &content.displays()[0];
356    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
357    /// # let config = SCStreamConfiguration::default();
358    /// let mut stream = SCStream::new(&filter, &config);
359    /// stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);
360    /// # Ok(())
361    /// # }
362    /// ```
363    ///
364    /// Using a closure:
365    /// ```rust,no_run
366    /// use screencapturekit::prelude::*;
367    ///
368    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
369    /// # let content = SCShareableContent::get()?;
370    /// # let display = &content.displays()[0];
371    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
372    /// # let config = SCStreamConfiguration::default();
373    /// let mut stream = SCStream::new(&filter, &config);
374    /// stream.add_output_handler(
375    ///     |_sample, _type| println!("Got frame!"),
376    ///     SCStreamOutputType::Screen
377    /// );
378    /// # Ok(())
379    /// # }
380    /// ```
381    pub fn add_output_handler(
382        &mut self,
383        handler: impl SCStreamOutputTrait + 'static,
384        of_type: SCStreamOutputType,
385    ) -> Option<usize> {
386        self.add_output_handler_with_queue(handler, of_type, None)
387    }
388
389    /// Add an output handler with a custom dispatch queue
390    ///
391    /// This allows controlling which thread/queue the handler is called on.
392    ///
393    /// # Arguments
394    ///
395    /// * `handler` - The handler to receive callbacks
396    /// * `of_type` - The type of output to receive
397    /// * `queue` - Optional custom dispatch queue for callbacks
398    ///
399    /// # Panics
400    ///
401    /// Panics if the internal handler mutex is poisoned.
402    ///
403    /// # Examples
404    ///
405    /// ```rust,no_run
406    /// use screencapturekit::prelude::*;
407    /// use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
408    ///
409    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
410    /// # let content = SCShareableContent::get()?;
411    /// # let display = &content.displays()[0];
412    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
413    /// # let config = SCStreamConfiguration::default();
414    /// let mut stream = SCStream::new(&filter, &config);
415    /// let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
416    ///
417    /// stream.add_output_handler_with_queue(
418    ///     |_sample, _type| println!("Got frame on custom queue!"),
419    ///     SCStreamOutputType::Screen,
420    ///     Some(&queue)
421    /// );
422    /// # Ok(())
423    /// # }
424    /// ```
425    pub fn add_output_handler_with_queue(
426        &mut self,
427        handler: impl SCStreamOutputTrait + 'static,
428        of_type: SCStreamOutputType,
429        queue: Option<&DispatchQueue>,
430    ) -> Option<usize> {
431        let handler_id = NEXT_HANDLER_ID.fetch_add(1, Ordering::Relaxed);
432
433        // Convert output type to int for Swift
434        let output_type_int = match of_type {
435            SCStreamOutputType::Screen => 0,
436            SCStreamOutputType::Audio => 1,
437            SCStreamOutputType::Microphone => 2,
438        };
439
440        let ok = if let Some(q) = queue {
441            unsafe {
442                ffi::sc_stream_add_stream_output_with_queue(self.ptr, output_type_int, q.as_ptr())
443            }
444        } else {
445            unsafe { ffi::sc_stream_add_stream_output(self.ptr, output_type_int) }
446        };
447
448        if ok {
449            unsafe { &*self.context }
450                .handlers
451                .lock()
452                .unwrap()
453                .push(HandlerEntry {
454                    id: handler_id,
455                    of_type,
456                    handler: Box::new(handler),
457                });
458            Some(handler_id)
459        } else {
460            None
461        }
462    }
463
464    /// Remove an output handler
465    ///
466    /// # Arguments
467    ///
468    /// * `id` - The handler ID returned from [`add_output_handler`](Self::add_output_handler)
469    /// * `of_type` - The type of output the handler was registered for
470    ///
471    /// # Panics
472    ///
473    /// Panics if the internal handler mutex is poisoned.
474    ///
475    /// # Returns
476    ///
477    /// Returns `true` if the handler was found and removed, `false` otherwise.
478    pub fn remove_output_handler(&mut self, id: usize, of_type: SCStreamOutputType) -> bool {
479        let mut handlers = unsafe { &*self.context }.handlers.lock().unwrap();
480        let Some(pos) = handlers.iter().position(|e| e.id == id) else {
481            return false;
482        };
483        handlers.remove(pos);
484
485        // If no more handlers for this output type, tell Swift to remove the output
486        let has_type = handlers.iter().any(|e| e.of_type == of_type);
487        drop(handlers);
488
489        if !has_type {
490            let output_type_int = match of_type {
491                SCStreamOutputType::Screen => 0,
492                SCStreamOutputType::Audio => 1,
493                SCStreamOutputType::Microphone => 2,
494            };
495            unsafe { ffi::sc_stream_remove_stream_output(self.ptr, output_type_int) };
496        }
497
498        true
499    }
500
501    /// Start capturing screen content
502    ///
503    /// This method blocks until the capture operation completes or fails.
504    ///
505    /// # Errors
506    ///
507    /// Returns `SCError::CaptureStartFailed` if the capture fails to start.
508    pub fn start_capture(&self) -> Result<(), SCError> {
509        let (completion, context) = UnitCompletion::new();
510        unsafe { ffi::sc_stream_start_capture(self.ptr, context, UnitCompletion::callback) };
511        completion.wait().map_err(SCError::CaptureStartFailed)
512    }
513
514    /// Stop capturing screen content
515    ///
516    /// This method blocks until the capture operation completes or fails.
517    ///
518    /// # Errors
519    ///
520    /// Returns `SCError::CaptureStopFailed` if the capture fails to stop.
521    pub fn stop_capture(&self) -> Result<(), SCError> {
522        let (completion, context) = UnitCompletion::new();
523        unsafe { ffi::sc_stream_stop_capture(self.ptr, context, UnitCompletion::callback) };
524        completion.wait().map_err(SCError::CaptureStopFailed)
525    }
526
527    /// Update the stream configuration
528    ///
529    /// This method blocks until the configuration update completes or fails.
530    ///
531    /// # Errors
532    ///
533    /// Returns `SCError::StreamError` if the configuration update fails.
534    pub fn update_configuration(
535        &self,
536        configuration: &SCStreamConfiguration,
537    ) -> Result<(), SCError> {
538        let (completion, context) = UnitCompletion::new();
539        unsafe {
540            ffi::sc_stream_update_configuration(
541                self.ptr,
542                configuration.as_ptr(),
543                context,
544                UnitCompletion::callback,
545            );
546        }
547        completion.wait().map_err(SCError::StreamError)
548    }
549
550    /// Update the content filter
551    ///
552    /// This method blocks until the filter update completes or fails.
553    ///
554    /// # Errors
555    ///
556    /// Returns `SCError::StreamError` if the filter update fails.
557    pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
558        let (completion, context) = UnitCompletion::new();
559        unsafe {
560            ffi::sc_stream_update_content_filter(
561                self.ptr,
562                filter.as_ptr(),
563                context,
564                UnitCompletion::callback,
565            );
566        }
567        completion.wait().map_err(SCError::StreamError)
568    }
569
570    /// Get the synchronization clock for this stream (macOS 13.0+)
571    ///
572    /// Returns the `CMClock` used to synchronize the stream's output.
573    /// This is useful for coordinating multiple streams or synchronizing
574    /// with other media.
575    ///
576    /// Returns `None` if the clock is not available (e.g., stream not started
577    /// or macOS version too old).
578    #[cfg(feature = "macos_13_0")]
579    pub fn synchronization_clock(&self) -> Option<crate::cm::CMClock> {
580        let ptr = unsafe { ffi::sc_stream_get_synchronization_clock(self.ptr) };
581        if ptr.is_null() {
582            None
583        } else {
584            Some(crate::cm::CMClock::from_ptr(ptr))
585        }
586    }
587
588    /// Add a recording output to the stream (macOS 15.0+)
589    ///
590    /// Starts recording if the stream is already capturing, otherwise recording
591    /// will start when capture begins. The recording is written to the file URL
592    /// specified in the `SCRecordingOutputConfiguration`.
593    ///
594    /// # Errors
595    ///
596    /// Returns `SCError::StreamError` if adding the recording output fails.
597    #[cfg(feature = "macos_15_0")]
598    pub fn add_recording_output(
599        &self,
600        recording_output: &crate::recording_output::SCRecordingOutput,
601    ) -> Result<(), SCError> {
602        let (completion, context) = UnitCompletion::new();
603        unsafe {
604            ffi::sc_stream_add_recording_output(
605                self.ptr,
606                recording_output.as_ptr(),
607                UnitCompletion::callback,
608                context,
609            );
610        }
611        completion.wait().map_err(SCError::StreamError)
612    }
613
614    /// Remove a recording output from the stream (macOS 15.0+)
615    ///
616    /// Stops recording if the stream is currently recording.
617    ///
618    /// # Errors
619    ///
620    /// Returns `SCError::StreamError` if removing the recording output fails.
621    #[cfg(feature = "macos_15_0")]
622    pub fn remove_recording_output(
623        &self,
624        recording_output: &crate::recording_output::SCRecordingOutput,
625    ) -> Result<(), SCError> {
626        let (completion, context) = UnitCompletion::new();
627        unsafe {
628            ffi::sc_stream_remove_recording_output(
629                self.ptr,
630                recording_output.as_ptr(),
631                UnitCompletion::callback,
632                context,
633            );
634        }
635        completion.wait().map_err(SCError::StreamError)
636    }
637
638    /// Returns the raw pointer to the underlying Swift `SCStream` instance.
639    #[allow(dead_code)]
640    pub(crate) fn as_ptr(&self) -> *const c_void {
641        self.ptr
642    }
643}
644
645impl Drop for SCStream {
646    fn drop(&mut self) {
647        if !self.ptr.is_null() {
648            unsafe { ffi::sc_stream_release(self.ptr) };
649        }
650        unsafe { StreamContext::release(self.context) };
651    }
652}
653
654impl Clone for SCStream {
655    /// Clone the stream reference.
656    ///
657    /// Cloning an `SCStream` creates a new reference to the same underlying
658    /// Swift `SCStream` object. The cloned stream shares the same handlers
659    /// as the original — they receive frames from the same capture session.
660    ///
661    /// Both the original and cloned stream share the same capture state, so:
662    /// - Starting capture on one affects both
663    /// - Stopping capture on one affects both
664    /// - Configuration updates affect both
665    /// - Handlers receive the same frames
666    ///
667    /// # Examples
668    ///
669    /// ```rust,no_run
670    /// use screencapturekit::prelude::*;
671    ///
672    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
673    /// # let content = SCShareableContent::get()?;
674    /// # let display = &content.displays()[0];
675    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
676    /// # let config = SCStreamConfiguration::default();
677    /// let mut stream = SCStream::new(&filter, &config);
678    /// stream.add_output_handler(|_, _| println!("Handler 1"), SCStreamOutputType::Screen);
679    ///
680    /// // Clone shares the same handlers
681    /// let stream2 = stream.clone();
682    /// // Both stream and stream2 will receive frames via Handler 1
683    /// # Ok(())
684    /// # }
685    /// ```
686    fn clone(&self) -> Self {
687        unsafe { StreamContext::retain(self.context) };
688
689        Self {
690            ptr: unsafe { crate::ffi::sc_stream_retain(self.ptr) },
691            context: self.context,
692        }
693    }
694}
695
696impl fmt::Debug for SCStream {
697    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
698        f.debug_struct("SCStream")
699            .field("ptr", &self.ptr)
700            .finish_non_exhaustive()
701    }
702}
703
704impl fmt::Display for SCStream {
705    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
706        write!(f, "SCStream")
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use std::sync::atomic::AtomicUsize;
714    use std::sync::Arc;
715
716    /// Regression test for #135: multiple concurrent streams must not leak
717    /// samples across each other.
718    ///
719    /// Creates two independent StreamContexts with separate handlers and
720    /// directly invokes each context's handlers. Verifies that each handler
721    /// only receives calls routed through its own context — not from the
722    /// other context. With the old global HANDLER_REGISTRY, both handlers
723    /// would have been called for every callback regardless of context.
724    #[test]
725    fn test_per_stream_callback_isolation() {
726        let count_a = Arc::new(AtomicUsize::new(0));
727        let count_b = Arc::new(AtomicUsize::new(0));
728
729        // Create two independent contexts (simulates two SCStream instances)
730        let ctx_a = StreamContext::new();
731        let ctx_b = StreamContext::new();
732
733        // Register an audio handler on context A
734        {
735            let counter = count_a.clone();
736            let mut handlers = unsafe { &*ctx_a }.handlers.lock().unwrap();
737            handlers.push(HandlerEntry {
738                id: 1,
739                of_type: SCStreamOutputType::Audio,
740                handler: Box::new(
741                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
742                        counter.fetch_add(1, Ordering::Relaxed);
743                        // Prevent Drop from calling cm_sample_buffer_release on our fake pointer
744                        std::mem::forget(buf);
745                    },
746                ),
747            });
748        }
749
750        // Register an audio handler on context B
751        {
752            let counter = count_b.clone();
753            let mut handlers = unsafe { &*ctx_b }.handlers.lock().unwrap();
754            handlers.push(HandlerEntry {
755                id: 2,
756                of_type: SCStreamOutputType::Audio,
757                handler: Box::new(
758                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
759                        counter.fetch_add(1, Ordering::Relaxed);
760                        std::mem::forget(buf);
761                    },
762                ),
763            });
764        }
765
766        // Simulate 5 audio callbacks on context A by directly calling matching handlers
767        for _ in 0..5 {
768            let handlers = unsafe { &*ctx_a }.handlers.lock().unwrap();
769            for entry in handlers
770                .iter()
771                .filter(|e| e.of_type == SCStreamOutputType::Audio)
772            {
773                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
774                entry
775                    .handler
776                    .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
777            }
778        }
779
780        // Simulate 3 audio callbacks on context B
781        for _ in 0..3 {
782            let handlers = unsafe { &*ctx_b }.handlers.lock().unwrap();
783            for entry in handlers
784                .iter()
785                .filter(|e| e.of_type == SCStreamOutputType::Audio)
786            {
787                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
788                entry
789                    .handler
790                    .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
791            }
792        }
793
794        // Handler A must have received exactly 5 — not 8
795        assert_eq!(
796            count_a.load(Ordering::Relaxed),
797            5,
798            "handler A received callbacks meant for B (cross-stream leak)"
799        );
800        // Handler B must have received exactly 3 — not 8
801        assert_eq!(
802            count_b.load(Ordering::Relaxed),
803            3,
804            "handler B received callbacks meant for A (cross-stream leak)"
805        );
806
807        unsafe {
808            StreamContext::release(ctx_a);
809            StreamContext::release(ctx_b);
810        }
811    }
812
813    /// Verify that handlers are filtered by output type within a single context.
814    #[test]
815    fn test_handler_output_type_filtering() {
816        let screen_count = Arc::new(AtomicUsize::new(0));
817        let audio_count = Arc::new(AtomicUsize::new(0));
818
819        let ctx = StreamContext::new();
820
821        {
822            let counter = screen_count.clone();
823            let mut handlers = unsafe { &*ctx }.handlers.lock().unwrap();
824            handlers.push(HandlerEntry {
825                id: 1,
826                of_type: SCStreamOutputType::Screen,
827                handler: Box::new(
828                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
829                        counter.fetch_add(1, Ordering::Relaxed);
830                        std::mem::forget(buf);
831                    },
832                ),
833            });
834        }
835        {
836            let counter = audio_count.clone();
837            let mut handlers = unsafe { &*ctx }.handlers.lock().unwrap();
838            handlers.push(HandlerEntry {
839                id: 2,
840                of_type: SCStreamOutputType::Audio,
841                handler: Box::new(
842                    move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
843                        counter.fetch_add(1, Ordering::Relaxed);
844                        std::mem::forget(buf);
845                    },
846                ),
847            });
848        }
849
850        // Send 4 screen callbacks
851        for _ in 0..4 {
852            let handlers = unsafe { &*ctx }.handlers.lock().unwrap();
853            for entry in handlers
854                .iter()
855                .filter(|e| e.of_type == SCStreamOutputType::Screen)
856            {
857                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
858                entry
859                    .handler
860                    .did_output_sample_buffer(buf, SCStreamOutputType::Screen);
861            }
862        }
863
864        // Send 2 audio callbacks
865        for _ in 0..2 {
866            let handlers = unsafe { &*ctx }.handlers.lock().unwrap();
867            for entry in handlers
868                .iter()
869                .filter(|e| e.of_type == SCStreamOutputType::Audio)
870            {
871                let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
872                entry
873                    .handler
874                    .did_output_sample_buffer(buf, SCStreamOutputType::Audio);
875            }
876        }
877
878        assert_eq!(screen_count.load(Ordering::Relaxed), 4);
879        assert_eq!(audio_count.load(Ordering::Relaxed), 2);
880
881        unsafe { StreamContext::release(ctx) };
882    }
883
884    /// Verify that StreamContext ref counting works correctly.
885    #[test]
886    fn test_stream_context_ref_counting() {
887        let ctx = StreamContext::new();
888
889        // Initial ref count is 1
890        assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
891
892        // Retain bumps to 2
893        unsafe { StreamContext::retain(ctx) };
894        assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 2);
895
896        // First release drops to 1 — context still alive
897        unsafe { StreamContext::release(ctx) };
898        assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
899
900        // Second release drops to 0 — context freed (no crash = success)
901        unsafe { StreamContext::release(ctx) };
902    }
903}