use screencapturekit::cm::CMSampleBufferExt;
use screencapturekit::{
cv::CVPixelBufferLockFlags,
shareable_content::SCShareableContent,
stream::{
configuration::SCStreamConfiguration, content_filter::SCContentFilter,
output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType, SCStream,
},
CMSampleBuffer,
};
use std::sync::{Arc, Mutex};
use std::time::Duration;
struct VideoTestOutput {
samples: Arc<Mutex<Vec<CMSampleBuffer>>>,
}
impl SCStreamOutputTrait for VideoTestOutput {
fn did_output_sample_buffer(
&self,
sample_buffer: CMSampleBuffer,
_of_type: SCStreamOutputType,
) {
if let Ok(mut guard) = self.samples.lock() {
guard.push(sample_buffer);
}
}
}
struct AudioTestOutput {
samples: Arc<Mutex<Vec<CMSampleBuffer>>>,
}
impl SCStreamOutputTrait for AudioTestOutput {
fn did_output_sample_buffer(
&self,
sample_buffer: CMSampleBuffer,
_of_type: SCStreamOutputType,
) {
if let Ok(mut guard) = self.samples.lock() {
guard.push(sample_buffer);
}
}
}
#[test]
fn test_video_capture() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!("SKIP: Screen recording permission required!");
println!(" Go to: System Settings → Privacy & Security → Screen Recording");
println!(" Error: {e:?}");
return; }
};
let displays = content.displays();
if displays.is_empty() {
eprintln!("SKIP: No displays available - skipping test");
return;
}
let display = &displays[0];
let mut config = SCStreamConfiguration::default();
config.set_width(1920);
config.set_height(1080);
config.set_captures_audio(false);
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let mut stream = SCStream::new(&filter, &config);
let samples = Arc::new(Mutex::new(Vec::new()));
let output = VideoTestOutput {
samples: samples.clone(),
};
stream.add_output_handler(output, SCStreamOutputType::Screen);
stream.start_capture().expect("Failed to start capture");
std::thread::sleep(Duration::from_secs(2));
stream.stop_capture().expect("Failed to stop capture");
let collected_samples = samples.lock().unwrap();
if collected_samples.is_empty() {
eprintln!(
"SKIP: No video samples captured - this may be due to permissions or environment"
);
return; }
println!("Captured {} video samples", collected_samples.len());
if let Some(sample) = collected_samples.first() {
if let Some(image_buffer) = sample.image_buffer() {
let width = image_buffer.width();
let height = image_buffer.height();
println!("Video frame size: {width}x{height}");
assert!(width > 0, "Invalid video width");
assert!(height > 0, "Invalid video height");
} else {
eprintln!("SKIP: First sample has no image buffer (may be idle frame)");
}
}
}
#[test]
fn test_audio_capture() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!("SKIP: Screen recording permission required!");
println!(" Error: {e:?}");
return;
}
};
let displays = content.displays();
if displays.is_empty() {
eprintln!("SKIP: No displays available - skipping test");
return;
}
let display = &displays[0];
let mut config = SCStreamConfiguration::default();
config.set_captures_audio(true);
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let mut stream = SCStream::new(&filter, &config);
let samples = Arc::new(Mutex::new(Vec::new()));
let output = AudioTestOutput {
samples: samples.clone(),
};
stream.add_output_handler(output, SCStreamOutputType::Audio);
stream.start_capture().expect("Failed to start capture");
std::thread::sleep(Duration::from_secs(3));
stream.stop_capture().expect("Failed to stop capture");
let collected_samples = samples.lock().unwrap();
if collected_samples.is_empty() {
eprintln!("SKIP: No audio samples captured (OK if no audio was playing)");
return;
}
println!("Captured {} audio samples", collected_samples.len());
let mut samples_with_data = 0;
for sample in collected_samples.iter() {
if let Some(audio_buffer_list) = sample.audio_buffer_list() {
let num_buffers = audio_buffer_list.num_buffers();
if num_buffers > 0 {
samples_with_data += 1;
if let Some(buffer) = audio_buffer_list.buffer(0) {
let data_size = buffer.data_byte_size();
println!("Audio buffer: {num_buffers} buffers, {data_size} bytes");
}
}
}
}
let total_samples = collected_samples.len();
drop(collected_samples);
println!("Audio samples with buffer data: {samples_with_data}/{total_samples}");
}
#[test]
fn test_video_and_audio_capture() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!("SKIP: Screen recording permission required!");
println!(" Error: {e:?}");
return;
}
};
let displays = content.displays();
if displays.is_empty() {
eprintln!("SKIP: No displays available - skipping test");
return;
}
let display = &displays[0];
let mut config = SCStreamConfiguration::default();
config.set_width(1280);
config.set_height(720);
config.set_captures_audio(true);
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let mut stream = SCStream::new(&filter, &config);
let video_samples = Arc::new(Mutex::new(Vec::new()));
let video_output = VideoTestOutput {
samples: video_samples.clone(),
};
stream.add_output_handler(video_output, SCStreamOutputType::Screen);
let audio_samples = Arc::new(Mutex::new(Vec::new()));
let audio_output = AudioTestOutput {
samples: audio_samples.clone(),
};
stream.add_output_handler(audio_output, SCStreamOutputType::Audio);
stream.start_capture().expect("Failed to start capture");
std::thread::sleep(Duration::from_secs(3));
stream.stop_capture().expect("Failed to stop capture");
let video_count = video_samples.lock().unwrap().len();
let audio_count = audio_samples.lock().unwrap().len();
println!("Captured {video_count} video samples and {audio_count} audio samples");
if video_count == 0 {
eprintln!("SKIP: No video samples captured - may be due to permissions");
return;
}
if audio_count == 0 {
eprintln!("SKIP: No audio samples captured (OK if no audio was playing)");
}
}
#[test]
fn test_pixel_buffer_locking() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!("SKIP: Screen recording permission required!");
println!(" Error: {e:?}");
return;
}
};
let displays = content.displays();
if displays.is_empty() {
eprintln!("SKIP: No displays available - skipping test");
return;
}
let display = &displays[0];
let mut config = SCStreamConfiguration::default();
config.set_width(640);
config.set_height(480);
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let mut stream = SCStream::new(&filter, &config);
let samples = Arc::new(Mutex::new(Vec::new()));
let output = VideoTestOutput {
samples: samples.clone(),
};
stream.add_output_handler(output, SCStreamOutputType::Screen);
stream.start_capture().expect("Failed to start capture");
std::thread::sleep(Duration::from_millis(500));
stream.stop_capture().expect("Failed to stop capture");
let collected_samples = samples.lock().unwrap();
if let Some(sample) = collected_samples.first() {
let Some(pixel_buffer) = sample.image_buffer() else {
eprintln!("SKIP: First sample has no image buffer (may be idle frame)");
return;
};
{
let lock_guard = pixel_buffer
.lock(CVPixelBufferLockFlags::READ_ONLY)
.expect("Failed to lock base address for reading");
let base_address = lock_guard.base_address();
assert!(!base_address.is_null(), "Base address is null");
let width = pixel_buffer.width();
let height = pixel_buffer.height();
let bytes_per_row = pixel_buffer.bytes_per_row();
println!("Locked pixel buffer: {width}x{height}, {bytes_per_row} bytes/row");
}
{
let mut lock_guard = pixel_buffer
.lock(CVPixelBufferLockFlags::NONE)
.expect("Failed to lock base address for writing");
let base_address_mut = lock_guard.base_address_mut();
assert!(
base_address_mut.is_some(),
"Mutable base address should be available for read-write lock"
);
assert!(
!base_address_mut.unwrap().is_null(),
"Mutable base address is null"
);
}
println!("Pixel buffer locking test passed");
}
}
#[test]
fn test_iosurface_backed_buffer() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!("SKIP: Screen recording permission required!");
println!(" Error: {e:?}");
return;
}
};
let displays = content.displays();
let Some(display) = displays.first() else {
eprintln!("SKIP: No displays available");
return;
};
let mut config = SCStreamConfiguration::default();
config.set_width(1920);
config.set_height(1080);
let filter = SCContentFilter::create()
.with_display(display)
.with_excluding_windows(&[])
.build();
let mut stream = SCStream::new(&filter, &config);
let samples = Arc::new(Mutex::new(Vec::new()));
let output = VideoTestOutput {
samples: samples.clone(),
};
stream.add_output_handler(output, SCStreamOutputType::Screen);
stream.start_capture().expect("Failed to start capture");
std::thread::sleep(Duration::from_millis(500));
stream.stop_capture().expect("Failed to stop capture");
let collected_samples = samples.lock().unwrap();
if let Some(sample) = collected_samples.first() {
let Some(pixel_buffer) = sample.image_buffer() else {
eprintln!("SKIP: First sample has no image buffer (may be idle frame)");
return;
};
let iosurface = pixel_buffer.io_surface();
assert!(iosurface.is_some(), "Pixel buffer is not IOSurface-backed");
if let Some(surface) = iosurface {
let width = surface.width();
let height = surface.height();
let bytes_per_row = surface.bytes_per_row();
println!("IOSurface: {width}x{height}, {bytes_per_row} bytes/row");
assert!(width > 0, "Invalid IOSurface width");
assert!(height > 0, "Invalid IOSurface height");
assert!(bytes_per_row > 0, "Invalid IOSurface bytes per row");
}
}
}
#[test]
fn test_shareable_content_below_window() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!("Skipping test: {e}");
return;
}
};
let windows = content.windows();
if windows.is_empty() {
eprintln!("Skipping test: no windows available");
return;
}
let reference_window = &windows[0];
let result = SCShareableContent::create()
.with_exclude_desktop_windows(false)
.below_window(reference_window);
match result {
Ok(below_content) => {
println!(
"Found {} windows below reference window",
below_content.windows().len()
);
}
Err(e) => {
eprintln!("Below window query failed (may be expected): {e}");
}
}
}
#[test]
fn test_shareable_content_above_window() {
let content = match SCShareableContent::get() {
Ok(c) => c,
Err(e) => {
eprintln!("Skipping test: {e}");
return;
}
};
let windows = content.windows();
if windows.is_empty() {
eprintln!("Skipping test: no windows available");
return;
}
let reference_window = &windows[0];
let result = SCShareableContent::create()
.with_exclude_desktop_windows(false)
.above_window(reference_window);
match result {
Ok(above_content) => {
println!(
"Found {} windows above reference window",
above_content.windows().len()
);
}
Err(e) => {
eprintln!("Above window query failed (may be expected): {e}");
}
}
}
#[cfg(feature = "macos_14_4")]
#[test]
fn test_shareable_content_current_process() {
let result = SCShareableContent::current_process();
match result {
Ok(content) => {
println!(
"Current process content: {} displays, {} windows, {} apps",
content.displays().len(),
content.windows().len(),
content.applications().len()
);
}
Err(e) => {
eprintln!("Current process content query failed: {e}");
}
}
}