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