aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDennis Fink2026-05-16 21:39:43 +0200
committerDennis Fink2026-05-16 21:39:43 +0200
commit2e7ee616e819735c4024d750d1e93cffa029ac05 (patch)
treeffdbdf522c97f359403fb9c0635cfb02885813ef
parenta53a593f962f8371ccc60a3a0a5edcaf4fc11390 (diff)
downloadtranscode.sh-2e7ee616e819735c4024d750d1e93cffa029ac05.tar.gz
transcode.sh-2e7ee616e819735c4024d750d1e93cffa029ac05.zip
test(testing): add generated media test files
Add a developer script that uses ffmpeg lavfi sources to create synthetic video test files without storing binary media in the repository. Allow callers to choose codec, resolution, duration, frame rate, source filter, pixel format, output path, and overwrite behavior from the command line. Expose the generator through just and ignore generated testfiles in git.
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rwxr-xr-xdevscripts/generate-testfile.sh697
-rw-r--r--justfile5
3 files changed, 703 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 1c0f7fd..1490f5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
dist/
+testfiles/
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,zsh,fish,vim,emacs,git
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,zsh,fish,vim,emacs,git
diff --git a/devscripts/generate-testfile.sh b/devscripts/generate-testfile.sh
new file mode 100755
index 0000000..e8bcbe8
--- /dev/null
+++ b/devscripts/generate-testfile.sh
@@ -0,0 +1,697 @@
+#!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+###############################################################################
+# DESCRIPTION
+# Test file generator for transcode.sh.
+#
+# This developer script creates small synthetic video files with ffmpeg. It
+# uses a lavfi source, such as smptebars, so test inputs can be generated
+# without keeping binary media files in the repository. Codec, resolution,
+# duration, frame rate, source filter, and output path can be selected from
+# the command line.
+#
+# NOTES
+# This script respects the NO_COLOR standard (https://no-color.org/).
+#
+# AUTHOR
+# Dennis Fink <me+coding@dennisfink.me>
+###############################################################################
+
+# TRACE enables bash execution tracing and is intended for debugging control
+# flow / expansions. DEBUG enables structured debug messages (see debug()) and
+# is intended for high-level state reporting.
+#
+# Recommended usage:
+# TRACE=1 devscripts/generate-testfile.sh ... # show every command bash executes
+# DEBUG=1 devscripts/generate-testfile.sh ... # show script-defined debug messages
+
+TRACE=${TRACE:-0}
+if [[ $TRACE -eq 1 ]]; then
+ set -o xtrace
+fi
+
+###############################################################################
+# SHELL SCRIPT HARDENING
+#
+# This script enables strict and predictable behavior for improved safety,
+# security, and debuggability. These measures help avoid common pitfalls
+# such as accidental globbing, unexpected alias expansion, silent failures,
+# or unintended word splitting.
+###############################################################################
+
+# Unalias everything to avoid unexpected alias expansion
+command unalias -a
+
+# Clear the shell command hash table to avoid stale command lookups
+hash -r
+
+# Set a safe IFS to prevent word-splitting vulnerabilities
+IFS=$'\n\t'
+
+# Set a secure default file creation mask (controls default permissions)
+umask 002
+
+# Ensure ERR traps are inherited by functions and subshells
+set -o errtrace
+# Treat use of undefined variables as an error and exit
+set -o nounset
+# Make pipelines fail if any command in the pipeline fails
+set -o pipefail
+
+###############################################################################
+# SCRIPT METADATA
+#
+# These variables describe the script and are used for the --version output
+# and for informational messages. They are marked readonly to prevent
+# accidental modification at runtime.
+###############################################################################
+
+readonly SCRIPTNAME=${0##*/}
+readonly DESCRIPTION="Test file generator for transcode.sh."
+readonly DATE_OF_CREATION=2026-05-16
+readonly DATE_OF_REVISION=2026-05-16
+readonly VERSION=0.0.1
+readonly AUTHOR="Dennis Fink <me+coding@dennisfink.me>"
+readonly LICENSE="BSD-3-Clause"
+
+###############################################################################
+# GLOBAL VARIABLES
+###############################################################################
+
+ALL_OFF=""
+BOLD=""
+RED=""
+GREEN=""
+YELLOW=""
+BLUE=""
+MAGENTA=""
+CYAN=""
+
+DEBUG=${DEBUG:-0}
+DRY_RUN=0
+QUIET=0
+VERBOSE=0
+OVERWRITE=0
+
+CODEC="libx264"
+RESOLUTION="1280x720"
+LENGTH="5"
+FRAME_RATE="30"
+FILTER="smptebars"
+PIX_FMT="yuv420p"
+FFMPEG_LOGLEVEL="warning"
+OUTPUT=""
+
+FFMPEG_COMMAND=()
+
+readonly OUTPUT_DIR="testfiles"
+
+###############################################################################
+# EXIT CODES
+###############################################################################
+
+EXIT_OK=0
+EXIT_RUNTIME_FAILURE=1
+EXIT_USAGE_ERROR=2
+EXIT_CONFIG_ERROR=3
+EXIT_MISSING_DEPENDENCY=127
+readonly EXIT_OK EXIT_RUNTIME_FAILURE EXIT_USAGE_ERROR EXIT_CONFIG_ERROR EXIT_MISSING_DEPENDENCY
+
+###############################################################################
+# OUTPUT HELPERS
+###############################################################################
+
+emit() {
+ local prefix="$1"
+ local color="$2"
+ shift 2
+
+ if [[ $# -eq 0 ]]; then
+ printf "%b%s%b\n" "${BOLD}${color}" "$prefix" "$ALL_OFF"
+ return
+ fi
+
+ local mesg="$1"
+ shift
+
+ local rest=""
+ if [[ $# -gt 0 ]]; then
+ printf -v rest ' %s' "$@"
+ rest="${rest# }"
+ fi
+
+ printf "%b%s%b %b%s%b" "${BOLD}${color}" "$prefix" "$ALL_OFF" "$BOLD" "$mesg" "$ALL_OFF"
+ if [[ -n "$rest" ]]; then
+ printf " %s" "$rest"
+ fi
+ printf "\n"
+}
+
+error() {
+ emit "==> ERROR:" "$RED" "$@" >&2
+}
+
+msg() {
+ if [[ $QUIET -eq 0 ]]; then
+ emit "==>" "$GREEN" "$@"
+ fi
+}
+
+warn() {
+ if [[ $QUIET -eq 0 ]]; then
+ emit "==>" "$YELLOW" "$@"
+ fi
+}
+
+verbose() {
+ if [[ $VERBOSE -eq 1 && $QUIET -eq 0 ]]; then
+ emit "==>" "$BLUE" "$@"
+ fi
+}
+
+debug() {
+ if [[ $DEBUG -eq 1 ]]; then
+ emit "==> DEBUG:" "$MAGENTA" "$@"
+ fi
+}
+
+###############################################################################
+# HELP AND VERSION OUTPUT
+###############################################################################
+
+print_help() {
+ printf "${BOLD}${MAGENTA}%s${ALL_OFF} - ${BOLD}${CYAN}%s${ALL_OFF} - ${BOLD}%s${ALL_OFF}
+
+${BOLD}${BLUE}Usage:${ALL_OFF} ${BOLD}${MAGENTA}%s${ALL_OFF} ${BOLD}${YELLOW}[OPTION]...${ALL_OFF} ${BOLD}${GREEN}[OUTPUT]${ALL_OFF}
+
+${BOLD}${BLUE}Generation options:${ALL_OFF}
+ ${BOLD}${YELLOW}-c, --codec${ALL_OFF} ${BOLD}${GREEN}CODEC${ALL_OFF} ffmpeg video codec to use (default: libx264)
+ ${BOLD}${YELLOW}-r, --resolution${ALL_OFF} ${BOLD}${GREEN}WIDTHxHEIGHT${ALL_OFF} output resolution (default: 1280x720)
+ ${BOLD}${YELLOW}-l, --length${ALL_OFF} ${BOLD}${GREEN}DURATION${ALL_OFF} test file length, in ffmpeg duration syntax (default: 5)
+ ${BOLD}${YELLOW}--rate${ALL_OFF} ${BOLD}${GREEN}FPS${ALL_OFF} frame rate for the generated source (default: 30)
+ ${BOLD}${YELLOW}--source${ALL_OFF} ${BOLD}${GREEN}SOURCE${ALL_OFF} lavfi video source (default: smptebars)
+ ${BOLD}${YELLOW}--pix-fmt${ALL_OFF} ${BOLD}${GREEN}FORMAT${ALL_OFF} output pixel format (default: yuv420p)
+ ${BOLD}${YELLOW}--ffmpeg-loglevel${ALL_OFF} ${BOLD}${GREEN}LEVEL${ALL_OFF} ffmpeg log level (default: warning)
+ ${BOLD}${YELLOW}-o, --output${ALL_OFF} ${BOLD}${GREEN}FILE${ALL_OFF} write to FILE instead of generated testfiles/ name
+ ${BOLD}${YELLOW}-f, --force${ALL_OFF} overwrite an existing output file
+ ${BOLD}${YELLOW}-n, --dry-run${ALL_OFF} show the ffmpeg command without creating a file
+
+${BOLD}${BLUE}Output options:${ALL_OFF}
+ ${BOLD}${YELLOW}-q, --quiet${ALL_OFF} suppress informational output
+ ${BOLD}${YELLOW}-v, --verbose${ALL_OFF} explain what is being done
+ ${BOLD}${YELLOW}--color${ALL_OFF} force colored output
+ ${BOLD}${YELLOW}--no-color${ALL_OFF} disable colored output
+
+${BOLD}${BLUE}General options:${ALL_OFF}
+ ${BOLD}${YELLOW}-h, --help${ALL_OFF} display this help and exit
+ ${BOLD}${YELLOW}--version${ALL_OFF} output version information and exit
+
+${BOLD}${BLUE}Examples:${ALL_OFF}
+ %s --codec libx264 --resolution 1280x720 --length 5
+ %s --codec libx265 --resolution 1920x1080 --length 00:00:10
+ %s --codec libvpx-vp9 --resolution 640x360 --length 3 --output testfiles/vp9.webm
+ %s --source testsrc2 --codec mpeg4 --resolution 320x240 --length 2
+" \
+ "$SCRIPTNAME" "$VERSION" "$DESCRIPTION" \
+ "$SCRIPTNAME" \
+ "$SCRIPTNAME" "$SCRIPTNAME" "$SCRIPTNAME" "$SCRIPTNAME"
+}
+
+print_version() {
+ printf "${RED}${BOLD}Scriptname:${ALL_OFF} %s
+${GREEN}${BOLD}Version:${ALL_OFF} %s
+${YELLOW}${BOLD}Description:${ALL_OFF} %s
+${BLUE}${BOLD}Author:${ALL_OFF} %s
+${MAGENTA}${BOLD}Date of creation:${ALL_OFF} %s
+${CYAN}${BOLD}Date of revision:${ALL_OFF} %s
+${RED}${BOLD}License:${ALL_OFF} %s
+
+Copyright (c) 2026 Dennis Fink <me+coding@dennisfink.me>.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" \
+ "$SCRIPTNAME" "$VERSION" "$DESCRIPTION" "$AUTHOR" "$DATE_OF_CREATION" \
+ "$DATE_OF_REVISION" "$LICENSE"
+}
+
+###############################################################################
+# TERMINAL COLOR HANDLING
+#
+# Colors and style escape sequences are configured dynamically through the
+# setup_colors function. Runtime color detection happens in MAIN EXECUTION so
+# all function definitions stay together before the executable flow.
+#
+# This script respects the NO_COLOR standard (https://no-color.org/). If the
+# environment variable NO_COLOR is set (to any value), all color output is
+# disabled. If FORCE_COLOR is set, colors are always enabled.
+###############################################################################
+
+setup_colors() {
+ if [[ "$ENABLE_COLOR" -eq 1 ]]; then
+ if tput setaf 0 >/dev/null 2>&1; then
+ debug "Setting colors via tput"
+ ALL_OFF="$(tput sgr0)"
+ BOLD="$(tput bold)"
+ RED="$(tput setaf 1)"
+ GREEN="$(tput setaf 2)"
+ YELLOW="$(tput setaf 3)"
+ BLUE="$(tput setaf 4)"
+ MAGENTA="$(tput setaf 5)"
+ CYAN="$(tput setaf 6)"
+ else
+ # Hardcoded ANSI fallback: colors only (no bold embedded), matching the
+ # tput path where BOLD and color variables are kept separate.
+ debug "Using hardcoded color ANSI escape sequences"
+ ALL_OFF="\e[0m"
+ BOLD="\e[1m"
+ RED="\e[31m"
+ GREEN="\e[32m"
+ YELLOW="\e[33m"
+ BLUE="\e[34m"
+ MAGENTA="\e[35m"
+ CYAN="\e[36m"
+ fi
+ else
+ ALL_OFF=""
+ BOLD=""
+ RED=""
+ GREEN=""
+ YELLOW=""
+ BLUE=""
+ MAGENTA=""
+ CYAN=""
+ fi
+}
+
+###############################################################################
+# GENERATION HELPERS
+###############################################################################
+
+require_command() {
+ local command_name="$1"
+
+ command -v "$command_name" >/dev/null 2>&1 || {
+ error "$command_name not found."
+ exit $EXIT_MISSING_DEPENDENCY
+ }
+}
+
+validate_resolution() {
+ if [[ ! "$RESOLUTION" =~ ^[1-9][0-9]*x[1-9][0-9]*$ ]]; then
+ error "Resolution must use WIDTHxHEIGHT format:" "$RESOLUTION"
+ exit $EXIT_USAGE_ERROR
+ fi
+}
+
+validate_length() {
+ if [[ "$LENGTH" =~ ^- ]]; then
+ error "Length must be a positive ffmpeg duration:" "$LENGTH"
+ exit $EXIT_USAGE_ERROR
+ fi
+}
+
+validate_frame_rate() {
+ if [[ ! "$FRAME_RATE" =~ ^[1-9][0-9]*([.][0-9]+)?$ ]]; then
+ error "Frame rate must be a positive number:" "$FRAME_RATE"
+ exit $EXIT_USAGE_ERROR
+ fi
+}
+
+extension_for_codec() {
+ case "$CODEC" in
+ libvpx | libvpx-vp9)
+ printf "webm"
+ ;;
+ libaom-av1 | librav1e | libsvtav1)
+ printf "mkv"
+ ;;
+ *)
+ printf "mp4"
+ ;;
+ esac
+}
+
+sanitized_name_part() {
+ local value="$1"
+ value="${value//\//-}"
+ value="${value//:/-}"
+ value="${value// /-}"
+ printf "%s" "$value"
+}
+
+default_output_path() {
+ local extension codec_part length_part
+ extension="$(extension_for_codec)"
+ codec_part="$(sanitized_name_part "$CODEC")"
+ length_part="$(sanitized_name_part "$LENGTH")"
+
+ printf "%s/%s-%s-%ss.%s" "$OUTPUT_DIR" "$codec_part" "$RESOLUTION" "$length_part" "$extension"
+}
+
+print_command() {
+ local quoted_command
+ printf -v quoted_command '%q ' "${FFMPEG_COMMAND[@]}"
+ printf "%s\n" "${quoted_command% }"
+}
+
+generate_test_file() {
+ local output_file="$1"
+
+ if [[ -e "$output_file" && $OVERWRITE -eq 0 ]]; then
+ error "Output file already exists; use --force to overwrite:" "$output_file"
+ exit $EXIT_RUNTIME_FAILURE
+ fi
+
+ local overwrite_flag="-n"
+
+ if [[ $OVERWRITE -eq 1 ]]; then
+ overwrite_flag="-y"
+ fi
+
+ FFMPEG_COMMAND=(
+ ffmpeg
+ -hide_banner
+ -loglevel "$FFMPEG_LOGLEVEL"
+ "$overwrite_flag"
+ -f lavfi
+ -i "${FILTER}=size=${RESOLUTION}:rate=${FRAME_RATE}"
+ -t "$LENGTH"
+ -an
+ -c:v "$CODEC"
+ -pix_fmt "$PIX_FMT"
+ "$output_file"
+ )
+
+ if [[ $DRY_RUN -eq 1 ]]; then
+ msg "DRY RUN:" "Would generate test file: $output_file"
+ print_command
+ return $EXIT_OK
+ fi
+
+ mkdir -p -- "$(dirname -- "$output_file")"
+
+ msg "Generating test file:" "$output_file"
+ verbose "Codec:" "$CODEC"
+ verbose "Resolution:" "$RESOLUTION"
+ verbose "Length:" "$LENGTH"
+ verbose "Filter:" "$FILTER"
+ verbose "Command:"
+ if [[ $VERBOSE -eq 1 && $QUIET -eq 0 ]]; then
+ print_command
+ fi
+
+ "${FFMPEG_COMMAND[@]}"
+ msg "Created:" "$output_file"
+}
+
+###############################################################################
+# MAIN EXECUTION
+###############################################################################
+
+# NOTE: This block calls debug() and reads $DEBUG. Both must be defined before
+# this point. Do not move this block above the GLOBAL VARIABLES or FUNCTIONS
+# sections without adjusting those dependencies.
+if [[ -n "${NO_COLOR:-}" ]]; then
+ debug "Colors are disabled via NO_COLOR"
+ ENABLE_COLOR=0
+elif [[ -n "${FORCE_COLOR:-}" ]]; then
+ debug "Colors are forced via FORCE_COLOR"
+ ENABLE_COLOR=1
+elif [[ -t 1 ]]; then
+ debug "Colors are enabled via interactive terminal"
+ ENABLE_COLOR=1
+else
+ debug "Colors are disabled"
+ ENABLE_COLOR=0
+fi
+setup_colors
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -h | --help | -\?)
+ print_help
+ exit $EXIT_OK
+ ;;
+ --version)
+ print_version
+ exit $EXIT_OK
+ ;;
+ -q | --quiet)
+ QUIET=1
+ shift
+ ;;
+ -v | --verbose)
+ VERBOSE=1
+ shift
+ ;;
+ --color)
+ ENABLE_COLOR=1
+ setup_colors
+ shift
+ ;;
+ --no-color)
+ ENABLE_COLOR=0
+ setup_colors
+ shift
+ ;;
+ -n | --dry-run)
+ DRY_RUN=1
+ shift
+ ;;
+ -f | --force)
+ OVERWRITE=1
+ shift
+ ;;
+ -c | --codec)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ CODEC="$2"
+ shift 2
+ ;;
+ --codec=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --codec must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ CODEC="${1#*=}"
+ shift
+ ;;
+ -r | --resolution)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ RESOLUTION="$2"
+ shift 2
+ ;;
+ --resolution=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --resolution must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ RESOLUTION="${1#*=}"
+ shift
+ ;;
+ -l | --length | --duration)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ LENGTH="$2"
+ shift 2
+ ;;
+ --length=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --length must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ LENGTH="${1#*=}"
+ shift
+ ;;
+ --duration=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --duration must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ LENGTH="${1#*=}"
+ shift
+ ;;
+ --rate)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ FRAME_RATE="$2"
+ shift 2
+ ;;
+ --rate=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --rate must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ FRAME_RATE="${1#*=}"
+ shift
+ ;;
+ --source)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ FILTER="$2"
+ shift 2
+ ;;
+ --source=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --source must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ FILTER="${1#*=}"
+ shift
+ ;;
+ --pix-fmt)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ PIX_FMT="$2"
+ shift 2
+ ;;
+ --pix-fmt=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --pix-fmt must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ PIX_FMT="${1#*=}"
+ shift
+ ;;
+ --ffmpeg-loglevel)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ FFMPEG_LOGLEVEL="$2"
+ shift 2
+ ;;
+ --ffmpeg-loglevel=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --ffmpeg-loglevel must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ FFMPEG_LOGLEVEL="${1#*=}"
+ shift
+ ;;
+ -o | --output)
+ if [[ $# -lt 2 ]]; then
+ error "Missing value for:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ if [[ ! -n "$2" ]]; then
+ error "Value for $1 must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ OUTPUT="$2"
+ shift 2
+ ;;
+ --output=*)
+ if [[ ! -n "${1#*=}" ]]; then
+ error "Value for --output must not be empty"
+ exit $EXIT_USAGE_ERROR
+ fi
+ OUTPUT="${1#*=}"
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ error "Unknown option:" "$1"
+ exit $EXIT_USAGE_ERROR
+ ;;
+ *)
+ if [[ -n "$OUTPUT" ]]; then
+ error "Unexpected extra argument:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ OUTPUT="$1"
+ shift
+ ;;
+ esac
+done
+
+if [[ $# -gt 0 ]]; then
+ if [[ -n "$OUTPUT" ]]; then
+ error "Unexpected extra argument:" "$1"
+ exit $EXIT_USAGE_ERROR
+ fi
+ OUTPUT="$1"
+ shift
+fi
+
+require_command ffmpeg
+validate_resolution
+validate_length
+validate_frame_rate
+
+if [[ -z "$OUTPUT" ]]; then
+ OUTPUT="$(default_output_path)"
+fi
+
+generate_test_file "$OUTPUT"
+exit $EXIT_OK
diff --git a/justfile b/justfile
index 7efa940..27bb985 100644
--- a/justfile
+++ b/justfile
@@ -9,5 +9,10 @@ prepare version:
release version: (prepare version)
./devscripts/release.sh release "{{version}}"
+# Generate a synthetic video test file with ffmpeg.
+testfile *args:
+ ./devscripts/generate-testfile.sh {{args}}
+
clean:
rm -f dist/transcode-*.tar.gz
+ rm -f testfiles/*