#![cfg(feature = "audio")]
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use crabcamera::audio::{
get_default_audio_device, list_audio_devices, AudioCapture, OpusEncoder, PTSClock,
};
#[test]
fn test_device_enumeration_comprehensive() {
match list_audio_devices() {
Ok(devices) => {
println!("Found {} audio devices", devices.len());
for (i, device) in devices.iter().enumerate() {
println!("Device {}: {} ({})", i, device.name, device.id);
assert!(!device.id.is_empty(), "Device ID cannot be empty");
assert!(!device.name.is_empty(), "Device name cannot be empty");
assert!(device.sample_rate > 0, "Sample rate must be positive");
assert!(
device.channels > 0 && device.channels <= 8,
"Channels must be 1-8"
);
assert!(
device.sample_rate >= 8000 && device.sample_rate <= 192000,
"Sample rate {} is unreasonable",
device.sample_rate
);
}
let default_devices: Vec<_> = devices.iter().filter(|d| d.is_default).collect();
assert!(
default_devices.len() <= 1,
"Cannot have more than one default device"
);
if !devices.is_empty() {
if default_devices.is_empty() {
match get_default_audio_device() {
Ok(default) => {
println!("Default device: {}", default.name);
assert!(
default.is_default,
"Default device should be marked as default"
);
}
Err(e) => {
println!("Warning: No default device available: {}", e);
}
}
}
}
}
Err(e) => {
println!(
"Audio enumeration failed (may be expected on headless systems): {}",
e
);
let error_str = e.to_string();
assert!(!error_str.is_empty(), "Error message should not be empty");
assert!(error_str.len() > 10, "Error message should be descriptive");
}
}
}
#[test]
fn test_default_device_handling() {
match get_default_audio_device() {
Ok(device) => {
assert!(
device.is_default,
"Default device should be marked as default"
);
assert!(
!device.id.is_empty(),
"Default device ID should not be empty"
);
assert!(
!device.name.is_empty(),
"Default device name should not be empty"
);
println!(
"Default audio device: {} (SR: {}Hz, CH: {})",
device.name, device.sample_rate, device.channels
);
}
Err(e) => {
println!("No default audio device (expected on some systems): {}", e);
}
}
}
#[test]
fn test_capture_lifecycle_comprehensive() {
let clock = PTSClock::new();
let test_configs = vec![
(None, 48000, 1), (None, 48000, 2), (Some("default".to_string()), 48000, 2), ];
for (device_id, sample_rate, channels) in test_configs {
println!(
"Testing config: device={:?}, sr={}, ch={}",
device_id, sample_rate, channels
);
match AudioCapture::new(device_id.clone(), sample_rate, channels, clock.clone()) {
Ok(mut capture) => {
assert!(
!capture.is_running(),
"Capture should not be running initially"
);
assert_eq!(capture.sample_rate(), sample_rate);
assert_eq!(capture.channels(), channels);
assert!(capture.start().is_ok(), "First start should succeed");
assert!(
capture.is_running(),
"Capture should be running after start"
);
assert!(capture.start().is_ok(), "Second start should be idempotent");
assert!(capture.is_running(), "Capture should still be running");
thread::sleep(Duration::from_millis(100));
let frames_before = capture.drain().len();
thread::sleep(Duration::from_millis(50));
let frames_after = capture.drain().len();
println!(
"Captured {} frames before, {} after 50ms",
frames_before, frames_after
);
assert!(capture.stop().is_ok(), "First stop should succeed");
assert!(
!capture.is_running(),
"Capture should not be running after stop"
);
assert!(capture.stop().is_ok(), "Second stop should be idempotent");
assert!(!capture.is_running(), "Capture should still be stopped");
assert!(capture.start().is_ok(), "Restart should succeed");
assert!(
capture.is_running(),
"Capture should be running after restart"
);
assert!(capture.stop().is_ok(), "Final stop should succeed");
println!("✓ Lifecycle test passed for config");
}
Err(e) => {
println!("Capture creation failed (may be expected): {}", e);
}
}
}
}
#[test]
fn test_capture_format_handling() {
let clock = PTSClock::new();
let formats = vec![
(48000, 1), (48000, 2), (44100, 2), ];
for (requested_rate, channels) in formats {
match AudioCapture::new(None, requested_rate, channels, clock.clone()) {
Ok(capture) => {
let actual_rate = capture.sample_rate();
let actual_channels = capture.channels();
println!(
"Requested {}Hz/{}ch, got {}Hz/{}ch",
requested_rate, channels, actual_rate, actual_channels
);
assert!(
actual_rate == 44100 || actual_rate == 48000,
"Sample rate should be standard: {}",
actual_rate
);
assert!(
actual_channels >= 1 && actual_channels <= 2,
"Channels should be 1 or 2: {}",
actual_channels
);
}
Err(e) => {
println!(
"Format {}Hz/{}ch not supported: {}",
requested_rate, channels, e
);
}
}
}
}
#[test]
fn test_audio_frame_properties() {
let clock = PTSClock::new();
if let Ok(mut capture) = AudioCapture::new(None, 48000, 2, clock) {
if capture.start().is_ok() {
thread::sleep(Duration::from_millis(100));
let frames = capture.drain();
if !frames.is_empty() {
println!("Captured {} frames for validation", frames.len());
for (i, frame) in frames.iter().take(5).enumerate() {
println!(
"Frame {}: {}Hz, {}ch, {:.3}s, {} samples",
i,
frame.sample_rate,
frame.channels,
frame.timestamp,
frame.samples.len()
);
assert_eq!(
frame.sample_rate, 48000,
"Frame should have correct sample rate"
);
assert!(
frame.channels == 1 || frame.channels == 2,
"Frame should have 1-2 channels"
);
assert!(frame.timestamp >= 0.0, "Timestamp should be non-negative");
assert!(!frame.samples.is_empty(), "Frame should have samples");
let expected_samples_per_channel =
frame.samples.len() / frame.channels as usize;
assert!(
expected_samples_per_channel > 0,
"Should have samples per channel"
);
assert!(
expected_samples_per_channel <= 4800,
"Sample count should be reasonable"
);
for (j, &sample) in frame.samples.iter().take(10).enumerate() {
assert!(
sample.is_finite(),
"Sample {} should be finite: {}",
j,
sample
);
assert!(
sample >= -2.0 && sample <= 2.0,
"Sample {} should be in reasonable range: {}",
j,
sample
);
}
}
for window in frames.windows(2) {
assert!(
window[1].timestamp >= window[0].timestamp,
"Timestamps should be non-decreasing: {} -> {}",
window[0].timestamp,
window[1].timestamp
);
}
} else {
println!("No frames captured (may be expected on quiet systems)");
}
let _ = capture.stop();
}
}
}
#[test]
fn test_pts_clock_synchronization() {
let shared_clock = PTSClock::new();
let start_time = Instant::now();
let configs = vec![(None, 48000, 1), (None, 48000, 2)];
let mut captures = Vec::new();
for (device_id, sample_rate, channels) in configs {
if let Ok(capture) =
AudioCapture::new(device_id, sample_rate, channels, shared_clock.clone())
{
captures.push(capture);
}
}
if captures.is_empty() {
println!("No audio devices available for PTS sync test");
return;
}
println!(
"Testing PTS synchronization with {} captures",
captures.len()
);
for capture in &mut captures {
let _ = capture.start();
}
let mut all_timestamps = Vec::new();
for i in 0..10 {
thread::sleep(Duration::from_millis(20));
let clock_pts = shared_clock.pts();
all_timestamps.push((i, clock_pts, start_time.elapsed().as_secs_f64()));
for (j, capture) in captures.iter().enumerate() {
for frame in capture.drain() {
let pts_diff = (frame.timestamp - clock_pts).abs();
assert!(
pts_diff < 0.1,
"Frame PTS {} should be close to clock PTS {} (diff: {:.3}s) for capture {}",
frame.timestamp,
clock_pts,
pts_diff,
j
);
}
}
}
for capture in &mut captures {
let _ = capture.stop();
}
println!("PTS progression over time:");
for (i, pts, elapsed) in &all_timestamps {
println!(
" Step {}: PTS={:.3}s, Elapsed={:.3}s, Diff={:.3}s",
i,
pts,
elapsed,
(pts - elapsed).abs()
);
}
if let (Some((_, first_pts, first_elapsed)), Some((_, last_pts, last_elapsed))) =
(all_timestamps.first(), all_timestamps.last())
{
let pts_duration = last_pts - first_pts;
let real_duration = last_elapsed - first_elapsed;
let drift = (pts_duration - real_duration).abs();
assert!(drift < 0.1,
"PTS should track real time within 100ms: PTS duration={:.3}s, real duration={:.3}s, drift={:.3}s",
pts_duration, real_duration, drift);
println!("✓ PTS synchronization test passed with {:.3}s drift", drift);
}
}
#[test]
fn test_invalid_device_handling() {
let clock = PTSClock::new();
let invalid_devices = vec![
Some("nonexistent_device_12345".to_string()),
Some("".to_string()), Some("invalid/device\\name".to_string()),
];
for device_id in invalid_devices {
let result = AudioCapture::new(device_id.clone(), 48000, 2, clock.clone());
match result {
Ok(_) => {
println!("Unexpectedly succeeded with device: {:?}", device_id);
}
Err(e) => {
println!("Expected error for device {:?}: {}", device_id, e);
let error_str = e.to_string();
assert!(!error_str.is_empty(), "Error message should not be empty");
assert!(
error_str.contains("device")
|| error_str.contains("Device")
|| error_str.contains("audio")
|| error_str.contains("Audio"),
"Error should mention device or audio: {}",
error_str
);
}
}
}
}
#[test]
fn test_invalid_format_handling() {
let clock = PTSClock::new();
let invalid_formats = vec![
(0, 2), (1000, 2), (500000, 2), (48000, 0), (48000, 10), ];
for (sample_rate, channels) in invalid_formats {
println!("Testing invalid format: {}Hz, {}ch", sample_rate, channels);
let result = AudioCapture::new(None, sample_rate, channels, clock.clone());
match result {
Ok(capture) => {
println!(
"Format adjusted to: {}Hz, {}ch",
capture.sample_rate(),
capture.channels()
);
assert!(
capture.sample_rate() > 0,
"Adjusted sample rate should be positive"
);
assert!(
capture.channels() > 0,
"Adjusted channels should be positive"
);
}
Err(e) => {
println!("Expected error for {}Hz/{}ch: {}", sample_rate, channels, e);
let error_str = e.to_string();
assert!(!error_str.is_empty(), "Error should not be empty");
}
}
}
}
#[test]
fn test_capture_performance() {
let clock = PTSClock::new();
if let Ok(mut capture) = AudioCapture::new(None, 48000, 2, clock) {
if capture.start().is_ok() {
let start_time = Instant::now();
let mut total_frames = 0;
let mut total_samples = 0;
let mut max_frame_gap = 0.0f64;
let mut last_timestamp = None;
while start_time.elapsed() < Duration::from_millis(1000) {
let frames = capture.drain();
total_frames += frames.len();
for frame in frames {
total_samples += frame.samples.len();
if let Some(last_ts) = last_timestamp {
let gap = frame.timestamp - last_ts;
max_frame_gap = max_frame_gap.max(gap);
}
last_timestamp = Some(frame.timestamp);
}
thread::sleep(Duration::from_millis(10));
}
let elapsed = start_time.elapsed().as_secs_f64();
println!("Performance metrics over {:.2}s:", elapsed);
println!(" Total frames: {}", total_frames);
println!(" Total samples: {}", total_samples);
println!(" Frames per second: {:.1}", total_frames as f64 / elapsed);
println!(" Max frame gap: {:.3}s", max_frame_gap);
if total_frames > 0 {
println!(
" Avg samples per frame: {:.1}",
total_samples as f64 / total_frames as f64
);
let fps = total_frames as f64 / elapsed;
assert!(
fps >= 10.0,
"Should capture at least 10 frames per second, got {:.1}",
fps
);
assert!(
max_frame_gap < 0.5,
"Frame gaps should be < 500ms, got {:.3}s",
max_frame_gap
);
let expected_samples = (48000.0 * 2.0 * elapsed) as usize; let sample_ratio = total_samples as f64 / expected_samples as f64;
assert!(
sample_ratio > 0.5 && sample_ratio < 2.0,
"Sample count should be reasonable: expected ~{}, got {} (ratio: {:.2})",
expected_samples,
total_samples,
sample_ratio
);
}
let _ = capture.stop();
println!("✓ Performance test passed");
}
} else {
println!("No audio device available for performance test");
}
}
#[test]
fn test_buffer_management() {
let clock = PTSClock::new();
if let Ok(mut capture) = AudioCapture::new(None, 48000, 2, clock) {
if capture.start().is_ok() {
println!("Testing buffer management with rapid draining");
for i in 0..100 {
let frames = capture.drain();
if i % 20 == 0 {
println!(" Iteration {}: drained {} frames", i, frames.len());
}
thread::sleep(Duration::from_millis(5));
}
thread::sleep(Duration::from_millis(50));
let final_frames = capture.drain();
println!("Final drain: {} frames", final_frames.len());
let _ = capture.stop();
println!("✓ Buffer management test passed");
}
}
}
#[test]
fn test_concurrent_access_safety() {
let clock = PTSClock::new();
if let Ok(mut capture) = AudioCapture::new(None, 48000, 2, clock) {
if capture.start().is_ok() {
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_flag_clone = stop_flag.clone();
let drain_thread = thread::spawn(move || {
let mut drain_count = 0;
while !stop_flag_clone.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(1));
drain_count += 1;
}
drain_count
});
for _ in 0..50 {
let frames = capture.drain();
if !frames.is_empty() {
println!("Main thread drained {} frames", frames.len());
}
thread::sleep(Duration::from_millis(10));
}
stop_flag.store(true, Ordering::Relaxed);
let drain_count = drain_thread.join().unwrap();
println!("Background thread completed {} iterations", drain_count);
let _ = capture.stop();
println!("✓ Concurrent access test passed");
}
}
}
#[test]
fn test_capture_to_encode_pipeline() {
let clock = PTSClock::new();
if let Ok(mut capture) = AudioCapture::new(None, 48000, 2, clock) {
if let Ok(mut encoder) = OpusEncoder::new(48000, 2, 128_000) {
if capture.start().is_ok() {
println!("Testing full capture -> encode pipeline");
let mut total_input_samples = 0;
let mut total_encoded_packets = 0;
let mut total_encoded_bytes = 0;
let start_time = Instant::now();
while start_time.elapsed() < Duration::from_millis(500) {
let frames = capture.drain();
for frame in frames {
total_input_samples += frame.samples.len();
match encoder.encode(&frame) {
Ok(packets) => {
total_encoded_packets += packets.len();
for packet in packets {
total_encoded_bytes += packet.data.len();
assert!(
!packet.data.is_empty(),
"Encoded packet should not be empty"
);
assert!(
packet.timestamp >= 0.0,
"Packet timestamp should be non-negative"
);
assert!(
packet.duration > 0.0,
"Packet duration should be positive"
);
assert!(
packet.duration <= 0.1,
"Packet duration should be reasonable"
);
let toc = packet.data[0];
let config = (toc >> 3) & 0x1F;
assert!(config < 32, "Opus config should be valid: {}", config);
}
}
Err(e) => {
println!("Encoding error: {}", e);
}
}
}
thread::sleep(Duration::from_millis(10));
}
match encoder.flush() {
Ok(packets) => {
total_encoded_packets += packets.len();
for packet in packets {
total_encoded_bytes += packet.data.len();
}
}
Err(e) => {
println!("Flush error: {}", e);
}
}
println!("Pipeline results:");
println!(" Input samples: {}", total_input_samples);
println!(" Encoded packets: {}", total_encoded_packets);
println!(" Encoded bytes: {}", total_encoded_bytes);
if total_input_samples > 0 {
println!(
" Compression ratio: {:.2}x",
(total_input_samples * 4) as f64 / total_encoded_bytes as f64
);
assert!(
total_encoded_packets > 0,
"Should have produced encoded packets"
);
assert!(
total_encoded_bytes > 0,
"Should have produced encoded bytes"
);
let raw_bytes = total_input_samples * 4; let compression_ratio = raw_bytes as f64 / total_encoded_bytes as f64;
assert!(
compression_ratio > 5.0,
"Opus should achieve significant compression: {:.2}x",
compression_ratio
);
}
let _ = capture.stop();
println!("✓ Capture-to-encode pipeline test passed");
}
} else {
println!("Could not create Opus encoder");
}
} else {
println!("No audio device available for pipeline test");
}
}