use ez_ffmpeg::frame_export::{FrameExtractor, Sampling, VideoFrame};
use ez_ffmpeg::Input;
use std::fs::File;
use std::io::{BufWriter, Write};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arg = std::env::args().nth(1);
if arg.is_none() {
println!("usage: frame_sampling [input-file] (using lavfi testsrc2 fallback)");
}
let input = |fallback: &str| match &arg {
Some(path) => Input::from(path.clone()),
None => Input::from(fallback).set_format("lavfi"),
};
let mut count = 0u64;
for frame in FrameExtractor::new(input("testsrc2=duration=2:size=640x360:rate=30"))
.sampling(Sampling::EveryNth(10))
.frames()?
{
let frame = frame?;
println!(
"EveryNth(10) export #{:>2}: {}x{} pts={:?}us",
frame.index(),
frame.width(),
frame.height(),
frame.pts_us()
);
count += 1;
}
println!("EveryNth(10) exported {count} frame(s)\n");
let thumbs: Vec<VideoFrame> =
FrameExtractor::new(input("testsrc2=duration=10:size=640x360:rate=30"))
.sampling(Sampling::EverySec(1.0))
.width(160)
.max_frames(8)
.collect_frames()?;
for t in &thumbs {
println!(
"EverySec(1.0) export #{:>2}: {}x{} pts={:?}us",
t.index(),
t.width(),
t.height(),
t.pts_us()
);
}
if thumbs.is_empty() {
println!("no frames sampled; nothing to write");
return Ok(());
}
write_strip("thumbnail_strip.ppm", &thumbs)?;
println!(
"wrote thumbnail_strip.ppm ({} thumbnails, {}x{})",
thumbs.len(),
thumbs.iter().map(|t| t.width()).sum::<u32>(),
thumbs[0].height()
);
Ok(())
}
fn write_strip(path: &str, frames: &[VideoFrame]) -> Result<(), Box<dyn std::error::Error>> {
let height = frames[0].height();
if frames.iter().any(|f| f.height() != height) {
return Err("all thumbnails must share one height".into());
}
let total_width: u32 = frames.iter().map(|f| f.width()).sum();
let mut w = BufWriter::new(File::create(path)?);
write!(w, "P6\n{total_width} {height}\n255\n")?;
for row in 0..height as usize {
for f in frames {
let stride = f.row_bytes();
w.write_all(&f.as_bytes()[row * stride..(row + 1) * stride])?;
}
}
w.flush()?;
Ok(())
}