#![allow(dead_code)]
use std::{
path::{Path, PathBuf},
process::Command,
sync::Once,
};
pub fn init_ffmpeg() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
ffmpeg_next::init().expect("ffmpeg init");
});
}
pub struct Corpus {
dir: tempfile::TempDir,
}
impl Corpus {
pub fn new() -> Option<Self> {
if !ffmpeg_cli_available() {
eprintln!(
"skip: the `ffmpeg` CLI is not on PATH — this lane generates its own test media \
(multi-track / cover-art / timecode containers and sine WAVs) because the committed \
corpus has none of those shapes."
);
return None;
}
init_ffmpeg();
Some(Self {
dir: tempfile::tempdir().expect("temp dir"),
})
}
fn path(&self, name: &str) -> PathBuf {
self.dir.path().join(name)
}
#[rustfmt::skip]
pub fn multi_track_mkv(&self) -> PathBuf {
let out = self.path("multi.mkv");
if out.exists() {
return out;
}
let font = self.path("font.ttf");
std::fs::write(&font, FONT_PAYLOAD).expect("write attachment payload");
let subs = self.path("subs.srt");
std::fs::write(&subs, SUBRIP).expect("write subtitles");
run_ffmpeg(&[
"-f", "lavfi", "-i", "testsrc2=size=160x120:rate=25:duration=2",
"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000:duration=2",
"-i", subs.to_str().expect("utf-8 path"),
"-map", "0:v", "-map", "1:a", "-map", "2:s",
"-c:v", "libx264", "-preset", "ultrafast", "-g", "25", "-pix_fmt", "yuv420p",
"-c:a", "aac", "-ac", "2", "-ar", "48000",
"-c:s", "srt",
"-attach", font.to_str().expect("utf-8 path"),
"-metadata:s:t", "mimetype=application/x-truetype-font",
"-metadata:s:t", "filename=font.ttf",
out.to_str().expect("utf-8 path"),
]);
out
}
#[rustfmt::skip]
pub fn cover_art_mp3(&self) -> PathBuf {
let out = self.path("cover.mp3");
if out.exists() {
return out;
}
let cover = self.path("cover.png");
run_ffmpeg(&[
"-f", "lavfi", "-i", "color=c=red:size=32x32:duration=0.04:rate=25",
"-frames:v", "1",
cover.to_str().expect("utf-8 path"),
]);
run_ffmpeg(&[
"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=44100:duration=2",
"-i", cover.to_str().expect("utf-8 path"),
"-map", "0:a", "-map", "1:v",
"-c:a", "libmp3lame", "-c:v", "copy",
"-id3v2_version", "3",
"-disposition:v", "attached_pic",
out.to_str().expect("utf-8 path"),
]);
out
}
pub fn subrip(&self) -> PathBuf {
let out = self.path("cues.srt");
if out.exists() {
return out;
}
std::fs::write(
&out,
"1\n00:00:01,000 --> 00:00:02,000\nfirst cue\n\n\
2\n00:00:03,000 --> 00:00:04,500\nsecond cue\n\n\
3\n00:00:06,000 --> 00:00:07,000\nthird cue\n\n",
)
.expect("writing the subrip fixture");
out
}
pub fn subrip_bulky(&self) -> PathBuf {
let out = self.path("bulky.srt");
if out.exists() {
return out;
}
let mut text = String::new();
for (index, marker) in ["alpha", "beta", "gamma"].iter().enumerate() {
let start = index * 3;
text.push_str(&format!(
"{}\n00:00:{:02},000 --> 00:00:{:02},000\n{}{}\n\n",
index + 1,
start + 1,
start + 2,
marker,
"x".repeat(8192),
));
}
std::fs::write(&out, text).expect("writing the bulky subrip fixture");
out
}
#[rustfmt::skip]
pub fn timecode_mov(&self) -> PathBuf {
let out = self.path("timecode.mov");
if out.exists() {
return out;
}
run_ffmpeg(&[
"-f", "lavfi", "-i", "testsrc2=size=160x120:rate=25:duration=2",
"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000:duration=2",
"-map", "0:v", "-map", "1:a",
"-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
"-c:a", "aac",
"-timecode", "01:00:00:00",
out.to_str().expect("utf-8 path"),
]);
out
}
pub fn exif_oriented_jpeg(&self, tag: u16) -> PathBuf {
let out = self.path(&format!("orient{tag}.jpg"));
if out.exists() {
return out;
}
let plain = self.path("orient-plain.jpg");
if !plain.exists() {
run_ffmpeg(&[
"-f",
"lavfi",
"-i",
"color=c=red:size=32x24:duration=0.04:rate=25",
"-frames:v",
"1",
plain.to_str().expect("utf-8 path"),
]);
}
let jpeg = std::fs::read(&plain).expect("read the plain jpeg");
assert_eq!(
&jpeg[..2],
b"\xff\xd8",
"an ffmpeg-written JPEG starts with SOI"
);
let mut spliced = Vec::with_capacity(jpeg.len() + 34);
spliced.extend_from_slice(&jpeg[..2]);
spliced.extend_from_slice(&exif_orientation_app1(tag));
spliced.extend_from_slice(&jpeg[2..]);
std::fs::write(&out, &spliced).expect("write the oriented jpeg");
out
}
pub fn indexed_png(&self) -> PathBuf {
let out = self.path("indexed.png");
if out.exists() {
return out;
}
#[rustfmt::skip]
run_ffmpeg(&[
"-f", "lavfi", "-i", "testsrc2=size=32x24:rate=1:duration=1",
"-frames:v", "1", "-pix_fmt", "pal8",
out.to_str().expect("utf-8 path"),
]);
out
}
pub fn software_only_video(&self) -> PathBuf {
let out = self.path("swonly.webm");
if out.exists() {
return out;
}
#[rustfmt::skip]
run_ffmpeg(&[
"-f", "lavfi", "-i", "testsrc2=size=1280x720:rate=5:d=1",
"-c:v", "libvpx", "-pix_fmt", "yuv420p",
out.to_str().expect("utf-8 path"),
]);
out
}
pub fn cropped_h264(&self) -> PathBuf {
let out = self.path("cropped.mp4");
if out.exists() {
return out;
}
#[rustfmt::skip]
run_ffmpeg(&[
"-f", "lavfi", "-i", "testsrc2=size=1920x1088:rate=5:d=1",
"-c:v", "libx264",
"-x264-params", "crop-rect=0,0,1888,1056",
"-pix_fmt", "yuv420p",
out.to_str().expect("utf-8 path"),
]);
out
}
pub fn surround_flac(&self) -> PathBuf {
let out = self.path("surround.flac");
if out.exists() {
return out;
}
let src = self.sine_wav("surround-src.wav", 48_000, 6, 440, 1.0);
#[rustfmt::skip]
run_ffmpeg(&[
"-i", src.to_str().expect("utf-8 path"),
"-c:a", "flac", "-sample_fmt", "s32",
out.to_str().expect("utf-8 path"),
]);
out
}
pub fn gray_png(&self, width: u32, height: u32) -> PathBuf {
let out = self.path(&format!("gray-{width}x{height}.png"));
if out.exists() {
return out;
}
let size = format!("color=c=gray:s={width}x{height}:r=1:d=1");
#[rustfmt::skip]
run_ffmpeg(&[
"-f", "lavfi", "-i", &size,
"-frames:v", "1", "-pix_fmt", "gray",
out.to_str().expect("utf-8 path"),
]);
out
}
pub fn monochrome_png(&self) -> PathBuf {
let out = self.path("mono.png");
if out.exists() {
return out;
}
#[rustfmt::skip]
run_ffmpeg(&[
"-f", "lavfi", "-i", "testsrc2=size=32x24:rate=1:duration=1",
"-frames:v", "1", "-pix_fmt", "monob",
out.to_str().expect("utf-8 path"),
]);
out
}
#[rustfmt::skip]
pub fn sine_wav(&self, name: &str, rate: u32, channels: u8, hz: u32, seconds: f32) -> PathBuf {
let out = self.path(name);
if out.exists() {
return out;
}
let source = format!("sine=frequency={hz}:sample_rate={rate}:duration={seconds}");
run_ffmpeg(&[
"-f", "lavfi", "-i", &source,
"-ac", &channels.to_string(),
"-c:a", "pcm_s16le",
out.to_str().expect("utf-8 path"),
]);
out
}
}
fn exif_orientation_app1(orientation: u16) -> Vec<u8> {
let mut ifd = Vec::new();
ifd.extend_from_slice(&1u16.to_le_bytes()); ifd.extend_from_slice(&0x0112u16.to_le_bytes()); ifd.extend_from_slice(&3u16.to_le_bytes()); ifd.extend_from_slice(&1u32.to_le_bytes()); ifd.extend_from_slice(&orientation.to_le_bytes()); ifd.extend_from_slice(&0u16.to_le_bytes()); ifd.extend_from_slice(&0u32.to_le_bytes());
let mut payload = Vec::new();
payload.extend_from_slice(b"Exif\0\0");
payload.extend_from_slice(b"II"); payload.extend_from_slice(&42u16.to_le_bytes()); payload.extend_from_slice(&8u32.to_le_bytes()); payload.extend_from_slice(&ifd);
let mut segment = vec![0xFF, 0xE1];
let len = u16::try_from(payload.len() + 2).expect("the segment is 34 bytes");
segment.extend_from_slice(&len.to_be_bytes());
segment.extend_from_slice(&payload);
segment
}
pub const FONT_PAYLOAD: &[u8] = b"FAKE-TTF-PAYLOAD-0123456789";
const SUBRIP: &str = "1\n00:00:00,000 --> 00:00:01,000\nhello\n\n\
2\n00:00:01,000 --> 00:00:02,000\nworld\n\n";
fn ffmpeg_cli_available() -> bool {
Command::new("ffmpeg")
.arg("-version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn run_ffmpeg(args: &[&str]) {
let out = Command::new("ffmpeg")
.args(["-y", "-loglevel", "error"])
.args(args)
.output()
.expect("run ffmpeg");
assert!(
out.status.success(),
"ffmpeg {args:?} failed:\n{}",
String::from_utf8_lossy(&out.stderr),
);
}
pub fn raw_packet_order(path: &Path) -> Vec<(usize, Option<i64>)> {
let mut input = ffmpeg_next::format::input(path).expect("open input");
let mut out = Vec::new();
loop {
let mut packet = ffmpeg_next::Packet::empty();
match packet.read(&mut input) {
Ok(()) => out.push((packet.stream(), packet.pts())),
Err(ffmpeg_next::Error::Eof) => break,
Err(ffmpeg_next::Error::InvalidData) => continue,
Err(e) => panic!("read: {e}"),
}
}
out
}
#[track_caller]
pub fn accepted<E: std::fmt::Debug>(status: Result<mediadecode::Sent, E>, what: &str) {
assert_eq!(
status.unwrap_or_else(|e| panic!("{what}: {e:?}")),
mediadecode::Sent::Accepted,
"{what}: the session asked to be drained where the test expected room",
);
}