screencapturekit 11.0.0

Safe Rust bindings for Apple's ScreenCaptureKit framework - screen and audio capture on macOS
Documentation
//! Core Media types tests
//!
//! Tests for `CMTime`, `CMSampleTimingInfo`, and related types

use screencapturekit::cm::{CMSampleTimingInfo, CMTime};

extern "C" {
    fn cm_test_copy_nsvalue_dirty_rects(
        values: *const f64,
        count: usize,
        out_rects: *mut *mut std::ffi::c_void,
        out_count: *mut usize,
    ) -> bool;
    fn cm_sample_buffer_free_dirty_rects(rects: *mut std::ffi::c_void);
}

#[test]
fn test_cmtime_creation() {
    let time = CMTime::new(1, 30);
    assert!(time.is_valid());

    let zero = CMTime::ZERO;
    assert!(zero.is_zero());
    assert!(zero.is_valid());

    let invalid = CMTime::INVALID;
    assert!(!invalid.is_valid());
}

#[test]
fn test_cmtime_special_values() {
    let pos_inf = CMTime::positive_infinity();
    assert!(pos_inf.is_positive_infinity());
    assert!(!pos_inf.is_negative_infinity());
    assert!(!pos_inf.is_indefinite());

    let neg_inf = CMTime::negative_infinity();
    assert!(neg_inf.is_negative_infinity());
    assert!(!neg_inf.is_positive_infinity());

    let indefinite = CMTime::indefinite();
    assert!(indefinite.is_indefinite());
    assert!(!indefinite.is_positive_infinity());
}

#[test]
fn test_cmtime_const_functions() {
    const FRAME_DURATION: CMTime = CMTime::new(1, 30);
    const ZERO: CMTime = CMTime::ZERO;
    const INVALID: CMTime = CMTime::INVALID;

    assert!(FRAME_DURATION.is_valid());
    assert!(ZERO.is_zero());
    assert!(!INVALID.is_valid());
}

#[test]
fn test_cmtime_const_equality() {
    const TIME1: CMTime = CMTime::new(1, 30);
    const TIME2: CMTime = CMTime::new(1, 30);
    const TIME3: CMTime = CMTime::new(2, 30);

    assert!(TIME1.equals(&TIME2));
    assert!(!TIME1.equals(&TIME3));
}

#[test]
fn test_cmtime_display() {
    let time = CMTime::new(1, 30);
    let display = format!("{time:?}");
    // Just verify it doesn't crash
    assert!(!display.is_empty());

    let zero = CMTime::ZERO;
    let zero_display = format!("{zero:?}");
    assert!(!zero_display.is_empty());
}

#[test]
fn test_cmsample_timing_info_creation() {
    let timing =
        CMSampleTimingInfo::with_times(CMTime::new(1, 30), CMTime::new(0, 30), CMTime::new(1, 30));

    // Verify valid times
    assert!(timing.has_valid_presentation_time());
    assert!(timing.has_valid_decode_time());
}

#[test]
fn test_cmsample_timing_info_invalid() {
    let timing = CMSampleTimingInfo::with_times(CMTime::INVALID, CMTime::INVALID, CMTime::INVALID);

    assert!(!timing.has_valid_presentation_time());
    assert!(!timing.has_valid_decode_time());
}

#[test]
fn test_cmsample_timing_info_display() {
    let timing =
        CMSampleTimingInfo::with_times(CMTime::new(1, 30), CMTime::new(0, 30), CMTime::new(1, 30));

    let display = format!("{timing:?}");
    // Just verify it doesn't crash
    assert!(!display.is_empty());
}

#[test]
fn test_cmsample_timing_default() {
    let timing = CMSampleTimingInfo::with_times(CMTime::ZERO, CMTime::ZERO, CMTime::ZERO);

    assert!(timing.is_valid());
}

#[test]
fn test_sample_buffer_cg_image_round_trips() {
    use screencapturekit::cm::{CMSampleBuffer, CMSampleBufferExt};
    use screencapturekit::cv::CVPixelBuffer;

    // Build a tiny BGRA CVPixelBuffer, wrap it in a CMSampleBuffer, then ask
    // for a CGImage and confirm dimensions round-trip. VTCreateCGImageFromCVPixelBuffer
    // is what actually runs under cg_image().
    let pb = CVPixelBuffer::create(64, 48, 0x4247_5241).expect("create BGRA pixel buffer"); // 'BGRA'
    let sb = CMSampleBuffer::create_for_image_buffer(&pb, CMTime::ZERO, CMTime::ZERO)
        .expect("wrap in sample buffer");
    let cg = sb.cg_image().expect("cg_image from sample buffer");
    assert_eq!(cg.width(), 64);
    assert_eq!(cg.height(), 48);
}

#[test]
fn test_image_sample_preserves_all_timing_fields() {
    use screencapturekit::cm::{CMSampleBuffer, CMSampleBufferExt};
    use screencapturekit::cv::CVPixelBuffer;

    let pixel_buffer =
        CVPixelBuffer::create(16, 16, 0x4247_5241).expect("create BGRA pixel buffer");
    let presentation_time = CMTime {
        value: 120,
        timescale: 600,
        flags: 3,
        epoch: 7,
    };
    let duration = CMTime {
        value: 10,
        timescale: 600,
        flags: 3,
        epoch: 11,
    };
    let sample =
        CMSampleBuffer::create_for_image_buffer(&pixel_buffer, presentation_time, duration)
            .expect("create image sample");
    let timing = sample.sample_timing_info(0).expect("sample timing info");

    assert_eq!(timing.presentation_time_stamp, presentation_time);
    assert_eq!(timing.duration, duration);
}

#[test]
fn test_sample_buffer_pixel_buffer_is_retained_and_specific() {
    use screencapturekit::cm::{CMSampleBuffer, CMSampleBufferExt};
    use screencapturekit::cv::CVPixelBuffer;

    let pixel_buffer = {
        let source = CVPixelBuffer::create(32, 24, 0x4247_5241).expect("create BGRA pixel buffer");
        let sample = CMSampleBuffer::create_for_image_buffer(
            &source,
            CMTime::new(1, 60),
            CMTime::new(1, 60),
        )
        .expect("create image sample");
        sample.pixel_buffer().expect("specific pixel buffer")
    };

    assert_eq!(pixel_buffer.width(), 32);
    assert_eq!(pixel_buffer.height(), 24);
}

#[test]
fn test_dirty_rect_parser_decodes_nsvalue_rectangles() {
    let expected = [1.5, 2.5, 30.0, 40.0, 10.0, 20.0, 3.0, 4.0];
    let mut rects = std::ptr::null_mut();
    let mut count = 0;

    let decoded = unsafe {
        cm_test_copy_nsvalue_dirty_rects(
            expected.as_ptr(),
            expected.len() / 4,
            &raw mut rects,
            &raw mut count,
        )
    };
    assert!(decoded);
    assert_eq!(count, 2);
    assert!(!rects.is_null());

    let actual = unsafe { std::slice::from_raw_parts(rects.cast::<f64>(), expected.len()) };
    for (actual, expected) in actual.iter().zip(expected) {
        assert!((*actual - expected).abs() < f64::EPSILON);
    }
    unsafe { cm_sample_buffer_free_dirty_rects(rects) };
}

#[test]
fn test_sample_buffer_cg_image_returns_err_on_no_image_buffer() {
    use screencapturekit::cm::{CMSampleBuffer, CMSampleBufferExt};
    use screencapturekit::cv::CVPixelBuffer;

    // An audio-only / metadata-only sample buffer has no CVImageBuffer; cg_image
    // should surface that as Err rather than producing a garbage CGImage.
    // Easiest construction here: build a 1x1 buffer, drop the pixel buffer
    // immediately, and confirm Err on a separately-constructed empty
    // sample buffer. (No pure constructor for empty CMSampleBuffer in
    // safe API, so this test focuses on the round-trip success path; the
    // failure path is exercised at the SCK layer when audio-only frames arrive.)
    let pb = CVPixelBuffer::create(8, 8, 0x4247_5241).expect("create BGRA pixel buffer");
    let sb = CMSampleBuffer::create_for_image_buffer(&pb, CMTime::ZERO, CMTime::ZERO)
        .expect("wrap in sample buffer");
    // Even a 1x1 buffer should round-trip OK; we mostly need to verify the
    // call path doesn't crash on tiny dims.
    let cg = sb
        .cg_image()
        .expect("cg_image succeeds on tiny BGRA buffer");
    assert_eq!(cg.width(), 8);
    assert_eq!(cg.height(), 8);
}