Media processing pulled out of the website that first needed it. ffmpeg, ImageMagick, libvips and exiftool are a real installation rather than an npm install, and encoder settings get revisited as browser support moves — neither of which belongs on the same clock as page templates and database schemas. No processing code yet. What is here is the tooling requirement, an installer, and docs/PIPELINE.md writing down what has NOT been decided — how a consumer talks to this, whether processing blocks an upload, where output goes — so those get chosen deliberately instead of by whatever gets written first. verify.sh checks by running: it encodes a video and probes the result against what ffmpeg was told to make, loads sharp under Bun specifically, and asserts a re-encode still strips GPS. That last one guards a real leak — if a future sharp changes its default, this fails a check rather than quietly publishing the coordinates of a client's living room. Confirmed non-vacuous by hiding ffmpeg and watching it exit 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
5.4 KiB
Bash
Executable File
147 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Check the media tooling by RUNNING it, not by asking the package manager whether it is present.
|
|
#
|
|
# A package can be installed and still not work: sharp is a native module that can resolve to a
|
|
# broken wasm build, and a codec can be missing from an ffmpeg that reports a version happily. Every
|
|
# check here does real work and compares the result against what was asked for.
|
|
#
|
|
# Exits non-zero if anything fails, so it is usable in CI or as the last step of install.sh.
|
|
|
|
set -uo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
WORK="$(mktemp -d)"
|
|
trap 'rm -rf "$WORK"' EXIT
|
|
|
|
FAILED=0
|
|
|
|
pass() { printf ' \033[32mok\033[0m %s\n' "$1"; }
|
|
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAILED=1; }
|
|
note() { printf ' \033[33m--\033[0m %s\n' "$1"; }
|
|
|
|
echo
|
|
echo "Binaries on PATH"
|
|
|
|
# `magick` is deliberately absent from this list: Ubuntu 24.04 ships ImageMagick 6, which provides
|
|
# convert/identify and no `magick`. See docs/TOOLING.md.
|
|
for cmd in ffmpeg ffprobe convert identify exiftool vipsthumbnail cwebp dwebp avifenc avifdec; do
|
|
if command -v "$cmd" >/dev/null 2>&1; then
|
|
pass "$cmd"
|
|
else
|
|
fail "$cmd is not on PATH"
|
|
fi
|
|
done
|
|
|
|
echo
|
|
echo "ffmpeg encodes and ffprobe reads back what was asked for"
|
|
|
|
if ffmpeg -hide_banner -loglevel error \
|
|
-f lavfi -i testsrc=duration=3:size=1280x720:rate=30 \
|
|
-c:v libx264 -pix_fmt yuv420p "$WORK/test.mp4" 2>"$WORK/ffmpeg.err"; then
|
|
|
|
probe=$(ffprobe -v error \
|
|
-show_entries stream=codec_name,width,height \
|
|
-show_entries format=duration \
|
|
-of default=noprint_wrappers=1 "$WORK/test.mp4" 2>/dev/null)
|
|
|
|
# Compared against what ffmpeg was TOLD to produce. A version string proves nothing; this proves
|
|
# the h264 encoder is actually present and the container is readable.
|
|
for expect in "codec_name=h264" "width=1280" "height=720" "duration=3.000000"; do
|
|
if grep -qx "$expect" <<<"$probe"; then pass "$expect"; else fail "expected $expect"; fi
|
|
done
|
|
else
|
|
fail "ffmpeg could not encode h264: $(head -1 "$WORK/ffmpeg.err")"
|
|
fi
|
|
|
|
echo
|
|
echo "ffmpeg extracts a poster frame"
|
|
|
|
if ffmpeg -hide_banner -loglevel error -i "$WORK/test.mp4" -ss 1 -frames:v 1 "$WORK/poster.jpg" 2>/dev/null \
|
|
&& [ -s "$WORK/poster.jpg" ]; then
|
|
pass "poster.jpg $(identify -format '%wx%h' "$WORK/poster.jpg" 2>/dev/null)"
|
|
else
|
|
fail "could not extract a poster frame"
|
|
fi
|
|
|
|
echo
|
|
echo "sharp runs under Bun"
|
|
|
|
# Run with the REPO as cwd, deliberately. Resolved from anywhere else, Bun finds sharp in its global
|
|
# install cache and lands on the wasm32 build, which is missing @emnapi/runtime and dies with
|
|
# "sharp.libvipsVersion is not a function" — a broken-looking failure that is not a broken install.
|
|
if [ -d "$REPO_ROOT/node_modules/sharp" ]; then
|
|
if out=$(cd "$REPO_ROOT" && bun -e '
|
|
import sharp from "sharp";
|
|
const px = 64;
|
|
const src = await sharp({ create: { width: 300, height: 200, channels: 3, background: "#888" } })
|
|
.jpeg().toBuffer();
|
|
for (const fmt of ["webp", "avif", "jpeg"]) {
|
|
const buf = await sharp(src).resize(px)[fmt]().toBuffer();
|
|
const m = await sharp(buf).metadata();
|
|
if (m.width !== px) throw new Error(`${fmt}: expected ${px}px, got ${m.width}`);
|
|
}
|
|
console.log(`${sharp.versions.sharp} libvips ${sharp.versions.vips}`);
|
|
' 2>&1); then
|
|
pass "sharp $out"
|
|
else
|
|
fail "sharp failed under Bun: $(tail -3 <<<"$out" | tr '\n' ' ')"
|
|
fi
|
|
else
|
|
note "sharp not installed — run 'bun install' in $REPO_ROOT"
|
|
FAILED=1
|
|
fi
|
|
|
|
echo
|
|
echo "exiftool reads metadata, and a sharp re-encode strips it"
|
|
|
|
if [ -s "$WORK/poster.jpg" ]; then
|
|
exiftool -q -overwrite_original \
|
|
-GPSLatitude=38.7223 -GPSLatitudeRef=N \
|
|
-GPSLongitude=-9.1393 -GPSLongitudeRef=W \
|
|
-Make="Apple" -Model="iPhone 15 Pro" "$WORK/poster.jpg" 2>/dev/null
|
|
|
|
if exiftool -s3 -GPSLatitude "$WORK/poster.jpg" 2>/dev/null | grep -q .; then
|
|
pass "exiftool wrote and read back GPS tags"
|
|
|
|
if [ -d "$REPO_ROOT/node_modules/sharp" ] \
|
|
&& (cd "$REPO_ROOT" && bun -e "
|
|
import sharp from 'sharp';
|
|
await sharp('$WORK/poster.jpg').resize(300).jpeg().toFile('$WORK/stripped.jpg');
|
|
" 2>/dev/null); then
|
|
|
|
# The point of the check: sharp drops EXIF unless .withMetadata() is called. If that ever
|
|
# stops being true, client GPS coordinates start getting published and nothing else says so.
|
|
leaked=$(exiftool -s3 -GPSLatitude -GPSLongitude -Make -Model "$WORK/stripped.jpg" 2>/dev/null | grep -c .)
|
|
if [ "$leaked" -eq 0 ]; then
|
|
pass "GPS, Make and Model gone after re-encode"
|
|
else
|
|
fail "$leaked identifying tag(s) SURVIVED the re-encode"
|
|
fi
|
|
else
|
|
note "skipped strip check — sharp unavailable"
|
|
fi
|
|
else
|
|
fail "exiftool could not write test tags"
|
|
fi
|
|
else
|
|
note "skipped — no poster frame to test with"
|
|
fi
|
|
|
|
echo
|
|
echo "Command-line encoders"
|
|
|
|
cwebp -quiet "$WORK/poster.jpg" -o "$WORK/t.webp" 2>/dev/null && [ -s "$WORK/t.webp" ] \
|
|
&& pass "cwebp" || fail "cwebp"
|
|
avifenc -s 10 "$WORK/poster.jpg" "$WORK/t.avif" >/dev/null 2>&1 && [ -s "$WORK/t.avif" ] \
|
|
&& pass "avifenc" || fail "avifenc"
|
|
vipsthumbnail "$WORK/poster.jpg" -s 100 -o "$WORK/thumb-%s.jpg" >/dev/null 2>&1 \
|
|
&& [ -s "$WORK/thumb-poster.jpg" ] && pass "vipsthumbnail" || fail "vipsthumbnail"
|
|
|
|
echo
|
|
if [ "$FAILED" -eq 0 ]; then
|
|
printf '\033[32mAll checks passed.\033[0m\n\n'
|
|
else
|
|
printf '\033[31mSomething failed above.\033[0m See docs/TOOLING.md.\n\n'
|
|
fi
|
|
exit "$FAILED"
|