diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-02 20:53:32 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-02 20:53:32 +0200 |
| commit | 5c5d338938e286a5a24048186d79e36b192fbe56 (patch) | |
| tree | 0a073791809f650bc7a5f14b5048e5e3e6e86d12 /src | |
| parent | 0687e489e0e2b273f8c3c466ad77cb2066bc168f (diff) | |
| download | ak820-upload-master.tar.gz ak820-upload-master.zip | |
Runs the same crop/scale/fps/frame-cap ffmpeg chain to a GIF file
instead of uploading, so the result can be previewed or shared.
Replaces the separate video2ak820 script.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 38 |
1 files changed, 30 insertions, 8 deletions
diff --git a/src/main.rs b/src/main.rs index fe2e00d..58633b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -112,30 +112,52 @@ fn payload(frames: &[u8], delay_ms: u32) -> Vec<u8> { out } -/// Decode any ffmpeg-readable input to raw 128x128 RGB565LE frames. -fn decode(input: &str, fps: u32) -> Vec<u8> { - let vf = format!("fps={fps},crop='min(iw,ih)':'min(iw,ih)',scale=128:128:flags=lanczos"); +/// Run ffmpeg on `input` with the keyboard's crop/scale/fps/frame cap, then +/// `extra` (filter suffix) and `out_args` (output options and target). +fn ffmpeg(input: &str, fps: u32, extra: &str, out_args: &[&str]) -> Vec<u8> { + let vf = format!("fps={fps},crop='min(iw,ih)':'min(iw,ih)',scale=128:128:flags=lanczos{extra}"); let out = Command::new("ffmpeg") - .args(["-v", "error", "-i", input, "-vf", &vf, "-frames:v"]) + .args(["-v", "error", "-y", "-i", input, "-filter_complex", &vf, "-frames:v"]) .arg(MAX_FRAMES.to_string()) - .args(["-f", "rawvideo", "-pix_fmt", "rgb565le", "-"]) + .args(out_args) .output() .unwrap_or_else(|e| die(format!("ffmpeg: {e}"))); - if !out.status.success() || out.stdout.len() < FRAME_BYTES { + if !out.status.success() { die(format!("ffmpeg failed on {input}: {}", String::from_utf8_lossy(&out.stderr).trim())); } out.stdout } +/// Decode any ffmpeg-readable input to raw 128x128 RGB565LE frames. +fn decode(input: &str, fps: u32) -> Vec<u8> { + let raw = ffmpeg(input, fps, "", &["-f", "rawvideo", "-pix_fmt", "rgb565le", "-"]); + if raw.len() < FRAME_BYTES { + die(format!("{input}: no frames decoded")); + } + raw +} + fn main() { - let args: Vec<String> = std::env::args().skip(1).collect(); + let mut args: Vec<String> = std::env::args().skip(1).collect(); + let gif = args.iter().position(|a| a == "--gif").map(|i| { + args.remove(i); + if i < args.len() { args.remove(i) } else { die("--gif needs an output file") } + }); let (input, fps) = match args.as_slice() { [i] => (i.as_str(), 15), [i, f] => (i.as_str(), f.parse().ok().filter(|f| (2..=50).contains(f)) .unwrap_or_else(|| die("fps must be 2..50 (max frame time is ~510 ms)"))), - _ => die("usage: ak820-upload <image|gif|video> [fps, default 15]"), + _ => die("usage: ak820-upload [--gif out.gif] <image|gif|video> [fps, default 15]"), }; + // --gif: write what would be uploaded as a GIF instead of uploading. + if let Some(out) = gif { + ffmpeg(input, fps, ",split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=4", + &["-loop", "0", &out]); + eprintln!("wrote {out}"); + return; + } + let frames = decode(input, fps); let n = frames.len() / FRAME_BYTES; let data = payload(&frames, 1000 / fps); |
