use screencapturekit::cm::CMSampleBufferExt;
#[cfg(feature = "macos_14_2")]
use screencapturekit::cm::CMSampleBufferSCExt;
use screencapturekit::{
cm::CMSampleBuffer,
shareable_content::SCShareableContent,
stream::{
configuration::{PixelFormat, SCStreamConfiguration},
content_filter::SCContentFilter,
output_trait::SCStreamOutputTrait,
output_type::SCStreamOutputType,
SCStream,
},
};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
struct FirstSampleCollector {
captured: Mutex<Option<CMSampleBuffer>>,
}
impl SCStreamOutputTrait for FirstSampleCollector {
fn did_output_sample_buffer(&self, sample_buffer: CMSampleBuffer, of_type: SCStreamOutputType) {
if !matches!(of_type, SCStreamOutputType::Screen) {
return;
}
if let Ok(mut slot) = self.captured.lock() {
if slot.is_none() {
*slot = Some(sample_buffer);
}
}
}
}
struct DelegatingHandler {
inner: Arc<FirstSampleCollector>,
}
impl SCStreamOutputTrait for DelegatingHandler {
fn did_output_sample_buffer(&self, sample_buffer: CMSampleBuffer, of_type: SCStreamOutputType) {
self.inner.did_output_sample_buffer(sample_buffer, of_type);
}
}
#[test]
fn test_yuv_420v_pixel_format_capture() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!(
"SKIP: Screen recording permission required to validate YUV capture (error: {e:?})"
);
return;
}
};
let displays = content.displays();
let Some(display) = displays.first() else {
eprintln!("SKIP: No displays available");
return;
};
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(640)
.with_height(480)
.with_pixel_format(PixelFormat::YCbCr_420v);
let collector = Arc::new(FirstSampleCollector {
captured: Mutex::new(None),
});
let mut stream = SCStream::new(&filter, &config);
stream
.add_output_handler(
DelegatingHandler {
inner: collector.clone(),
},
SCStreamOutputType::Screen,
)
.expect("add_output_handler failed");
if let Err(e) = stream.start_capture() {
eprintln!("SKIP: stream failed to start (typically permission-related): {e:?}");
return;
}
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while collector.captured.lock().is_ok_and(|s| s.is_none())
&& std::time::Instant::now() < deadline
{
std::thread::sleep(Duration::from_millis(20));
}
let _ = stream.stop_capture();
let sample = collector
.captured
.lock()
.ok()
.and_then(|mut s| s.take())
.expect("no sample captured within 2s");
let Some(image_buffer) = sample.image_buffer() else {
eprintln!("SKIP: first sample had no image buffer (likely idle frame); test inconclusive");
return;
};
let plane_count = image_buffer.plane_count();
assert!(
plane_count >= 2,
"expected biplanar YUV (>=2 planes) for YCbCr_420v configuration, \
got plane_count={plane_count} — stream likely fell back to BGRA"
);
let _guard = image_buffer
.lock_read_only()
.expect("lock_read_only failed for YUV buffer");
}
#[test]
fn test_handler_add_remove_mid_capture() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!(
"SKIP: Screen recording permission required for mid-capture handler lifecycle test (error: {e:?})"
);
return;
}
};
let displays = content.displays();
let Some(display) = displays.first() else {
eprintln!("SKIP: No displays available");
return;
};
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(320)
.with_height(240)
.with_pixel_format(PixelFormat::BGRA);
let count = Arc::new(AtomicUsize::new(0));
let active = Arc::new(AtomicBool::new(true));
let count_handler = count.clone();
let active_handler = active.clone();
let handler = move |_sample: CMSampleBuffer, _of_type: SCStreamOutputType| {
if active_handler.load(Ordering::Relaxed) {
count_handler.fetch_add(1, Ordering::Relaxed);
}
};
let mut stream = SCStream::new(&filter, &config);
let id = stream
.add_output_handler(handler, SCStreamOutputType::Screen)
.expect("add_output_handler failed");
if let Err(e) = stream.start_capture() {
eprintln!("SKIP: stream failed to start: {e:?}");
return;
}
std::thread::sleep(Duration::from_millis(500));
let count_before_remove = count.load(Ordering::Relaxed);
assert!(
count_before_remove > 0,
"expected at least one frame before mid-capture removal, got {count_before_remove}"
);
let removed = stream.remove_output_handler(id, SCStreamOutputType::Screen);
active.store(false, Ordering::Relaxed);
assert!(removed, "remove_output_handler returned false");
let count_after_remove = count.load(Ordering::Relaxed);
std::thread::sleep(Duration::from_millis(500));
let count_settled = count.load(Ordering::Relaxed);
let _ = stream.stop_capture();
let drift = count_settled.saturating_sub(count_after_remove);
assert!(
drift <= 2,
"after remove_output_handler, the handler kept firing: \
count_after_remove={count_after_remove}, count_settled={count_settled}, drift={drift}"
);
}
#[cfg(feature = "macos_14_2")]
#[test]
fn test_presenter_overlay_content_rect_absent_when_overlay_disabled() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!(
"SKIP: Screen recording permission required for Presenter Overlay attachment test (error: {e:?})"
);
return;
}
};
let displays = content.displays();
let Some(display) = displays.first() else {
eprintln!("SKIP: No displays available");
return;
};
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let config = SCStreamConfiguration::new()
.with_width(320)
.with_height(240)
.with_pixel_format(PixelFormat::BGRA);
let collector = Arc::new(FirstSampleCollector {
captured: Mutex::new(None),
});
let mut stream = SCStream::new(&filter, &config);
stream
.add_output_handler(
DelegatingHandler {
inner: collector.clone(),
},
SCStreamOutputType::Screen,
)
.expect("add_output_handler failed");
if let Err(e) = stream.start_capture() {
eprintln!("SKIP: stream failed to start: {e:?}");
return;
}
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while collector.captured.lock().is_ok_and(|s| s.is_none())
&& std::time::Instant::now() < deadline
{
std::thread::sleep(Duration::from_millis(20));
}
let _ = stream.stop_capture();
let sample = collector
.captured
.lock()
.ok()
.and_then(|mut s| s.take())
.expect("no sample captured within 2s");
let overlay_rect = sample.presenter_overlay_content_rect();
if let Some(rect) = overlay_rect {
assert!(
!rect.origin.x.is_finite() || rect.size.width == 0.0 || rect.size.height == 0.0,
"presenter_overlay_content_rect() returned a finite rect ({rect:?}) on a \
stream that did not enable Presenter Overlay; expected None or CGRectNull"
);
}
}