initial
This commit is contained in:
commit
b45450f65a
3 changed files with 1382 additions and 0 deletions
1260
Cargo.lock
generated
Normal file
1260
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal 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
113
src/main.rs
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue