use std::ffi::{c_void, CStr};
use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use crate::error::SCError;
use crate::stream::delegate_trait::SCStreamDelegateTrait;
use crate::utils::completion::{is_timeout_error, UnitCompletion};
use crate::utils::panic_safe::catch_user_panic;
use crate::{
dispatch_queue::DispatchQueue,
ffi,
stream::{
configuration::SCStreamConfiguration, content_filter::SCContentFilter,
output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType,
},
};
struct HandlerEntry {
id: usize,
of_type: SCStreamOutputType,
handler: Arc<dyn SCStreamOutputTrait>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum OutputQueue {
BridgeDefault,
Custom(usize),
}
struct StreamContext {
handlers: RwLock<Vec<HandlerEntry>>,
delegate: RwLock<Option<Arc<dyn SCStreamDelegateTrait>>>,
output_queues: RwLock<Vec<(SCStreamOutputType, OutputQueue)>>,
output_mutation: Mutex<()>,
capturing: Arc<AtomicBool>,
start_unconfirmed: Arc<AtomicBool>,
#[cfg(feature = "macos_15_0")]
recording_outputs: AtomicUsize,
ref_count: AtomicUsize,
}
impl StreamContext {
fn new(delegate: Option<Arc<dyn SCStreamDelegateTrait>>) -> *mut Self {
let ctx = Box::new(Self {
handlers: RwLock::new(Vec::new()),
delegate: RwLock::new(delegate),
output_queues: RwLock::new(Vec::new()),
output_mutation: Mutex::new(()),
capturing: Arc::new(AtomicBool::new(false)),
start_unconfirmed: Arc::new(AtomicBool::new(false)),
#[cfg(feature = "macos_15_0")]
recording_outputs: AtomicUsize::new(0),
ref_count: AtomicUsize::new(1),
});
Box::into_raw(ctx)
}
unsafe fn retain(ptr: *mut Self) {
unsafe { &*ptr }.ref_count.fetch_add(1, Ordering::Relaxed);
}
unsafe fn release(ptr: *mut Self) {
if ptr.is_null() {
return;
}
let prev = unsafe { &*ptr }.ref_count.fetch_sub(1, Ordering::Release);
if prev == 1 {
std::sync::atomic::fence(Ordering::Acquire);
drop(unsafe { Box::from_raw(ptr) });
}
}
fn delegate_snapshot(&self) -> Option<Arc<dyn SCStreamDelegateTrait>> {
self.delegate
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn handler_snapshot(&self, of_type: SCStreamOutputType) -> Vec<Arc<dyn SCStreamOutputTrait>> {
self.handlers
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.filter(|e| e.of_type == of_type)
.map(|e| Arc::clone(&e.handler))
.collect()
}
#[cfg(feature = "macos_15_0")]
fn has_handlers(&self) -> bool {
!self
.handlers
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
}
}
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<StreamContext>();
};
static NEXT_HANDLER_ID: AtomicUsize = AtomicUsize::new(1);
const fn native_output_type(of_type: SCStreamOutputType) -> i32 {
match of_type {
SCStreamOutputType::Screen => 0,
SCStreamOutputType::Audio => 1,
SCStreamOutputType::Microphone => 2,
}
}
extern "C" fn context_retain_cb(context: *mut c_void) {
if !context.is_null() {
unsafe { StreamContext::retain(context.cast::<StreamContext>()) };
}
}
extern "C" fn context_release_cb(context: *mut c_void) {
catch_user_panic("StreamContext::release", || unsafe {
StreamContext::release(context.cast::<StreamContext>());
});
}
extern "C" fn delegate_error_callback(context: *mut c_void, error_code: i32, msg: *const i8) {
if context.is_null() {
return;
}
let ctx = unsafe { &*(context.cast::<StreamContext>()) };
ctx.capturing.store(false, Ordering::Release);
let message = if msg.is_null() {
"Unknown error".to_string()
} else {
unsafe { CStr::from_ptr(msg) }
.to_str()
.unwrap_or("Unknown error")
.to_string()
};
let error = if error_code != 0 {
crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
|| SCError::StreamError(format!("{message} (code: {error_code})")),
|code| SCError::SCStreamError {
code,
message: Some(message.clone()),
},
)
} else {
SCError::StreamError(message)
};
let Some(delegate) = ctx.delegate_snapshot() else {
eprintln!("SCStream error: {error}");
return;
};
catch_user_panic("delegate.did_stop_with_error", || {
delegate.did_stop_with_error(error);
});
}
extern "C" fn delegate_event_callback(context: *mut c_void, event: i32) {
if context.is_null() {
return;
}
let ctx = unsafe { &*(context.cast::<StreamContext>()) };
let Some(delegate) = ctx.delegate_snapshot() else {
return;
};
catch_user_panic("delegate lifecycle event", || match event {
0 => delegate.stream_did_become_active(),
1 => delegate.stream_did_become_inactive(),
2 => delegate.output_video_effect_did_start_for_stream(),
3 => delegate.output_video_effect_did_stop_for_stream(),
other => eprintln!("SCStream: unknown delegate event code {other}"),
});
}
extern "C" fn sample_handler(context: *mut c_void, sample_buffer: *const c_void, output_type: i32) {
if sample_buffer.is_null() {
return;
}
if context.is_null() {
unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
return;
}
let ctx = unsafe { &*(context.cast::<StreamContext>()) };
let output_type_enum = match output_type {
0 => SCStreamOutputType::Screen,
1 => SCStreamOutputType::Audio,
2 => SCStreamOutputType::Microphone,
_ => {
eprintln!("Unknown output type: {output_type}");
unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
return;
}
};
let matching = ctx.handler_snapshot(output_type_enum);
if matching.is_empty() {
unsafe { crate::cm::ffi::cm_sample_buffer_release(sample_buffer.cast_mut()) };
return;
}
let last = matching.len() - 1;
for (index, handler) in matching.iter().enumerate() {
if index != last {
unsafe { crate::cm::ffi::cm_sample_buffer_retain(sample_buffer.cast_mut()) };
}
let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(sample_buffer.cast_mut()) };
catch_user_panic("output handler", || {
handler.did_output_sample_buffer(buffer, output_type_enum);
});
}
}
pub struct SCStream {
ptr: *const c_void,
context: *mut StreamContext,
}
unsafe impl Send for SCStream {}
unsafe impl Sync for SCStream {}
impl SCStream {
pub fn new(filter: &SCContentFilter, configuration: &SCStreamConfiguration) -> Self {
Self::create(filter, configuration, None)
}
pub fn new_with_delegate(
filter: &SCContentFilter,
configuration: &SCStreamConfiguration,
delegate: impl SCStreamDelegateTrait + 'static,
) -> Self {
Self::create(filter, configuration, Some(Arc::new(delegate)))
}
fn create(
filter: &SCContentFilter,
configuration: &SCStreamConfiguration,
delegate: Option<Arc<dyn SCStreamDelegateTrait>>,
) -> Self {
let context = StreamContext::new(delegate);
let context_ptr = context.cast::<c_void>();
let ptr = unsafe {
ffi::sc_stream_create(
filter.as_ptr(),
configuration.as_ptr(),
context_ptr,
delegate_error_callback,
sample_handler,
context_retain_cb,
context_release_cb,
)
};
if !ptr.is_null() {
unsafe { ffi::sc_stream_set_delegate_event_callback(ptr, delegate_event_callback) };
}
Self { ptr, context }
}
pub fn add_output_handler(
&mut self,
handler: impl SCStreamOutputTrait + 'static,
of_type: SCStreamOutputType,
) -> Option<usize> {
self.add_output_handler_with_queue(handler, of_type, None)
}
pub fn add_output_handler_with_queue(
&mut self,
handler: impl SCStreamOutputTrait + 'static,
of_type: SCStreamOutputType,
queue: Option<&DispatchQueue>,
) -> Option<usize> {
#[cfg(not(feature = "macos_15_0"))]
if of_type == SCStreamOutputType::Microphone {
eprintln!("SCStream: microphone output requires the macos_15_0 feature");
return None;
}
let requested = queue.map_or(OutputQueue::BridgeDefault, |q| {
OutputQueue::Custom(q.as_ptr() as usize)
});
let ctx = unsafe { &*self.context };
let _mutation_guard = ctx
.output_mutation
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut established = ctx
.output_queues
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(&(_, existing)) = established.iter().find(|(ty, _)| *ty == of_type) {
if queue.is_some() && existing != requested {
drop(established);
eprintln!(
"SCStream: refusing to add a {of_type:?} handler on a different dispatch \
queue — ScreenCaptureKit delivers every handler of one output type on the \
queue chosen by the first registration. Reuse that queue (pass None) or \
remove the existing {of_type:?} handlers first."
);
return None;
}
}
let output_type_int = native_output_type(of_type);
let ok = if let Some(q) = queue {
unsafe {
ffi::sc_stream_add_stream_output_with_queue(self.ptr, output_type_int, q.as_ptr())
}
} else {
unsafe { ffi::sc_stream_add_stream_output(self.ptr, output_type_int) }
};
if !ok {
drop(established);
eprintln!(
"SCStream: failed to register output handler for {of_type:?} \
(ScreenCaptureKit rejected addStreamOutput)"
);
return None;
}
if !established.iter().any(|(ty, _)| *ty == of_type) {
established.push((of_type, requested));
}
drop(established);
let handler_id = NEXT_HANDLER_ID.fetch_add(1, Ordering::Relaxed);
ctx.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(HandlerEntry {
id: handler_id,
of_type,
handler: Arc::new(handler),
});
Some(handler_id)
}
pub fn remove_output_handler(&mut self, id: usize, of_type: SCStreamOutputType) -> bool {
match self.try_remove_output_handler(id, of_type) {
Ok(removed) => removed,
Err(error) => {
eprintln!("SCStream: {error}");
false
}
}
}
pub fn try_remove_output_handler(
&mut self,
id: usize,
of_type: SCStreamOutputType,
) -> Result<bool, SCError> {
let ctx = unsafe { &*self.context };
let mutation_guard = ctx
.output_mutation
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut handlers = ctx
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let Some(pos) = handlers
.iter()
.position(|e| e.id == id && e.of_type == of_type)
else {
return Ok(false);
};
let removed_handler = handlers.remove(pos);
let has_type = handlers.iter().any(|e| e.of_type == of_type);
drop(handlers);
let result = if has_type {
Ok(true)
} else {
let removed = unsafe {
ffi::sc_stream_remove_stream_output(self.ptr, native_output_type(of_type))
};
if removed {
ctx.output_queues
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.retain(|(ty, _)| *ty != of_type);
Ok(true)
} else {
Err(SCError::StreamError(format!(
"ScreenCaptureKit rejected removeStreamOutput for {of_type:?}; the handler was \
detached but the native output is still registered"
)))
}
};
drop(mutation_guard);
drop(removed_handler);
result
}
pub fn start_capture(&self) -> Result<(), SCError> {
let context = unsafe { &*self.context };
if !claim_start(&context.capturing, &context.start_unconfirmed) {
return Ok(());
}
let (completion, context) = UnitCompletion::new();
unsafe { ffi::sc_stream_start_capture(self.ptr, context, UnitCompletion::callback) };
match completion.wait() {
Ok(()) => Ok(()),
Err(error) => {
let context = unsafe { &*self.context };
if is_timeout_error(&error) {
context.start_unconfirmed.store(true, Ordering::Release);
} else {
context.capturing.store(false, Ordering::Release);
}
Err(SCError::CaptureStartFailed(error))
}
}
}
pub fn stop_capture(&self) -> Result<(), SCError> {
let context = unsafe { &*self.context };
if !context.capturing.swap(false, Ordering::AcqRel) {
return Ok(());
}
let (completion, context) = UnitCompletion::new();
unsafe { ffi::sc_stream_stop_capture(self.ptr, context, UnitCompletion::callback) };
if let Err(error) = completion.wait() {
unsafe { &*self.context }
.capturing
.store(true, Ordering::Release);
return Err(SCError::CaptureStopFailed(error));
}
Ok(())
}
#[cfg(feature = "macos_14_0")]
pub fn update_configuration(
&self,
configuration: &SCStreamConfiguration,
) -> Result<(), SCError> {
let (completion, context) = UnitCompletion::new();
unsafe {
ffi::sc_stream_update_configuration(
self.ptr,
configuration.as_ptr(),
context,
UnitCompletion::callback,
);
}
completion.wait().map_err(SCError::StreamError)
}
pub fn update_content_filter(&self, filter: &SCContentFilter) -> Result<(), SCError> {
let (completion, context) = UnitCompletion::new();
unsafe {
ffi::sc_stream_update_content_filter(
self.ptr,
filter.as_ptr(),
context,
UnitCompletion::callback,
);
}
completion.wait().map_err(SCError::StreamError)
}
#[cfg(feature = "macos_13_0")]
pub fn synchronization_clock(&self) -> Option<crate::cm::CMClock> {
let ptr = unsafe { ffi::sc_stream_get_synchronization_clock(self.ptr) };
crate::cm::CMClock::from_raw(ptr)
}
#[cfg(feature = "macos_15_0")]
pub fn add_recording_output(
&self,
recording_output: &crate::recording_output::SCRecordingOutput,
) -> Result<(), SCError> {
let stream_context = unsafe { &*self.context };
let (completion, context) = UnitCompletion::new();
unsafe {
ffi::sc_stream_add_recording_output(
self.ptr,
recording_output.as_ptr(),
UnitCompletion::callback,
context,
);
}
completion.wait().map_err(SCError::StreamError)?;
stream_context
.recording_outputs
.fetch_add(1, Ordering::AcqRel);
Ok(())
}
#[cfg(feature = "macos_15_0")]
pub fn remove_recording_output(
&self,
recording_output: &crate::recording_output::SCRecordingOutput,
) -> Result<(), SCError> {
let context = unsafe { &*self.context };
if context.capturing.load(Ordering::Acquire)
&& !context.has_handlers()
&& context.recording_outputs.load(Ordering::Acquire) == 1
{
self.stop_capture()?;
let (completion, completion_context) = UnitCompletion::new();
unsafe {
ffi::sc_recording_output_wait_until_terminal(
recording_output.as_ptr(),
completion_context,
UnitCompletion::callback,
);
}
completion.wait().map_err(SCError::StreamError)?;
}
let (completion, completion_context) = UnitCompletion::new();
unsafe {
ffi::sc_stream_remove_recording_output(
self.ptr,
recording_output.as_ptr(),
UnitCompletion::callback,
completion_context,
);
}
let outcome = completion.wait();
let removed = match &outcome {
Ok(()) => true,
Err(error) => is_timeout_error(error),
};
if removed {
context
.recording_outputs
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
Some(count.saturating_sub(1))
})
.ok();
}
outcome.map_err(SCError::StreamError)?;
Ok(())
}
#[allow(dead_code)]
pub(crate) fn as_ptr(&self) -> *const c_void {
self.ptr
}
#[cfg(feature = "async")]
pub(crate) fn capture_state(&self) -> Arc<AtomicBool> {
Arc::clone(&unsafe { &*self.context }.capturing)
}
#[cfg(feature = "async")]
pub(crate) fn start_unconfirmed_state(&self) -> Arc<AtomicBool> {
Arc::clone(&unsafe { &*self.context }.start_unconfirmed)
}
}
pub(crate) fn claim_start(capturing: &AtomicBool, start_unconfirmed: &AtomicBool) -> bool {
let was_capturing = capturing.swap(true, Ordering::AcqRel);
let unconfirmed = start_unconfirmed.swap(false, Ordering::AcqRel);
!was_capturing || unconfirmed
}
impl Drop for SCStream {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe { ffi::sc_stream_release(self.ptr) };
}
unsafe { StreamContext::release(self.context) };
}
}
impl Clone for SCStream {
fn clone(&self) -> Self {
unsafe { StreamContext::retain(self.context) };
Self {
ptr: unsafe { crate::ffi::sc_stream_retain(self.ptr) },
context: self.context,
}
}
}
impl fmt::Debug for SCStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SCStream")
.field("ptr", &self.ptr)
.finish_non_exhaustive()
}
}
impl fmt::Display for SCStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SCStream")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
#[test]
fn test_per_stream_callback_isolation() {
let count_a = Arc::new(AtomicUsize::new(0));
let count_b = Arc::new(AtomicUsize::new(0));
let ctx_a = StreamContext::new(None);
let ctx_b = StreamContext::new(None);
{
let counter = count_a.clone();
let mut handlers = unsafe { &*ctx_a }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handlers.push(HandlerEntry {
id: 1,
of_type: SCStreamOutputType::Audio,
handler: Arc::new(
move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
counter.fetch_add(1, Ordering::Relaxed);
std::mem::forget(buf);
},
),
});
}
{
let counter = count_b.clone();
let mut handlers = unsafe { &*ctx_b }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handlers.push(HandlerEntry {
id: 2,
of_type: SCStreamOutputType::Audio,
handler: Arc::new(
move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
counter.fetch_add(1, Ordering::Relaxed);
std::mem::forget(buf);
},
),
});
}
for _ in 0..5 {
let handlers = unsafe { &*ctx_a }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for entry in handlers
.iter()
.filter(|e| e.of_type == SCStreamOutputType::Audio)
{
let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
entry
.handler
.did_output_sample_buffer(buf, SCStreamOutputType::Audio);
}
}
for _ in 0..3 {
let handlers = unsafe { &*ctx_b }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for entry in handlers
.iter()
.filter(|e| e.of_type == SCStreamOutputType::Audio)
{
let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
entry
.handler
.did_output_sample_buffer(buf, SCStreamOutputType::Audio);
}
}
assert_eq!(
count_a.load(Ordering::Relaxed),
5,
"handler A received callbacks meant for B (cross-stream leak)"
);
assert_eq!(
count_b.load(Ordering::Relaxed),
3,
"handler B received callbacks meant for A (cross-stream leak)"
);
unsafe {
StreamContext::release(ctx_a);
StreamContext::release(ctx_b);
}
}
#[test]
fn test_handler_output_type_filtering() {
let screen_count = Arc::new(AtomicUsize::new(0));
let audio_count = Arc::new(AtomicUsize::new(0));
let ctx = StreamContext::new(None);
{
let counter = screen_count.clone();
let mut handlers = unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handlers.push(HandlerEntry {
id: 1,
of_type: SCStreamOutputType::Screen,
handler: Arc::new(
move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
counter.fetch_add(1, Ordering::Relaxed);
std::mem::forget(buf);
},
),
});
}
{
let counter = audio_count.clone();
let mut handlers = unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handlers.push(HandlerEntry {
id: 2,
of_type: SCStreamOutputType::Audio,
handler: Arc::new(
move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
counter.fetch_add(1, Ordering::Relaxed);
std::mem::forget(buf);
},
),
});
}
for _ in 0..4 {
let handlers = unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for entry in handlers
.iter()
.filter(|e| e.of_type == SCStreamOutputType::Screen)
{
let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
entry
.handler
.did_output_sample_buffer(buf, SCStreamOutputType::Screen);
}
}
for _ in 0..2 {
let handlers = unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for entry in handlers
.iter()
.filter(|e| e.of_type == SCStreamOutputType::Audio)
{
let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
entry
.handler
.did_output_sample_buffer(buf, SCStreamOutputType::Audio);
}
}
assert_eq!(screen_count.load(Ordering::Relaxed), 4);
assert_eq!(audio_count.load(Ordering::Relaxed), 2);
unsafe { StreamContext::release(ctx) };
}
#[test]
fn test_stream_context_ref_counting() {
let ctx = StreamContext::new(None);
assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
unsafe { StreamContext::retain(ctx) };
assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 2);
unsafe { StreamContext::release(ctx) };
assert_eq!(unsafe { &*ctx }.ref_count.load(Ordering::Relaxed), 1);
unsafe { StreamContext::release(ctx) };
}
#[test]
fn test_panic_in_handler_is_isolated() {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let panicked_count = Arc::new(AtomicUsize::new(0));
let normal_count = Arc::new(AtomicUsize::new(0));
let ctx = StreamContext::new(None);
{
let counter = panicked_count.clone();
let mut handlers = unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handlers.push(HandlerEntry {
id: 1,
of_type: SCStreamOutputType::Audio,
handler: Arc::new(
move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
counter.fetch_add(1, Ordering::Relaxed);
std::mem::forget(buf);
panic!("intentional test panic");
},
),
});
}
{
let counter = normal_count.clone();
let mut handlers = unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handlers.push(HandlerEntry {
id: 2,
of_type: SCStreamOutputType::Audio,
handler: Arc::new(
move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
counter.fetch_add(1, Ordering::Relaxed);
std::mem::forget(buf);
},
),
});
}
for _ in 0..5 {
let handlers = unsafe { &*ctx }
.handlers
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for entry in handlers
.iter()
.filter(|e| e.of_type == SCStreamOutputType::Audio)
{
let buf = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
catch_user_panic("test handler", || {
entry
.handler
.did_output_sample_buffer(buf, SCStreamOutputType::Audio);
});
}
}
assert_eq!(
panicked_count.load(Ordering::Relaxed),
5,
"panicking handler stopped firing after first panic"
);
assert_eq!(
normal_count.load(Ordering::Relaxed),
5,
"well-behaved handler stopped firing after panicker poisoned state"
);
drop(
unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
unsafe { StreamContext::release(ctx) };
std::panic::set_hook(original_hook);
}
#[test]
fn test_handler_may_take_the_write_lock_from_inside_a_callback() {
struct Reentrant(*mut StreamContext);
unsafe impl Send for Reentrant {}
unsafe impl Sync for Reentrant {}
impl SCStreamOutputTrait for Reentrant {
fn did_output_sample_buffer(
&self,
buffer: crate::cm::CMSampleBuffer,
_of_type: SCStreamOutputType,
) {
std::mem::forget(buffer);
let mut handlers = unsafe { &*self.0 }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handlers.retain(|e| e.id != 1);
}
}
let ctx = StreamContext::new(None);
unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(HandlerEntry {
id: 1,
of_type: SCStreamOutputType::Screen,
handler: Arc::new(Reentrant(ctx)),
});
for handler in unsafe { &*ctx }.handler_snapshot(SCStreamOutputType::Screen) {
let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
handler.did_output_sample_buffer(buffer, SCStreamOutputType::Screen);
}
assert!(
unsafe { &*ctx }
.handler_snapshot(SCStreamOutputType::Screen)
.is_empty(),
"handler failed to remove itself from inside its own callback"
);
unsafe { StreamContext::release(ctx) };
}
#[test]
fn test_snapshot_keeps_a_concurrently_removed_handler_alive() {
let ctx = StreamContext::new(None);
let calls = Arc::new(AtomicUsize::new(0));
{
let counter = calls.clone();
unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(HandlerEntry {
id: 1,
of_type: SCStreamOutputType::Audio,
handler: Arc::new(
move |buf: crate::cm::CMSampleBuffer, _ty: SCStreamOutputType| {
counter.fetch_add(1, Ordering::Relaxed);
std::mem::forget(buf);
},
),
});
}
let snapshot = unsafe { &*ctx }.handler_snapshot(SCStreamOutputType::Audio);
unsafe { &*ctx }
.handlers
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
for handler in &snapshot {
let buffer = unsafe { crate::cm::CMSampleBuffer::from_ptr(std::ptr::null_mut()) };
handler.did_output_sample_buffer(buffer, SCStreamOutputType::Audio);
}
assert_eq!(calls.load(Ordering::Relaxed), 1);
assert!(unsafe { &*ctx }
.handler_snapshot(SCStreamOutputType::Audio)
.is_empty());
unsafe { StreamContext::release(ctx) };
}
#[test]
fn test_delegate_snapshot_runs_user_code_unlocked() {
struct Counting(Arc<AtomicUsize>);
impl SCStreamDelegateTrait for Counting {
fn stream_did_become_active(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
let calls = Arc::new(AtomicUsize::new(0));
let ctx = StreamContext::new(Some(Arc::new(Counting(calls.clone()))));
let delegate = unsafe { &*ctx }
.delegate_snapshot()
.expect("delegate should be present");
assert!(unsafe { &*ctx }.delegate.try_write().is_ok());
delegate.stream_did_become_active();
assert_eq!(calls.load(Ordering::Relaxed), 1);
unsafe { StreamContext::release(ctx) };
}
#[test]
fn test_delegate_event_callback_routes_each_event_code() {
#[derive(Default)]
struct Recorder {
events: std::sync::Mutex<Vec<&'static str>>,
}
impl Recorder {
fn record(&self, what: &'static str) {
self.events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(what);
}
}
impl SCStreamDelegateTrait for Arc<Recorder> {
fn stream_did_become_active(&self) {
self.record("active");
}
fn stream_did_become_inactive(&self) {
self.record("inactive");
}
fn output_video_effect_did_start_for_stream(&self) {
self.record("effect_start");
}
fn output_video_effect_did_stop_for_stream(&self) {
self.record("effect_stop");
}
}
let recorder = Arc::new(Recorder::default());
let ctx = StreamContext::new(Some(Arc::new(Arc::clone(&recorder))));
for event in 0..4 {
delegate_event_callback(ctx.cast::<c_void>(), event);
}
delegate_event_callback(ctx.cast::<c_void>(), 99);
delegate_event_callback(std::ptr::null_mut(), 0);
assert_eq!(
*recorder
.events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec!["active", "inactive", "effect_start", "effect_stop"]
);
unsafe { StreamContext::release(ctx) };
}
#[test]
fn test_delegate_event_callback_without_a_delegate_is_a_noop() {
let ctx = StreamContext::new(None);
for event in 0..4 {
delegate_event_callback(ctx.cast::<c_void>(), event);
}
unsafe { StreamContext::release(ctx) };
}
#[test]
fn test_claim_start_skips_the_native_call_while_capturing() {
let capturing = AtomicBool::new(false);
let unconfirmed = AtomicBool::new(false);
assert!(claim_start(&capturing, &unconfirmed));
assert!(!claim_start(&capturing, &unconfirmed));
}
#[test]
fn test_claim_start_reissues_after_an_unconfirmed_start() {
let capturing = AtomicBool::new(true);
let unconfirmed = AtomicBool::new(true);
assert!(claim_start(&capturing, &unconfirmed));
assert!(!claim_start(&capturing, &unconfirmed));
}
#[test]
fn test_claim_start_clears_the_flag_even_when_idle() {
let capturing = AtomicBool::new(false);
let unconfirmed = AtomicBool::new(true);
assert!(claim_start(&capturing, &unconfirmed));
assert!(!unconfirmed.load(Ordering::Acquire));
assert!(!claim_start(&capturing, &unconfirmed));
}
}