use std::env; use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use temp_env::with_var; use tempfile::tempdir; pub(super) fn with_fake_media_tools( ffprobe_output: &[u8], ffmpeg_video_output: &[u8], ffmpeg_audio_output: &[u8], test: impl FnOnce(&Path) -> T, ) -> T { let temp_dir = tempdir().expect("tempdir"); fs::write(temp_dir.path().join("ffprobe.out"), ffprobe_output).expect("write ffprobe"); fs::write( temp_dir.path().join("ffmpeg-video.out"), ffmpeg_video_output, ) .expect("write ffmpeg video"); fs::write( temp_dir.path().join("ffmpeg-audio.out"), ffmpeg_audio_output, ) .expect("write ffmpeg audio"); write_executable( temp_dir.path(), "ffprobe", "#!/bin/sh\ncat \"$(dirname \"$0\")/ffprobe.out\"\n", ); write_executable( temp_dir.path(), "ffmpeg", "#!/bin/sh\ncase \" $* \" in\n *\" -map 0:v:0 \"*) cat \"$(dirname \"$0\")/ffmpeg-video.out\" ;;\n *\" -map 0:a:0 \"*) cat \"$(dirname \"$0\")/ffmpeg-audio.out\" ;;\n *) printf 'unexpected ffmpeg args: %s\\n' \"$*\" >&2; exit 64 ;;\nesac\n", ); let prior_path = env::var("PATH").unwrap_or_default(); let merged_path = if prior_path.is_empty() { temp_dir.path().display().to_string() } else { format!("{}:{prior_path}", temp_dir.path().display()) }; let capture_path = temp_dir.path().join("capture.mkv"); fs::write(&capture_path, b"fake-capture").expect("write capture"); with_var("PATH", Some(merged_path.as_str()), || test(&capture_path)) } pub(super) fn frame_json(timestamps: &[f64]) -> Vec { let frames = timestamps .iter() .map(|timestamp| { serde_json::json!({ "best_effort_timestamp_time": format!("{timestamp:.3}") }) }) .collect::>(); serde_json::to_vec(&serde_json::json!({ "frames": frames })).expect("frame json") } pub(super) fn click_track_samples(click_times_s: &[f64], total_samples: usize) -> Vec { let mut samples = vec![0i16; total_samples]; for click_time_s in click_times_s { let start = (*click_time_s * 48_000.0).round() as usize; for sample in samples.iter_mut().skip(start).take(300) { *sample = 18_000; } } samples } pub(super) fn thumbnail_video_bytes(brightness_values: &[u8]) -> Vec { const SIDE: usize = 64; let mut bytes = Vec::with_capacity(brightness_values.len() * SIDE * SIDE); for brightness in brightness_values { let mut frame = vec![20u8; SIDE * SIDE]; for y in SIDE / 4..SIDE - SIDE / 4 { for x in SIDE / 4..SIDE - SIDE / 4 { frame[y * SIDE + x] = *brightness; } } bytes.extend_from_slice(&frame); } bytes } pub(super) fn thumbnail_rgb_video_bytes(colors: &[(u8, u8, u8)]) -> Vec { const SIDE: usize = 64; let mut bytes = Vec::with_capacity(colors.len() * SIDE * SIDE * 3); for (r, g, b) in colors { let mut frame = vec![0u8; SIDE * SIDE * 3]; for y in SIDE / 4..SIDE - SIDE / 4 { for x in SIDE / 4..SIDE - SIDE / 4 { let offset = (y * SIDE + x) * 3; frame[offset] = *r; frame[offset + 1] = *g; frame[offset + 2] = *b; } } bytes.extend_from_slice(&frame); } bytes } pub(super) fn audio_samples_to_bytes(samples: &[i16]) -> Vec { samples .iter() .flat_map(|sample| sample.to_le_bytes()) .collect() } fn write_executable(dir: &Path, name: &str, contents: &str) { let path = dir.join(name); fs::write(&path, contents).expect("write script"); let mut permissions = fs::metadata(&path).expect("script metadata").permissions(); permissions.set_mode(0o755); fs::set_permissions(&path, permissions).expect("script permissions"); }