← all scripts

demo/gif_to_mp4.sh

28 of 28 statements covered (100.00%).

coveredmissednever traced by Bash (not counted)A line ending in … continues the statement above it and shares its fate.

1#!/usr/bin/env bash
2# ─────────────────────────────────────────────────────────────────────────────
3# Convert a demo GIF to an MP4 for embedding outside GitHub
4# ─────────────────────────────────────────────────────────────────────────────
5# GitHub renders the committed GIFs directly, but most other surfaces
6# (LinkedIn, Slack, blog posts, internal wikis) treat native video far
7# better than GIFs — autoplay, scrubbing, and a fraction of the bytes.
8# This helper converts any of the demo recordings (or any GIF) into a
9# widely-compatible H.264 MP4:
10#
11# - yuv420p pixel format (the one every player/platform accepts)
12# - +faststart (moov atom up front, so streaming starts immediately)
13# - even-dimension scaling (H.264 requires width/height % 2 == 0;
14# agg output is often odd-height)
15#
16# MP4s are deliberately NOT committed to the repository — demo/*.mp4 is
17# gitignored, and the tracked-media policy in
18# .github/scripts/validate_demo_gifs.py governs GIFs only. Generate the
19# MP4 locally whenever you need one.
20#
21# Usage:
22# bash demo/gif_to_mp4.sh <input.gif> [output.mp4]
23#
24# bash demo/gif_to_mp4.sh demo/autopilot-claude-code.gif
25# bash demo/gif_to_mp4.sh demo/live_demo.gif /tmp/live_demo.mp4
26#
27# Options (via environment variables):
28# MP4_FPS=12 Output frame rate (default: 12 — plenty for terminal
29# recordings, keeps files small)
30#
31# Prerequisites:
32# - ffmpeg: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)
33#
34# An existing output file is replaced (conversions are cheap and
35# deterministic; the GIF remains the source of truth).
36# ─────────────────────────────────────────────────────────────────────────────
37
3811set -euo pipefail
39
40usage() {
416 grep '^#' "$0" | sed -n '2,40p' | sed 's/^# \{0,1\}//'
42}
43
4431if [ "$#" -lt 1 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
452 usage
462 exit 2
47fi
48
499INPUT="$1"
509OUTPUT="${2:-${INPUT%.gif}.mp4}"
519FPS="${MP4_FPS:-12}"
52
539if ! command -v ffmpeg >/dev/null 2>&1; then
541 echo "error: ffmpeg is not installed." >&2
551 echo " brew install ffmpeg (macOS)" >&2
561 echo " apt install ffmpeg (Linux)" >&2
571 exit 1
58fi
59
608if [ ! -f "$INPUT" ]; then
611 echo "error: input GIF not found: $INPUT" >&2
621 exit 1
63fi
64
657case "$INPUT" in
666 *.gif) : ;;
672 *) echo "error: input must be a .gif file, got: $INPUT" >&2; exit 1 ;;
68esac
69
706if [ "$INPUT" = "$OUTPUT" ]; then
711 echo "error: input and output are the same path: $INPUT" >&2
721 exit 1
73fi
74
755case "$FPS" in
762 ''|*[!0-9]*) echo "error: MP4_FPS must be a positive integer, got: $FPS" >&2; exit 1 ;;
77esac
78
79# trunc(n/2)*2 rounds each dimension down to even, which H.264 + yuv420p
80# require; lanczos keeps terminal text crisp through the (at most 1px)
81# rescale.
824ffmpeg -hide_banner -loglevel error -y \
83 -i "$INPUT" \
84 -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2:flags=lanczos" \
85 -pix_fmt yuv420p \
86 -movflags +faststart \
87 -r "$FPS" \
88 "$OUTPUT"
89
9012SIZE=$(du -h "$OUTPUT" | cut -f1)
914echo "✓ MP4 written: ${OUTPUT} (${SIZE}, ${FPS} fps)"
924echo " Note: MP4s are gitignored on purpose — the committed GIF stays the source of truth."