From 1f24ee8276084bbba081e053c17f8ee9907a21f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 11 Apr 2026 18:42:57 +0200 Subject: [PATCH] feat: compress MP4 videos at Docker build time via FFmpeg Hugo has no video processing, so FFmpeg runs in the builder stage before hugo builds, compressing every content/**/*.mp4 in-place. scripts/compress-videos.sh: - H.264 CRF 28 + preset slow (good web compression) - faststart for progressive streaming - AAC 64k audio - Only replaces source if output is actually smaller, so already-lean videos are skipped Dockerfile: - apk adds ffmpeg alongside hugo in the builder stage - RUN compress-videos.sh runs after COPY . . before pnpm build (compressed files land in public/ via Hugo's copy of page bundles) Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 7 ++++-- scripts/compress-videos.sh | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 scripts/compress-videos.sh diff --git a/Dockerfile b/Dockerfile index 2ab0a09..dfb4d48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Stage 1: Build FROM node:22-alpine AS builder -# Install Hugo -RUN apk add --no-cache hugo +# Install Hugo + FFmpeg (video compression) +RUN apk add --no-cache hugo ffmpeg # Install pnpm RUN corepack enable && corepack prepare pnpm@latest --activate @@ -16,6 +16,9 @@ RUN pnpm install --frozen-lockfile # Copy source files COPY . . +# Compress MP4 videos in-place before Hugo builds +RUN sh scripts/compress-videos.sh content + # Build CSS and Hugo site RUN pnpm build diff --git a/scripts/compress-videos.sh b/scripts/compress-videos.sh new file mode 100644 index 0000000..bf6f604 --- /dev/null +++ b/scripts/compress-videos.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# Compress MP4 page-bundle videos with FFmpeg before Hugo builds. +# +# Usage: sh scripts/compress-videos.sh [content-dir] +# +# Encoding choices: +# H.264 CRF 28 — good web quality, ~60-80% smaller than camera/AI originals +# preset slow — best compression for a given CRF (fine in CI/Docker) +# faststart — moov atom moved to front for progressive streaming +# aac 64k — low-bitrate audio (videos are muted in-player by default) +# +# The script replaces each source file in-place only when the compressed +# output is actually smaller, so already-lean videos are left untouched. + +set -e + +CONTENT_DIR="${1:-content}" + +find "$CONTENT_DIR" -name "*.mp4" | while IFS= read -r src; do + tmp="${src%.mp4}.__tmp.mp4" + + echo "▶ $src" + + ffmpeg -i "$src" \ + -c:v libx264 \ + -crf 28 \ + -preset slow \ + -movflags +faststart \ + -c:a aac \ + -b:a 64k \ + -loglevel error \ + -y "$tmp" + + src_size=$(stat -c%s "$src" 2>/dev/null || stat -f%z "$src") + tmp_size=$(stat -c%s "$tmp" 2>/dev/null || stat -f%z "$tmp") + + if [ "$tmp_size" -lt "$src_size" ]; then + mv "$tmp" "$src" + saved=$(( (src_size - tmp_size) / 1024 )) + echo " ✓ ${src_size} → ${tmp_size} bytes (−${saved} KB)" + else + rm -f "$tmp" + echo " — already optimal, skipped" + fi +done + +echo "Done."