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}