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}