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
6use std::collections::HashMap;
7use std::ffi::{c_void, CStr};
8use std::fmt;
9use std::sync::Mutex;
10
11use crate::error::SCError;
12use crate::stream::delegate_trait::SCStreamDelegateTrait;
13use crate::utils::completion::UnitCompletion;
14use crate::{
15    dispatch_queue::DispatchQueue,
16    ffi,
17    stream::{
18        configuration::SCStreamConfiguration, content_filter::SCContentFilter,
19        output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType,
20    },
21};
22
23// Handler entry with reference count
24struct HandlerEntry {
25    handler: Box<dyn SCStreamOutputTrait>,
26    ref_count: usize,
27}
28
29// Global registry for output handlers with reference counting
30static HANDLER_REGISTRY: Mutex<Option<HashMap<usize, HandlerEntry>>> = Mutex::new(None);
31static NEXT_HANDLER_ID: Mutex<usize> = Mutex::new(1);
32
33// Global registry for stream delegates (keyed by stream pointer) with reference counting
34struct DelegateEntry {
35    delegate: Box<dyn SCStreamDelegateTrait>,
36    ref_count: usize,
37}
38static DELEGATE_REGISTRY: Mutex<Option<HashMap<usize, DelegateEntry>>> = Mutex::new(None);
39
40/// Increment a handler's ref count
41#[allow(clippy::significant_drop_tightening)]
42fn increment_handler_ref_count(id: usize) {
43    let mut registry = HANDLER_REGISTRY.lock().unwrap();
44    let Some(handlers) = registry.as_mut() else {
45        return;
46    };
47    if let Some(entry) = handlers.get_mut(&id) {
48        entry.ref_count += 1;
49    }
50}
51
52/// Decrement a handler's ref count, returning true if entry was removed (`ref_count` reached 0)
53#[allow(clippy::significant_drop_tightening)]
54fn decrement_handler_ref_count(id: usize) -> bool {
55    let mut registry = HANDLER_REGISTRY.lock().unwrap();
56    let Some(handlers) = registry.as_mut() else {
57        return false;
58    };
59    let Some(entry) = handlers.get_mut(&id) else {
60        return false;
61    };
62
63    entry.ref_count = entry.ref_count.saturating_sub(1);
64    if entry.ref_count == 0 {
65        handlers.remove(&id);
66        true
67    } else {
68        false
69    }
70}
71
72/// Increment a delegate's ref count
73#[allow(clippy::significant_drop_tightening)]
74fn increment_delegate_ref_count(stream_key: usize) {
75    let Ok(mut registry) = DELEGATE_REGISTRY.lock() else {
76        return;
77    };
78    let Some(delegates) = registry.as_mut() else {
79        return;
80    };
81    if let Some(entry) = delegates.get_mut(&stream_key) {
82        entry.ref_count += 1;
83    }
84}
85
86/// Decrement a delegate's ref count, returning true if entry was removed (`ref_count` reached 0)
87#[allow(clippy::significant_drop_tightening)]
88fn decrement_delegate_ref_count(stream_key: usize) -> bool {
89    let Ok(mut registry) = DELEGATE_REGISTRY.lock() else {
90        return false;
91    };
92    let Some(delegates) = registry.as_mut() else {
93        return false;
94    };
95    let Some(entry) = delegates.get_mut(&stream_key) else {
96        return false;
97    };
98
99    entry.ref_count = entry.ref_count.saturating_sub(1);
100    if entry.ref_count == 0 {
101        delegates.remove(&stream_key);
102        true
103    } else {
104        false
105    }
106}
107
108// C callback for stream errors that dispatches to registered delegate
109extern "C" fn delegate_error_callback(stream: *const c_void, error_code: i32, msg: *const i8) {
110    let message = if msg.is_null() {
111        "Unknown error".to_string()
112    } else {
113        unsafe { CStr::from_ptr(msg) }
114            .to_str()
115            .unwrap_or("Unknown error")
116            .to_string()
117    };
118
119    let error = if error_code != 0 {
120        crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
121            || SCError::StreamError(format!("{message} (code: {error_code})")),
122            |code| SCError::SCStreamError {
123                code,
124                message: Some(message.clone()),
125            },
126        )
127    } else {
128        SCError::StreamError(message.clone())
129    };
130
131    // Look up delegate in registry and call it
132    let stream_key = stream as usize;
133    if let Ok(registry) = DELEGATE_REGISTRY.lock() {
134        if let Some(ref delegates) = *registry {
135            if let Some(entry) = delegates.get(&stream_key) {
136                entry.delegate.did_stop_with_error(error);
137                entry.delegate.stream_did_stop(Some(message));
138                return;
139            }
140        }
141    }
142
143    // Fallback to logging if no delegate registered
144    eprintln!("SCStream error: {error}");
145}
146
147// C callback that retrieves handler from registry
148extern "C" fn sample_handler(
149    _stream: *const c_void,
150    sample_buffer: *const c_void,
151    output_type: i32,
152) {
153    // Mutex poisoning is unrecoverable in C callback context; unwrap is appropriate
154    let registry = HANDLER_REGISTRY.lock().unwrap();
155    if let Some(handlers) = registry.as_ref() {
156        if handlers.is_empty() {
157            // No handlers registered - release the buffer that Swift passed us
158            unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
159            return;
160        }
161
162        let output_type_enum = match output_type {
163            0 => SCStreamOutputType::Screen,
164            1 => SCStreamOutputType::Audio,
165            2 => SCStreamOutputType::Microphone,
166            _ => {
167                eprintln!("Unknown output type: {output_type}");
168                // Unknown type - release the buffer
169                unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
170                return;
171            }
172        };
173
174        let handler_count = handlers.len();
175
176        // Call all registered handlers
177        for (idx, (_id, entry)) in handlers.iter().enumerate() {
178            // Convert raw pointer to CMSampleBuffer
179            let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(sample_buffer.cast_mut()) };
180
181            // For all handlers except the last, we need to retain the buffer
182            if idx < handler_count - 1 {
183                // Retain the buffer so it's not released when this handler's buffer is dropped
184                unsafe { crate::cm::ffi::cm_sample_buffer_retain(sample_buffer.cast_mut()) };
185            }
186            // The last handler will release the original retained reference from Swift
187
188            entry
189                .handler
190                .did_output_sample_buffer(buffer, output_type_enum);
191        }
192    } else {
193        // No registry - release the buffer
194        unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
195    }
196}
197
198/// `SCStream` is a lightweight wrapper around the Swift `SCStream` instance.
199/// It provides direct FFI access to `ScreenCaptureKit` functionality.
200///
201/// This is the primary and only implementation of `SCStream` in v1.0+.
202/// All `ScreenCaptureKit` operations go through Swift FFI bindings.
203///
204/// # Examples
205///
206/// ```no_run
207/// use screencapturekit::prelude::*;
208///
209/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
210/// // Get shareable content
211/// let content = SCShareableContent::get()?;
212/// let display = &content.displays()[0];
213///
214/// // Create filter and configuration
215/// let filter = SCContentFilter::create()
216///     .with_display(display)
217///     .with_excluding_windows(&[])
218///     .build();
219/// let config = SCStreamConfiguration::new()
220///     .with_width(1920)
221///     .with_height(1080);
222///
223/// // Create and start stream
224/// let mut stream = SCStream::new(&filter, &config);
225/// stream.start_capture()?;
226///
227/// // ... capture frames ...
228///
229/// stream.stop_capture()?;
230/// # Ok(())
231/// # }
232/// ```
233pub struct SCStream {
234    ptr: *const c_void,
235    /// Handler IDs registered by this stream instance, keyed by output type
236    handler_ids: Vec<(usize, SCStreamOutputType)>,
237}
238
239unsafe impl Send for SCStream {}
240unsafe impl Sync for SCStream {}
241
242impl SCStream {
243    /// Create a new stream with a content filter and configuration
244    ///
245    /// # Examples
246    ///
247    /// ```no_run
248    /// use screencapturekit::prelude::*;
249    ///
250    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
251    /// let content = SCShareableContent::get()?;
252    /// let display = &content.displays()[0];
253    /// let filter = SCContentFilter::create()
254    ///     .with_display(display)
255    ///     .with_excluding_windows(&[])
256    ///     .build();
257    /// let config = SCStreamConfiguration::new()
258    ///     .with_width(1920)
259    ///     .with_height(1080);
260    ///
261    /// let stream = SCStream::new(&filter, &config);
262    /// # Ok(())
263    /// # }
264    /// ```
265    pub fn new(filter: &SCContentFilter, configuration: &SCStreamConfiguration) -> Self {
266        extern "C" fn error_callback(_stream: *const c_void, error_code: i32, msg: *const i8) {
267            let message = if msg.is_null() {
268                "Unknown error"
269            } else {
270                unsafe { CStr::from_ptr(msg) }
271                    .to_str()
272                    .unwrap_or("Unknown error")
273            };
274
275            if error_code != 0 {
276                if let Some(code) = crate::error::SCStreamErrorCode::from_raw(error_code) {
277                    eprintln!("SCStream error ({code}): {message}");
278                } else {
279                    eprintln!("SCStream error (code {error_code}): {message}");
280                }
281            } else {
282                eprintln!("SCStream error: {message}");
283            }
284        }
285        let ptr = unsafe {
286            ffi::sc_stream_create(filter.as_ptr(), configuration.as_ptr(), error_callback)
287        };
288        // Note: The Swift bridge should never return null for a valid filter/config,
289        // but we handle it gracefully by creating an empty stream that will fail on use.
290        // This maintains API compatibility while being more defensive.
291        Self {
292            ptr,
293            handler_ids: Vec::new(),
294        }
295    }
296
297    /// Create a new stream with a content filter, configuration, and delegate
298    ///
299    /// The delegate receives callbacks for stream lifecycle events:
300    /// - `did_stop_with_error` - Called when the stream stops due to an error
301    /// - `stream_did_stop` - Called when the stream stops (with optional error message)
302    ///
303    /// # Panics
304    ///
305    /// Panics if the internal delegate registry mutex is poisoned.
306    ///
307    /// # Examples
308    ///
309    /// ```no_run
310    /// use screencapturekit::prelude::*;
311    /// use screencapturekit::stream::delegate_trait::StreamCallbacks;
312    ///
313    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
314    /// let content = SCShareableContent::get()?;
315    /// let display = &content.displays()[0];
316    /// let filter = SCContentFilter::create()
317    ///     .with_display(display)
318    ///     .with_excluding_windows(&[])
319    ///     .build();
320    /// let config = SCStreamConfiguration::new()
321    ///     .with_width(1920)
322    ///     .with_height(1080);
323    ///
324    /// let delegate = StreamCallbacks::new()
325    ///     .on_error(|e| eprintln!("Stream error: {}", e))
326    ///     .on_stop(|err| {
327    ///         if let Some(msg) = err {
328    ///             eprintln!("Stream stopped with error: {}", msg);
329    ///         }
330    ///     });
331    ///
332    /// let stream = SCStream::new_with_delegate(&filter, &config, delegate);
333    /// stream.start_capture()?;
334    /// # Ok(())
335    /// # }
336    /// ```
337    pub fn new_with_delegate(
338        filter: &SCContentFilter,
339        configuration: &SCStreamConfiguration,
340        delegate: impl SCStreamDelegateTrait + 'static,
341    ) -> Self {
342        let ptr = unsafe {
343            ffi::sc_stream_create(
344                filter.as_ptr(),
345                configuration.as_ptr(),
346                delegate_error_callback,
347            )
348        };
349
350        // Store delegate in registry keyed by stream pointer
351        if !ptr.is_null() {
352            let stream_key = ptr as usize;
353            let mut registry = DELEGATE_REGISTRY.lock().unwrap();
354            if registry.is_none() {
355                *registry = Some(HashMap::new());
356            }
357            registry.as_mut().unwrap().insert(
358                stream_key,
359                DelegateEntry {
360                    delegate: Box::new(delegate),
361                    ref_count: 1,
362                },
363            );
364        }
365
366        Self {
367            ptr,
368            handler_ids: Vec::new(),
369        }
370    }
371
372    /// Add an output handler to receive captured frames
373    ///
374    /// # Arguments
375    ///
376    /// * `handler` - The handler to receive callbacks. Can be:
377    ///   - A struct implementing [`SCStreamOutputTrait`]
378    ///   - A closure `|CMSampleBuffer, SCStreamOutputType| { ... }`
379    /// * `of_type` - The type of output to receive (Screen, Audio, or Microphone)
380    ///
381    /// # Returns
382    ///
383    /// Returns `Some(handler_id)` on success, `None` on failure.
384    /// The handler ID can be used with [`remove_output_handler`](Self::remove_output_handler).
385    ///
386    /// # Examples
387    ///
388    /// Using a struct:
389    /// ```rust,no_run
390    /// use screencapturekit::prelude::*;
391    ///
392    /// struct MyHandler;
393    /// impl SCStreamOutputTrait for MyHandler {
394    ///     fn did_output_sample_buffer(&self, _sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
395    ///         println!("Got frame!");
396    ///     }
397    /// }
398    ///
399    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
400    /// # let content = SCShareableContent::get()?;
401    /// # let display = &content.displays()[0];
402    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
403    /// # let config = SCStreamConfiguration::default();
404    /// let mut stream = SCStream::new(&filter, &config);
405    /// stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);
406    /// # Ok(())
407    /// # }
408    /// ```
409    ///
410    /// Using a closure:
411    /// ```rust,no_run
412    /// use screencapturekit::prelude::*;
413    ///
414    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
415    /// # let content = SCShareableContent::get()?;
416    /// # let display = &content.displays()[0];
417    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
418    /// # let config = SCStreamConfiguration::default();
419    /// let mut stream = SCStream::new(&filter, &config);
420    /// stream.add_output_handler(
421    ///     |_sample, _type| println!("Got frame!"),
422    ///     SCStreamOutputType::Screen
423    /// );
424    /// # Ok(())
425    /// # }
426    /// ```
427    pub fn add_output_handler(
428        &mut self,
429        handler: impl SCStreamOutputTrait + 'static,
430        of_type: SCStreamOutputType,
431    ) -> Option<usize> {
432        self.add_output_handler_with_queue(handler, of_type, None)
433    }
434
435    /// Add an output handler with a custom dispatch queue
436    ///
437    /// This allows controlling which thread/queue the handler is called on.
438    ///
439    /// # Arguments
440    ///
441    /// * `handler` - The handler to receive callbacks
442    /// * `of_type` - The type of output to receive
443    /// * `queue` - Optional custom dispatch queue for callbacks
444    ///
445    /// # Panics
446    ///
447    /// Panics if the internal handler registry mutex is poisoned.
448    ///
449    /// # Examples
450    ///
451    /// ```rust,no_run
452    /// use screencapturekit::prelude::*;
453    /// use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
454    ///
455    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
456    /// # let content = SCShareableContent::get()?;
457    /// # let display = &content.displays()[0];
458    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
459    /// # let config = SCStreamConfiguration::default();
460    /// let mut stream = SCStream::new(&filter, &config);
461    /// let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
462    ///
463    /// stream.add_output_handler_with_queue(
464    ///     |_sample, _type| println!("Got frame on custom queue!"),
465    ///     SCStreamOutputType::Screen,
466    ///     Some(&queue)
467    /// );
468    /// # Ok(())
469    /// # }
470    /// ```
471    pub fn add_output_handler_with_queue(
472        &mut self,
473        handler: impl SCStreamOutputTrait + 'static,
474        of_type: SCStreamOutputType,
475        queue: Option<&DispatchQueue>,
476    ) -> Option<usize> {
477        // Get next handler ID
478        let handler_id = {
479            // Mutex poisoning is unrecoverable; unwrap is appropriate
480            let mut id_lock = NEXT_HANDLER_ID.lock().unwrap();
481            let id = *id_lock;
482            *id_lock += 1;
483            id
484        };
485
486        // Store handler in registry
487        {
488            // Mutex poisoning is unrecoverable; unwrap is appropriate
489            let mut registry = HANDLER_REGISTRY.lock().unwrap();
490            if registry.is_none() {
491                *registry = Some(HashMap::new());
492            }
493            // We just ensured registry is Some above
494            registry.as_mut().unwrap().insert(
495                handler_id,
496                HandlerEntry {
497                    handler: Box::new(handler),
498                    ref_count: 1,
499                },
500            );
501        }
502
503        // Convert output type to int for Swift
504        let output_type_int = match of_type {
505            SCStreamOutputType::Screen => 0,
506            SCStreamOutputType::Audio => 1,
507            SCStreamOutputType::Microphone => 2,
508        };
509
510        let ok = if let Some(q) = queue {
511            unsafe {
512                ffi::sc_stream_add_stream_output_with_queue(
513                    self.ptr,
514                    output_type_int,
515                    sample_handler,
516                    q.as_ptr(),
517                )
518            }
519        } else {
520            unsafe { ffi::sc_stream_add_stream_output(self.ptr, output_type_int, sample_handler) }
521        };
522
523        if ok {
524            self.handler_ids.push((handler_id, of_type));
525            Some(handler_id)
526        } else {
527            // Remove from registry since Swift rejected it
528            HANDLER_REGISTRY
529                .lock()
530                .unwrap()
531                .as_mut()
532                .map(|handlers| handlers.remove(&handler_id));
533            None
534        }
535    }
536
537    /// Remove an output handler
538    ///
539    /// # Arguments
540    ///
541    /// * `id` - The handler ID returned from [`add_output_handler`](Self::add_output_handler)
542    /// * `of_type` - The type of output the handler was registered for
543    ///
544    /// # Panics
545    ///
546    /// Panics if the internal handler registry mutex is poisoned.
547    ///
548    /// # Returns
549    ///
550    /// Returns `true` if the handler was found and removed, `false` otherwise.
551    pub fn remove_output_handler(&mut self, id: usize, of_type: SCStreamOutputType) -> bool {
552        // Remove from our tracking
553        let Some(pos) = self.handler_ids.iter().position(|(hid, _)| *hid == id) else {
554            return false;
555        };
556        self.handler_ids.remove(pos);
557
558        // Decrement ref count in global registry, remove from Swift if this was the last reference
559        if decrement_handler_ref_count(id) {
560            let output_type_int = match of_type {
561                SCStreamOutputType::Screen => 0,
562                SCStreamOutputType::Audio => 1,
563                SCStreamOutputType::Microphone => 2,
564            };
565            unsafe { ffi::sc_stream_remove_stream_output(self.ptr, output_type_int) }
566        } else {
567            true
568        }
569    }
570
571    /// Start capturing screen content
572    ///
573    /// This method blocks until the capture operation completes or fails.
574    ///
575    /// # Errors
576    ///
577    /// Returns `SCError::CaptureStartFailed` if the capture fails to start.
578    pub fn start_capture(&self) -> Result<(), SCError> {
579        let (completion, context) = UnitCompletion::new();
580        unsafe { ffi::sc_stream_start_capture(self.ptr, context, UnitCompletion::callback) };
581        completion.wait().map_err(SCError::CaptureStartFailed)
582    }
583
584    /// Stop capturing screen content
585    ///
586    /// This method blocks until the capture operation completes or fails.
587    ///
588    /// # Errors
589    ///
590    /// Returns `SCError::CaptureStopFailed` if the capture fails to stop.
591    pub fn stop_capture(&self) -> Result<(), SCError> {
592        let (completion, context) = UnitCompletion::new();
593        unsafe { ffi::sc_stream_stop_capture(self.ptr, context, UnitCompletion::callback) };
594        completion.wait().map_err(SCError::CaptureStopFailed)
595    }
596
597    /// Update the stream configuration
598    ///
599    /// This method blocks until the configuration update completes or fails.
600    ///
601    /// # Errors
602    ///
603    /// Returns `SCError::StreamError` if the configuration update fails.
604    pub fn update_configuration(
605        &self,
606        configuration: &SCStreamConfiguration,
607    ) -> Result<(), SCError> {
608        let (completion, context) = UnitCompletion::new();
609        unsafe {
610            ffi::sc_stream_update_configuration(
611                self.ptr,
612                configuration.as_ptr(),
613                context,
614                UnitCompletion::callback,
615            );
616        }
617        completion.wait().map_err(SCError::StreamError)
618    }
619
620    /// Update the content filter
621    ///
622    /// This method blocks until the filter update completes or fails.
623    ///
624    /// # Errors
625    ///
626    /// Returns `SCError::StreamError` if the filter update fails.
627    pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
628        let (completion, context) = UnitCompletion::new();
629        unsafe {
630            ffi::sc_stream_update_content_filter(
631                self.ptr,
632                filter.as_ptr(),
633                context,
634                UnitCompletion::callback,
635            );
636        }
637        completion.wait().map_err(SCError::StreamError)
638    }
639
640    /// Get the synchronization clock for this stream (macOS 13.0+)
641    ///
642    /// Returns the `CMClock` used to synchronize the stream's output.
643    /// This is useful for coordinating multiple streams or synchronizing
644    /// with other media.
645    ///
646    /// Returns `None` if the clock is not available (e.g., stream not started
647    /// or macOS version too old).
648    #[cfg(feature = "macos_13_0")]
649    pub fn synchronization_clock(&self) -> Option<crate::cm::CMClock> {
650        let ptr = unsafe { ffi::sc_stream_get_synchronization_clock(self.ptr) };
651        if ptr.is_null() {
652            None
653        } else {
654            Some(crate::cm::CMClock::from_ptr(ptr))
655        }
656    }
657
658    /// Add a recording output to the stream (macOS 15.0+)
659    ///
660    /// Starts recording if the stream is already capturing, otherwise recording
661    /// will start when capture begins. The recording is written to the file URL
662    /// specified in the `SCRecordingOutputConfiguration`.
663    ///
664    /// # Errors
665    ///
666    /// Returns `SCError::StreamError` if adding the recording output fails.
667    #[cfg(feature = "macos_15_0")]
668    pub fn add_recording_output(
669        &self,
670        recording_output: &crate::recording_output::SCRecordingOutput,
671    ) -> Result<(), SCError> {
672        let (completion, context) = UnitCompletion::new();
673        unsafe {
674            ffi::sc_stream_add_recording_output(
675                self.ptr,
676                recording_output.as_ptr(),
677                UnitCompletion::callback,
678                context,
679            );
680        }
681        completion.wait().map_err(SCError::StreamError)
682    }
683
684    /// Remove a recording output from the stream (macOS 15.0+)
685    ///
686    /// Stops recording if the stream is currently recording.
687    ///
688    /// # Errors
689    ///
690    /// Returns `SCError::StreamError` if removing the recording output fails.
691    #[cfg(feature = "macos_15_0")]
692    pub fn remove_recording_output(
693        &self,
694        recording_output: &crate::recording_output::SCRecordingOutput,
695    ) -> Result<(), SCError> {
696        let (completion, context) = UnitCompletion::new();
697        unsafe {
698            ffi::sc_stream_remove_recording_output(
699                self.ptr,
700                recording_output.as_ptr(),
701                UnitCompletion::callback,
702                context,
703            );
704        }
705        completion.wait().map_err(SCError::StreamError)
706    }
707
708    /// Returns the raw pointer to the underlying Swift `SCStream` instance.
709    #[allow(dead_code)]
710    pub(crate) fn as_ptr(&self) -> *const c_void {
711        self.ptr
712    }
713}
714
715impl Drop for SCStream {
716    fn drop(&mut self) {
717        // Clean up all registered handlers (decrement ref counts)
718        for (id, of_type) in std::mem::take(&mut self.handler_ids) {
719            if decrement_handler_ref_count(id) {
720                // This was the last reference, tell Swift to remove the output
721                let output_type_int = match of_type {
722                    SCStreamOutputType::Screen => 0,
723                    SCStreamOutputType::Audio => 1,
724                    SCStreamOutputType::Microphone => 2,
725                };
726                unsafe { ffi::sc_stream_remove_stream_output(self.ptr, output_type_int) };
727            }
728        }
729
730        // Clean up delegate from registry (decrement ref count)
731        if !self.ptr.is_null() {
732            decrement_delegate_ref_count(self.ptr as usize);
733        }
734
735        if !self.ptr.is_null() {
736            unsafe { ffi::sc_stream_release(self.ptr) };
737        }
738    }
739}
740
741impl Clone for SCStream {
742    /// Clone the stream reference.
743    ///
744    /// Cloning an `SCStream` creates a new reference to the same underlying
745    /// Swift `SCStream` object. The cloned stream shares the same handlers
746    /// as the original - they receive frames from the same capture session.
747    ///
748    /// Both the original and cloned stream share the same capture state, so:
749    /// - Starting capture on one affects both
750    /// - Stopping capture on one affects both
751    /// - Configuration updates affect both
752    /// - Handlers receive the same frames
753    ///
754    /// # Examples
755    ///
756    /// ```rust,no_run
757    /// use screencapturekit::prelude::*;
758    ///
759    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
760    /// # let content = SCShareableContent::get()?;
761    /// # let display = &content.displays()[0];
762    /// # let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
763    /// # let config = SCStreamConfiguration::default();
764    /// let mut stream = SCStream::new(&filter, &config);
765    /// stream.add_output_handler(|_, _| println!("Handler 1"), SCStreamOutputType::Screen);
766    ///
767    /// // Clone shares the same handlers
768    /// let stream2 = stream.clone();
769    /// // Both stream and stream2 will receive frames via Handler 1
770    /// # Ok(())
771    /// # }
772    /// ```
773    fn clone(&self) -> Self {
774        // Increment delegate ref count if one exists for this stream
775        if !self.ptr.is_null() {
776            increment_delegate_ref_count(self.ptr as usize);
777        }
778
779        // Increment handler ref counts for all handlers this stream references
780        for (id, _) in &self.handler_ids {
781            increment_handler_ref_count(*id);
782        }
783
784        unsafe {
785            Self {
786                ptr: crate::ffi::sc_stream_retain(self.ptr),
787                handler_ids: self.handler_ids.clone(),
788            }
789        }
790    }
791}
792
793impl fmt::Debug for SCStream {
794    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795        f.debug_struct("SCStream")
796            .field("ptr", &self.ptr)
797            .field("handler_ids", &self.handler_ids)
798            .finish()
799    }
800}
801
802impl fmt::Display for SCStream {
803    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
804        write!(f, "SCStream")
805    }
806}