#![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;
#[test]
fn test_device_enumeration_safe() {
let result = list_audio_devices();
match result {
Ok(devices) => {
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) => {
println!("Audio enumeration error (expected on some systems): {}", e);
}
}
}
#[test]
fn test_capture_lifecycle_safe() {
use crabcamera::audio::AudioCapture;
let clock = PTSClock::new();
match AudioCapture::new(None, 48000, 2, clock) {
Ok(mut capture) => {
assert!(capture.start().is_ok());
assert!(capture.start().is_ok());
std::thread::sleep(Duration::from_millis(50));
assert!(capture.stop().is_ok());
assert!(capture.stop().is_ok());
}
Err(e) => {
println!("Audio capture unavailable: {}", e);
}
}
}
#[test]
fn test_encoded_audio_headers_valid() {
let mut encoder = OpusEncoder::new(48000, 2, 128_000).expect("Opus encoder should create");
let frame = AudioFrame {
samples: vec![0.0f32; 960 * 2], 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];
assert!(!packet.data.is_empty(), "Encoded data should not be empty");
let toc = packet.data[0];
let config = (toc >> 3) & 0x1F;
assert!(config < 32, "TOC config should be valid: {}", config);
assert!(packet.timestamp >= 0.0, "Timestamp should be non-negative");
assert!(
(packet.duration - 0.020).abs() < 0.001,
"Duration should be ~20ms, got {}",
packet.duration
);
}
#[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");
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");
assert!(stats.video_frames > 0, "Should have video frames");
assert!(stats.bytes_written > 0, "Should have bytes written");
let metadata = std::fs::metadata(&output).expect("File should exist");
assert!(metadata.len() > 0, "File should have content");
let file_start = std::fs::read(&output).expect("Read file");
assert!(file_start.len() >= 8, "File should have MP4 header");
assert_eq!(&file_start[4..8], b"ftyp", "Should have ftyp box");
}
#[test]
fn test_av_recording_config_with_audio() {
let dir = tempdir().expect("Create temp dir");
let output = dir.path().join("av_recording.mp4");
let config = RecordingConfig::new(320, 240, 30.0).with_audio(AudioConfig {
device_id: None, sample_rate: 48000,
channels: 2,
bitrate: 128_000,
});
match Recorder::new(&output, config) {
Ok(recorder) => {
assert!(recorder.audio_enabled(), "Audio should be enabled");
drop(recorder);
}
Err(e) => {
println!("Recorder creation failed (may be expected): {}", e);
}
}
}
#[test]
#[ignore = "Timing-sensitive test - CI environments have variable latency"]
fn test_pts_clock_sync_within_policy() {
let clock = PTSClock::new();
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);
let start = std::time::Instant::now();
for i in 0..30 {
video_pts.push(clock.pts());
if i % 2 == 0 || i % 3 == 0 {
audio_pts.push(clock.pts());
}
std::thread::sleep(frame_duration);
}
let elapsed = start.elapsed();
let expected_duration = 1.0; 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
);
for window in video_pts.windows(2) {
let delta = window[1] - window[0];
assert!(delta >= 0.0, "PTS should be monotonically increasing");
assert!(
delta < 0.100,
"Frame delta should be < 100ms, got {:.3}s",
delta
);
}
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
);
}
}
#[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");
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");
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");
assert!(stats.video_frames > 0, "Should have video frames");
assert!(stats.bytes_written > 0, "Should have bytes written");
let metadata = std::fs::metadata(&output).expect("File should exist");
assert!(metadata.len() > 10_000, "A/V file should be substantial");
}
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(),
)
}