crabcamera 0.8.3

Advanced cross-platform camera integration for Tauri applications
Documentation
//! Audio-Video Integration Tests for CrabCamera Recording Module
//!
//! Tests that validate:
//! - Recordings contain valid audio and video tracks
//! - Audio/video synchronization within ±40ms bounds
//! - Proper error handling and resource cleanup
//!
//! Run with: cargo test --test av_integration --features "recording,audio"

#![cfg(all(feature = "recording", feature = "audio"))]

use std::time::Duration;
use tempfile::tempdir;

use crabcamera::audio::{list_audio_devices, AudioFrame, OpusEncoder, PTSClock};
use crabcamera::recording::{AudioConfig, Recorder, RecordingConfig};
use crabcamera::types::CameraFrame;

// ═══════════════════════════════════════════════════════════════════════════
// @UnitTests
// ═══════════════════════════════════════════════════════════════════════════

/// Per #RecordingTests_AV: ! device_enumeration_safe
#[test]
fn test_device_enumeration_safe() {
    // Should not panic even if no audio devices
    let result = list_audio_devices();
    // Either returns devices or an error, never panics
    match result {
        Ok(devices) => {
            // Verify each device has required fields
            for device in &devices {
                assert!(!device.id.is_empty(), "Device ID should not be empty");
                assert!(!device.name.is_empty(), "Device name should not be empty");
                assert!(device.sample_rate > 0, "Sample rate should be positive");
                assert!(device.channels > 0, "Channels should be positive");
            }
        }
        Err(e) => {
            // Error is expected on systems without audio
            println!("Audio enumeration error (expected on some systems): {}", e);
        }
    }
}

/// Per #RecordingTests_AV: ! capture_start_stop_safe (extended)
#[test]
fn test_capture_lifecycle_safe() {
    use crabcamera::audio::AudioCapture;

    let clock = PTSClock::new();

    // Try to create capture - may fail if no device
    match AudioCapture::new(None, 48000, 2, clock) {
        Ok(mut capture) => {
            // Multiple starts are safe
            assert!(capture.start().is_ok());
            assert!(capture.start().is_ok());

            // Brief capture window
            std::thread::sleep(Duration::from_millis(50));

            // Multiple stops are safe
            assert!(capture.stop().is_ok());
            assert!(capture.stop().is_ok());
        }
        Err(e) => {
            // Expected on systems without audio
            println!("Audio capture unavailable: {}", e);
        }
    }
}

/// Per #RecordingTests_AV: ! encoded_audio_headers_valid
#[test]
fn test_encoded_audio_headers_valid() {
    let mut encoder = OpusEncoder::new(48000, 2, 128_000).expect("Opus encoder should create");

    // Create a full frame worth of audio (960 samples @ 48kHz = 20ms)
    let frame = AudioFrame {
        samples: vec![0.0f32; 960 * 2], // 960 stereo samples
        sample_rate: 48000,
        channels: 2,
        timestamp: 0.0,
    };

    let packets = encoder.encode(&frame).expect("Encode should succeed");
    assert_eq!(packets.len(), 1, "Should produce exactly one packet");

    let packet = &packets[0];
    // Opus packets start with TOC byte
    assert!(!packet.data.is_empty(), "Encoded data should not be empty");

    // Verify TOC byte is valid Opus format
    // TOC byte structure: config (5 bits) | s (1 bit) | c (2 bits)
    let toc = packet.data[0];
    let config = (toc >> 3) & 0x1F;
    // Config 0-31 are valid for Opus
    assert!(config < 32, "TOC config should be valid: {}", config);

    // Verify timestamp is set
    assert!(packet.timestamp >= 0.0, "Timestamp should be non-negative");

    // Verify duration is approximately 20ms for 960 samples @ 48kHz
    assert!(
        (packet.duration - 0.020).abs() < 0.001,
        "Duration should be ~20ms, got {}",
        packet.duration
    );
}

// ═══════════════════════════════════════════════════════════════════════════
// @IntegrationTest
// ═══════════════════════════════════════════════════════════════════════════

/// Per #RecordingTests_AV: ! contains_video_track
#[test]
fn test_video_only_recording_produces_valid_file() {
    let dir = tempdir().expect("Create temp dir");
    let output = dir.path().join("video_only.mp4");

    let config = RecordingConfig::new(320, 240, 30.0);
    let mut recorder = Recorder::new(&output, config).expect("Recorder should create");

    // Write some frames
    for i in 0..30 {
        let gray = ((i * 8) % 256) as u8;
        let frame = create_test_frame(320, 240, gray);
        recorder.write_frame(&frame).expect("Write frame");
    }

    let stats = recorder.finish().expect("Finish recording");

    // Verify video was written
    assert!(stats.video_frames > 0, "Should have video frames");
    assert!(stats.bytes_written > 0, "Should have bytes written");

    // Verify file exists and has content
    let metadata = std::fs::metadata(&output).expect("File should exist");
    assert!(metadata.len() > 0, "File should have content");

    // Verify MP4 header (starts with ftyp box)
    let file_start = std::fs::read(&output).expect("Read file");
    // MP4 files start with size (4 bytes) + "ftyp" signature
    assert!(file_start.len() >= 8, "File should have MP4 header");
    assert_eq!(&file_start[4..8], b"ftyp", "Should have ftyp box");
}

/// Per #RecordingTests_AV: ! contains_audio_track_when_enabled
#[test]
fn test_av_recording_config_with_audio() {
    let dir = tempdir().expect("Create temp dir");
    let output = dir.path().join("av_recording.mp4");

    // Create config with audio enabled
    let config = RecordingConfig::new(320, 240, 30.0).with_audio(AudioConfig {
        device_id: None, // Default device
        sample_rate: 48000,
        channels: 2,
        bitrate: 128_000,
    });

    // Try to create recorder - this tests audio track configuration
    match Recorder::new(&output, config) {
        Ok(recorder) => {
            // Audio is enabled in config
            assert!(recorder.audio_enabled(), "Audio should be enabled");

            // Recorder was created successfully with audio track configured
            // The actual audio capture may fail on systems without audio devices,
            // but the muxer configuration is valid
            drop(recorder);
        }
        Err(e) => {
            println!("Recorder creation failed (may be expected): {}", e);
        }
    }
}

/// Per #RecordingTests_AV: ! sync_within_policy
/// This test verifies the PTS clock produces consistent timestamps
#[test]
#[ignore = "Timing-sensitive test - CI environments have variable latency"]
fn test_pts_clock_sync_within_policy() {
    let clock = PTSClock::new();

    // Simulate 1 second of recording at 30 fps with audio packets every 20ms
    let mut video_pts = Vec::new();
    let mut audio_pts = Vec::new();

    let frame_duration = Duration::from_secs_f64(1.0 / 30.0);
    let _audio_duration = Duration::from_millis(20);

    // Collect PTS values over simulated time
    let start = std::time::Instant::now();
    for i in 0..30 {
        // Video PTS
        video_pts.push(clock.pts());

        // Audio comes at different rate (every ~20ms)
        if i % 2 == 0 || i % 3 == 0 {
            audio_pts.push(clock.pts());
        }

        std::thread::sleep(frame_duration);
    }
    let elapsed = start.elapsed();

    // Verify timing is approximately correct
    let expected_duration = 1.0; // ~1 second
    let actual_duration = elapsed.as_secs_f64();
    assert!(
        (actual_duration - expected_duration).abs() < 0.2,
        "Test should run for ~1s, got {:.2}s",
        actual_duration
    );

    // Per #AVSyncPolicy: ! max_drift <= 100ms
    // Check that PTS values are monotonically increasing and bounded
    for window in video_pts.windows(2) {
        let delta = window[1] - window[0];
        // Each frame should be ~33ms apart (at 30fps)
        // Allow for timing jitter but enforce max drift
        assert!(delta >= 0.0, "PTS should be monotonically increasing");
        assert!(
            delta < 0.100,
            "Frame delta should be < 100ms, got {:.3}s",
            delta
        );
    }

    // Verify audio PTS is also reasonable
    for window in audio_pts.windows(2) {
        let delta = window[1] - window[0];
        assert!(delta >= 0.0, "Audio PTS should be monotonically increasing");
        assert!(
            delta < 0.100,
            "Audio delta should be < 100ms, got {:.3}s",
            delta
        );
    }
}

/// Integration test: Full A/V recording pipeline (requires audio device)
/// This test is skipped in CI where no audio device is available
#[test]
#[ignore = "Requires audio device - run manually with --ignored"]
fn test_full_av_recording_produces_valid_file() {
    let dir = tempdir().expect("Create temp dir");
    let output = dir.path().join("full_av.mp4");

    // Create config with audio
    let config = RecordingConfig::new(320, 240, 30.0).with_audio(AudioConfig {
        device_id: None,
        sample_rate: 48000,
        channels: 2,
        bitrate: 128_000,
    });

    let mut recorder = Recorder::new(&output, config).expect("Recorder should create");

    // Write frames for 1 second
    for i in 0..30 {
        let gray = ((i * 8) % 256) as u8;
        let frame = create_test_frame(320, 240, gray);
        recorder.write_frame(&frame).expect("Write frame");
        std::thread::sleep(Duration::from_millis(33));
    }

    let stats = recorder.finish().expect("Finish recording");

    // Verify both tracks present
    assert!(stats.video_frames > 0, "Should have video frames");
    assert!(stats.bytes_written > 0, "Should have bytes written");

    // File should be larger with audio
    let metadata = std::fs::metadata(&output).expect("File should exist");
    assert!(metadata.len() > 10_000, "A/V file should be substantial");
}

// ═══════════════════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════════════════

/// Create a test camera frame with uniform color
fn create_test_frame(width: u32, height: u32, gray: u8) -> CameraFrame {
    CameraFrame::new(
        vec![gray; (width * height * 3) as usize],
        width,
        height,
        "test_device".to_string(),
    )
}