#!/bin/bash # SPDX-License-Identifier: BSD-3-Clause # build_video.sh - build the final open-iov 1.0.0 demo video: a drawtext # title card, the split-screen main segment (two GZDOOM VMs, one per pane, # captured through the KVMFR ivshmem path), and an optional end card, # concatenated into one 1280x720@30 libx264 yuv420p mp4. # # Usage: # build_video.sh [end-text] # build_video.sh [end-text] # title-text text for the title card (required, non-empty) # main.mp4 pre-built split-screen video (e.g. from split_screen.sh) # dirA dirB two frame-%04d.ppm dirs; split_screen.sh (next to this # script) is invoked first at FPS to build the main segment # end-text optional text for the end card (omitted = no end card) # # Output: $OUT if set, else ./open-iov-1.0.0-demo.mp4 in the current dir. # Env knobs: FPS (default 30), CARD_SECS (default 5), FONTSIZE (default 48). # # Approach: title/end cards are generated with ffmpeg drawtext (white on # dark 0x101010, 1280x720); the main segment is normalized to 1280x720@30 # (aspect-preserving fit + letterbox pad, audio dropped - the demo video is # video-only); the three segments are concatenated with the concat demuxer # followed by ONE re-encode. Stream-copy concat is brittle across # separately-generated mp4s (timestamp/param mismatches); the re-encode is # cheap at these lengths and guarantees a clean single-stream output. # The title font is auto-detected (DejaVuSans/NotoSans); the script fails # with a clear message if no TTF font exists. # # Requires: ffmpeg with libx264 + drawtext (libfreetype); split_screen.sh # next to this script for the two-dirs form. set -euo pipefail usage() { sed -n '3,31p' "$0" | sed 's/^# \{0,1\}//' } die() { echo "build_video.sh: ERROR: $*" >&2; exit 1; } # grep -q would SIGPIPE the ffmpeg producer under pipefail; capture first. ffmpeg_has() { # $1 = encoder/filter token local out out=$(ffmpeg -hide_banner "$2" 2>/dev/null) || true case "$out" in *" $1 "*) return 0 ;; *) return 1 ;; esac } command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg not found in PATH" ffmpeg_has libx264 -encoders || die "ffmpeg has no libx264 encoder" ffmpeg_has drawtext -filters || die "ffmpeg has no drawtext filter (needs libfreetype)" # --- locate a usable TTF font ------------------------------------------- FONT="" for f in \ /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf \ /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \ /usr/share/fonts/truetype/noto/NotoSans-Regular.ttf \ /usr/share/fonts/dejavu/DejaVuSans.ttf; do if [ -f "$f" ]; then FONT=$f; break; fi done if [ -z "$FONT" ] && command -v fc-match >/dev/null 2>&1; then f=$(fc-match -f '%{file}' sans 2>/dev/null || true) [ -n "$f" ] && [ -f "$f" ] && FONT=$f fi [ -n "$FONT" ] || die "no usable TTF font found (looked for DejaVuSans, NotoSans; \ install fonts-dejavu-core or set up fontconfig)" echo "build_video.sh: font=$FONT" >&2 # --- args ---------------------------------------------------------------- [ "$#" -ge 1 ] || { usage; exit 2; } TITLE=$1; shift [ -n "$TITLE" ] || die "title text must be non-empty" MAIN="" DIR_A="" DIR_B="" END="" if [ "$#" -ge 1 ] && [ -d "$1" ]; then DIR_A=$1 [ "$#" -ge 2 ] && [ -d "$2" ] || die "two-dirs form needs two directories: [end-text]" DIR_B=$2 END=${3:-} elif [ "$#" -ge 1 ]; then [ -f "$1" ] || die "main input not found: $1" MAIN=$1 END=${2:-} else die "missing main input (mp4 file or two frame dirs)" fi FPS=${FPS:-30} CARD_SECS=${CARD_SECS:-5} FONTSIZE=${FONTSIZE:-48} OUT=${OUT:-$PWD/open-iov-1.0.0-demo.mp4} HERE=$(cd "$(dirname "$0")" && pwd) TMP=$(mktemp -d "${TMPDIR:-/tmp}/build_video.XXXXXX") trap 'rm -rf "$TMP"' EXIT # --- cards (drawtext; text via textfile so ':' ',' '%' etc. are literal) -- make_card() { local text=$1 out=$2 tf="$TMP/card-$$.txt" printf '%s' "$text" > "$tf" ffmpeg -hide_banner -loglevel warning -y \ -f lavfi -i "color=c=0x101010:s=1280x720:r=$FPS:d=$CARD_SECS" \ -vf "drawtext=fontfile=$FONT:textfile=$tf:fontcolor=white:fontsize=$FONTSIZE:expansion=none:x=(w-text_w)/2:y=(h-text_h)/2" \ -c:v libx264 -preset medium -crf 20 -pix_fmt yuv420p \ "$out" } echo "build_video.sh: title card (${CARD_SECS}s)..." >&2 make_card "$TITLE" "$TMP/title.mp4" # --- main segment -------------------------------------------------------- if [ -n "$DIR_A" ]; then [ -x "$HERE/split_screen.sh" ] || die "split_screen.sh not found/executable next to build_video.sh" echo "build_video.sh: building split-screen main from $DIR_A + $DIR_B..." >&2 "$HERE/split_screen.sh" "$DIR_A" "$DIR_B" "$TMP/split.mp4" "$FPS" MAIN=$TMP/split.mp4 fi echo "build_video.sh: normalizing main segment to 1280x720@$FPS..." >&2 ffmpeg -hide_banner -loglevel warning -y -i "$MAIN" \ -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=$FPS" \ -an -c:v libx264 -preset medium -crf 20 -pix_fmt yuv420p \ "$TMP/main.mp4" # --- concat [title, main, end] ------------------------------------------ printf "file '%s'\n" "$TMP/title.mp4" "$TMP/main.mp4" > "$TMP/list.txt" if [ -n "$END" ]; then echo "build_video.sh: end card (${CARD_SECS}s)..." >&2 make_card "$END" "$TMP/end.mp4" printf "file '%s'\n" "$TMP/end.mp4" >> "$TMP/list.txt" fi echo "build_video.sh: concatenating..." >&2 ffmpeg -hide_banner -loglevel warning -y -f concat -safe 0 -i "$TMP/list.txt" \ -c:v libx264 -preset medium -crf 20 -pix_fmt yuv420p \ -r "$FPS" -movflags +faststart \ "$OUT" [ -s "$OUT" ] || die "ffmpeg produced no output: $OUT" if command -v ffprobe >/dev/null 2>&1; then ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height,r_frame_rate,duration \ -show_entries format=duration,size \ -of default=nw=1 "$OUT" | sed 's/^/build_video.sh: /' >&2 fi echo "VIDEO_DONE $OUT" >&2