This commit is contained in:
mehbark 2024-11-21 15:03:41 -05:00
commit b45450f65a
3 changed files with 1382 additions and 0 deletions

1260
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "bad-apple"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.20", features = ["derive"] }
image = "0.25.5"
tempfile = "3.13.0"

113
src/main.rs Normal file
View file

@ -0,0 +1,113 @@
use std::{
fs::{self, File},
io::{self, BufWriter, Write},
path::{Path, PathBuf},
process::Command,
thread,
time::{self, Duration, Instant},
};
use clap::Parser;
use image::{GenericImageView, ImageReader};
const BAD_APPLE_PATH: &str = "../../bad-apple.webm";
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(index = 1)]
height: u32,
#[arg(short, long, default_value = BAD_APPLE_PATH)]
path: String,
}
fn main() -> io::Result<()> {
let Args { height, path } = Args::parse();
let width = 4 * height / 3;
// cleaned up when dropped, which is good, but think!
let work_dir = tempfile::tempdir()?;
let frame_paths = get_frames(File::open(&path)?, &work_dir)?;
let frame_rate = get_framerate(File::open(path)?)?;
let time_per_frame = Duration::from_micros((1e6 / frame_rate) as u64);
let first_frame = ImageReader::open(&frame_paths[0])?.decode().unwrap();
let real_width = first_frame.width();
let real_height = first_frame.height();
let max_pixel_bytes = b"\x1b[48;2;255;255;255m ".len();
let max_row_bytes = max_pixel_bytes * (width as usize) + b"\n".len();
let mut writer = BufWriter::with_capacity(max_row_bytes * (height as usize), io::stdout());
let mut start_time = Instant::now();
write!(writer, "\x1b[2J")?;
for path in frame_paths {
let image = ImageReader::open(path)?.decode().unwrap();
for row in 0..height {
for col in 0..width {
let col = col * real_width / width;
let row = row * real_height / height;
let image::Rgba([r, g, b, _]) = image.get_pixel(col, row);
write!(writer, "\x1b[48;2;{r};{g};{b}m ")?;
}
writeln!(writer)?;
}
let end_time = Instant::now();
let late = end_time.duration_since(start_time) > time_per_frame;
writer.flush()?;
println!("\x1b[K\x1b[0m{:?}", end_time.duration_since(start_time));
println!("\x1b[H");
thread::sleep(time_per_frame.saturating_sub(end_time.duration_since(start_time)));
start_time = end_time;
}
Ok(())
}
fn get_framerate(video: File) -> io::Result<f32> {
// ffprobe -v 0 -of csv=p=0 -select_streams v:0 -show_entries stream=r_frame_rate infile
let output = Command::new("ffprobe")
.args([
"-v",
"0",
"-of",
"csv=p=0",
"-select_streams",
"v:0",
"-show_entries",
"stream=r_frame_rate",
"-",
])
.stdin(video)
.output()?
.stdout;
let output = String::from_utf8_lossy(&output);
let (num, dem) = output
.trim()
.split_once('/')
.expect("ffprobe returns num/dem");
println!("{num} {dem}");
let num: f32 = num.parse().expect("ffprobe numerator is number");
let dem: f32 = dem.parse().expect("ffprobe denonimator is number");
Ok(num / dem)
}
fn get_frames(video: File, work_path: impl AsRef<Path>) -> io::Result<Vec<PathBuf>> {
// ffmpeg -r 1 -i file.mp4 -r 1 "$filename%03d.png
let mut cmd = Command::new("ffmpeg")
.args(["-r", "1", "-i", "pipe:", "-r", "1", "%04d.png"])
.stdin(video)
.current_dir(&work_path)
.spawn()?;
let exit_code = cmd.wait()?;
assert!(exit_code.success(), "bad exit code {exit_code}");
let dir = fs::read_dir(work_path)?;
let mut paths: Vec<PathBuf> = dir.flatten().map(|f| f.path()).collect();
paths.sort_by_cached_key(|f| f.display().to_string());
Ok(paths)
}