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::utils::sync_completion::UnitCompletion;
13use crate::{
14    dispatch_queue::DispatchQueue,
15    ffi,
16    stream::{
17        configuration::SCStreamConfiguration, content_filter::SCContentFilter,
18        output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType,
19    },
20};
21
22// Global registry for output handlers
23static HANDLER_REGISTRY: Mutex<Option<HashMap<usize, Box<dyn SCStreamOutputTrait>>>> =
24    Mutex::new(None);
25static NEXT_HANDLER_ID: Mutex<usize> = Mutex::new(1);
26
27// C callback that retrieves handler from registry
28extern "C" fn sample_handler(
29    _stream: *const c_void,
30    sample_buffer: *const c_void,
31    output_type: i32,
32) {
33    // Mutex poisoning is unrecoverable in C callback context; unwrap is appropriate
34    let registry = HANDLER_REGISTRY.lock().unwrap();
35    if let Some(handlers) = registry.as_ref() {
36        if handlers.is_empty() {
37            return;
38        }
39
40        let output_type_enum = if output_type == 1 {
41            SCStreamOutputType::Audio
42        } else {
43            SCStreamOutputType::Screen
44        };
45
46        let handler_count = handlers.len();
47
48        // Call all registered handlers
49        for (idx, (_id, handler)) in handlers.iter().enumerate() {
50            // Convert raw pointer to CMSampleBuffer
51            let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(sample_buffer.cast_mut()) };
52
53            // For all handlers except the last, we need to retain the buffer
54            if idx < handler_count - 1 {
55                // Retain the buffer so it's not released when this handler's buffer is dropped
56                unsafe { crate::cm::ffi::cm_sample_buffer_retain(sample_buffer.cast_mut()) };
57            }
58            // The last handler will release the original retained reference from Swift
59
60            handler.did_output_sample_buffer(buffer, output_type_enum);
61        }
62    }
63}
64
65/// `SCStream` is a lightweight wrapper around the Swift `SCStream` instance.
66/// It provides direct FFI access to `ScreenCaptureKit` functionality.
67///
68/// This is the primary and only implementation of `SCStream` in v1.0+.
69/// All `ScreenCaptureKit` operations go through Swift FFI bindings.
70///
71/// # Examples
72///
73/// ```no_run
74/// use screencapturekit::prelude::*;
75///
76/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
77/// // Get shareable content
78/// let content = SCShareableContent::get()?;
79/// let display = &content.displays()[0];
80///
81/// // Create filter and configuration
82/// let filter = SCContentFilter::builder()
83///     .display(display)
84///     .exclude_windows(&[])
85///     .build();
86/// let mut config = SCStreamConfiguration::default();
87/// config.set_width(1920);
88/// config.set_height(1080);
89///
90/// // Create and start stream
91/// let mut stream = SCStream::new(&filter, &config);
92/// stream.start_capture()?;
93///
94/// // ... capture frames ...
95///
96/// stream.stop_capture()?;
97/// # Ok(())
98/// # }
99/// ```
100pub struct SCStream {
101    ptr: *const c_void,
102}
103
104unsafe impl Send for SCStream {}
105unsafe impl Sync for SCStream {}
106
107impl SCStream {
108    /// Create a new stream with a content filter and configuration
109    ///
110    /// # Panics
111    ///
112    /// Panics if the Swift bridge returns a null stream pointer.
113    ///
114    /// # Examples
115    ///
116    /// ```no_run
117    /// use screencapturekit::prelude::*;
118    ///
119    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
120    /// let content = SCShareableContent::get()?;
121    /// let display = &content.displays()[0];
122    /// let filter = SCContentFilter::builder()
123    ///     .display(display)
124    ///     .exclude_windows(&[])
125    ///     .build();
126    /// let config = SCStreamConfiguration::default();
127    ///
128    /// let stream = SCStream::new(&filter, &config);
129    /// # Ok(())
130    /// # }
131    /// ```
132    pub fn new(filter: &SCContentFilter, configuration: &SCStreamConfiguration) -> Self {
133        extern "C" fn error_callback(_stream: *const c_void, msg: *const i8) {
134            if !msg.is_null() {
135                if let Ok(s) = unsafe { CStr::from_ptr(msg) }.to_str() {
136                    eprintln!("SCStream error: {s}");
137                }
138            }
139        }
140        let ptr = unsafe {
141            ffi::sc_stream_create(filter.as_ptr(), configuration.as_ptr(), error_callback)
142        };
143        assert!(!ptr.is_null(), "Swift bridge returned null stream");
144        Self { ptr }
145    }
146
147    pub fn new_with_delegate(
148        filter: &SCContentFilter,
149        configuration: &SCStreamConfiguration,
150        _delegate: impl crate::stream::delegate_trait::SCStreamDelegateTrait,
151    ) -> Self {
152        // Delegate callbacks not yet mapped in bridge version; stored for API parity.
153        Self::new(filter, configuration)
154    }
155
156    /// Add an output handler to receive captured frames
157    ///
158    /// # Arguments
159    ///
160    /// * `handler` - The handler to receive callbacks. Can be:
161    ///   - A struct implementing [`SCStreamOutputTrait`]
162    ///   - A closure `|CMSampleBuffer, SCStreamOutputType| { ... }`
163    /// * `of_type` - The type of output to receive (Screen, Audio, or Microphone)
164    ///
165    /// # Returns
166    ///
167    /// Returns `Some(handler_id)` on success, `None` on failure.
168    /// The handler ID can be used with [`remove_output_handler`](Self::remove_output_handler).
169    ///
170    /// # Examples
171    ///
172    /// Using a struct:
173    /// ```rust,no_run
174    /// use screencapturekit::prelude::*;
175    ///
176    /// struct MyHandler;
177    /// impl SCStreamOutputTrait for MyHandler {
178    ///     fn did_output_sample_buffer(&self, _sample: CMSampleBuffer, _of_type: SCStreamOutputType) {
179    ///         println!("Got frame!");
180    ///     }
181    /// }
182    ///
183    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
184    /// # let content = SCShareableContent::get()?;
185    /// # let display = &content.displays()[0];
186    /// # let filter = SCContentFilter::builder().display(display).exclude_windows(&[]).build();
187    /// # let config = SCStreamConfiguration::default();
188    /// let mut stream = SCStream::new(&filter, &config);
189    /// stream.add_output_handler(MyHandler, SCStreamOutputType::Screen);
190    /// # Ok(())
191    /// # }
192    /// ```
193    ///
194    /// Using a closure:
195    /// ```rust,no_run
196    /// use screencapturekit::prelude::*;
197    ///
198    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
199    /// # let content = SCShareableContent::get()?;
200    /// # let display = &content.displays()[0];
201    /// # let filter = SCContentFilter::builder().display(display).exclude_windows(&[]).build();
202    /// # let config = SCStreamConfiguration::default();
203    /// let mut stream = SCStream::new(&filter, &config);
204    /// stream.add_output_handler(
205    ///     |_sample, _type| println!("Got frame!"),
206    ///     SCStreamOutputType::Screen
207    /// );
208    /// # Ok(())
209    /// # }
210    /// ```
211    pub fn add_output_handler(
212        &mut self,
213        handler: impl SCStreamOutputTrait + 'static,
214        of_type: SCStreamOutputType,
215    ) -> Option<usize> {
216        self.add_output_handler_with_queue(handler, of_type, None)
217    }
218
219    /// Add an output handler with a custom dispatch queue
220    ///
221    /// This allows controlling which thread/queue the handler is called on.
222    ///
223    /// # Arguments
224    ///
225    /// * `handler` - The handler to receive callbacks
226    /// * `of_type` - The type of output to receive
227    /// * `queue` - Optional custom dispatch queue for callbacks
228    ///
229    /// # Panics
230    ///
231    /// Panics if the internal handler registry mutex is poisoned.
232    ///
233    /// # Examples
234    ///
235    /// ```rust,no_run
236    /// use screencapturekit::prelude::*;
237    /// use screencapturekit::dispatch_queue::{DispatchQueue, DispatchQoS};
238    ///
239    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
240    /// # let content = SCShareableContent::get()?;
241    /// # let display = &content.displays()[0];
242    /// # let filter = SCContentFilter::builder().display(display).exclude_windows(&[]).build();
243    /// # let config = SCStreamConfiguration::default();
244    /// let mut stream = SCStream::new(&filter, &config);
245    /// let queue = DispatchQueue::new("com.myapp.capture", DispatchQoS::UserInteractive);
246    ///
247    /// stream.add_output_handler_with_queue(
248    ///     |_sample, _type| println!("Got frame on custom queue!"),
249    ///     SCStreamOutputType::Screen,
250    ///     Some(&queue)
251    /// );
252    /// # Ok(())
253    /// # }
254    /// ```
255    pub fn add_output_handler_with_queue(
256        &mut self,
257        handler: impl SCStreamOutputTrait + 'static,
258        of_type: SCStreamOutputType,
259        queue: Option<&DispatchQueue>,
260    ) -> Option<usize> {
261        // Get next handler ID
262        let handler_id = {
263            // Mutex poisoning is unrecoverable; unwrap is appropriate
264            let mut id_lock = NEXT_HANDLER_ID.lock().unwrap();
265            let id = *id_lock;
266            *id_lock += 1;
267            id
268        };
269
270        // Store handler in registry
271        {
272            // Mutex poisoning is unrecoverable; unwrap is appropriate
273            let mut registry = HANDLER_REGISTRY.lock().unwrap();
274            if registry.is_none() {
275                *registry = Some(HashMap::new());
276            }
277            // We just ensured registry is Some above
278            registry
279                .as_mut()
280                .unwrap()
281                .insert(handler_id, Box::new(handler));
282        }
283
284        // Convert output type to int for Swift
285        let output_type_int = match of_type {
286            SCStreamOutputType::Screen => 0,
287            SCStreamOutputType::Audio => 1,
288            SCStreamOutputType::Microphone => 2,
289        };
290
291        let ok = if let Some(q) = queue {
292            unsafe {
293                ffi::sc_stream_add_stream_output_with_queue(
294                    self.ptr,
295                    output_type_int,
296                    sample_handler,
297                    q.as_ptr(),
298                )
299            }
300        } else {
301            unsafe { ffi::sc_stream_add_stream_output(self.ptr, output_type_int, sample_handler) }
302        };
303
304        if ok {
305            Some(handler_id)
306        } else {
307            None
308        }
309    }
310
311    /// Remove an output handler
312    ///
313    /// # Arguments
314    ///
315    /// * `id` - The handler ID returned from [`add_output_handler`](Self::add_output_handler)
316    /// * `of_type` - The type of output the handler was registered for
317    ///
318    /// # Panics
319    ///
320    /// Panics if the internal handler registry mutex is poisoned.
321    ///
322    /// # Returns
323    ///
324    /// Returns `true` if the handler was found and removed, `false` otherwise.
325    pub fn remove_output_handler(&mut self, id: usize, _of_type: SCStreamOutputType) -> bool {
326        // Mutex poisoning is unrecoverable; unwrap is appropriate
327        let mut registry = HANDLER_REGISTRY.lock().unwrap();
328        registry
329            .as_mut()
330            .and_then(|handlers| handlers.remove(&id))
331            .is_some()
332    }
333
334    /// Start capturing screen content
335    ///
336    /// This method blocks until the capture operation completes or fails.
337    ///
338    /// # Errors
339    ///
340    /// Returns `SCError::CaptureStartFailed` if the capture fails to start.
341    pub fn start_capture(&self) -> Result<(), SCError> {
342        let (completion, context) = UnitCompletion::new();
343        unsafe { ffi::sc_stream_start_capture(self.ptr, context, UnitCompletion::callback) };
344        completion.wait().map_err(SCError::CaptureStartFailed)
345    }
346
347    /// Stop capturing screen content
348    ///
349    /// This method blocks until the capture operation completes or fails.
350    ///
351    /// # Errors
352    ///
353    /// Returns `SCError::CaptureStopFailed` if the capture fails to stop.
354    pub fn stop_capture(&self) -> Result<(), SCError> {
355        let (completion, context) = UnitCompletion::new();
356        unsafe { ffi::sc_stream_stop_capture(self.ptr, context, UnitCompletion::callback) };
357        completion.wait().map_err(SCError::CaptureStopFailed)
358    }
359
360    /// Update the stream configuration
361    ///
362    /// This method blocks until the configuration update completes or fails.
363    ///
364    /// # Errors
365    ///
366    /// Returns `SCError::StreamError` if the configuration update fails.
367    pub fn update_configuration(
368        &self,
369        configuration: &SCStreamConfiguration,
370    ) -> Result<(), SCError> {
371        let (completion, context) = UnitCompletion::new();
372        unsafe {
373            ffi::sc_stream_update_configuration(
374                self.ptr,
375                configuration.as_ptr(),
376                context,
377                UnitCompletion::callback,
378            );
379        }
380        completion.wait().map_err(SCError::StreamError)
381    }
382
383    /// Update the content filter
384    ///
385    /// This method blocks until the filter update completes or fails.
386    ///
387    /// # Errors
388    ///
389    /// Returns `SCError::StreamError` if the filter update fails.
390    pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
391        let (completion, context) = UnitCompletion::new();
392        unsafe {
393            ffi::sc_stream_update_content_filter(
394                self.ptr,
395                filter.as_ptr(),
396                context,
397                UnitCompletion::callback,
398            );
399        }
400        completion.wait().map_err(SCError::StreamError)
401    }
402}
403
404impl Drop for SCStream {
405    fn drop(&mut self) {
406        unsafe { ffi::sc_stream_release(self.ptr) };
407    }
408}
409
410impl Clone for SCStream {
411    fn clone(&self) -> Self {
412        unsafe {
413            Self {
414                ptr: crate::ffi::sc_stream_retain(self.ptr),
415            }
416        }
417    }
418}
419
420impl fmt::Debug for SCStream {
421    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422        f.debug_struct("SCStream").field("ptr", &self.ptr).finish()
423    }
424}
425
426impl fmt::Display for SCStream {
427    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428        write!(f, "SCStream")
429    }
430}