#!/usr/bin/env bash
# Make an MP4/H.264 file.
#
# Copyright (c) 2006-2023 by Reto Kromer
#
# This Bash script is released under a 3-Clause BSD License and is provided
# "as is" without warranty or support of any kind.
# initialise constants
VERSION='2023-01-28'
SCRIPT_NAME="$(basename "$0")"
CONFIG_FILE="${HOME}/.config/AVpres/Bash_AVpres/${SCRIPT_NAME}.txt"
RED='\033[1;31m'
BLUE='\033[1;34m'
NC='\033[0m'
# load configuration file if any and initialise default values
[[ -f "${CONFIG_FILE}" ]] && . "${CONFIG_FILE}"
ffmpeg_bin="${ffmpeg_bin:-$(which ffmpeg)}"
ffprobe_bin="${ffprobe_bin:-$(which ffprobe)}"
ff_glo_opt="${ff_glo_opt:--y}"
f="${f:-image2}"
framerate="${framerate:-24}"
filter_v="${filter_v}"
c_v="${c_v:-libx264}"
preset="${preset:-veryslow}"
crf="${crf:-18}"
qp="${qp:-18}"
pix_fmt="${pix_fmt:-yuv420p}"
filter_a="${filter_a}"
c_a="${c_a:-aac}"
movflags="${movflags:-+faststart}"
suffix="${suffix:-_H264}"
extension="${extension:-mp4}"
# initialise other default values
regex_c_v='^\(libx264|libx264rgb|h264\$)'
regex_pix_fmt='^\(yuv420p|yuv420p10le\$)'
# initialise variables
unset input_path
unset output_path
unset ff_in_opt
unset ff_out_opt
unset first_file
unset start_number
unset input_file_regex
# get date-and-time stamp
date_time() {
TZ='UTC' date +'[%F %T %Z]'
}
# check that Bash 3.2 or later is running
if ! printf '%s\n%s\n' "$(bash -c 'echo ${BASH_VERSION}')" "3.2" | sort -rVC; then
echo -e "${BLUE}Warning: This 'bash' binary is very old. Tested for version 3.2 or later.${NC}"
fi
# start log file
[[ -d '/tmp/AVpres' ]] || mkdir -p '/tmp/AVpres'
LOG_FILE="$(mktemp "/tmp/AVpres/${SCRIPT_NAME}.XXXXXXXXXX")"
echo "$(date_time) ${SCRIPT_NAME} ${VERSION}" > "${LOG_FILE}"
echo "$(date_time) $0 $*" >> "${LOG_FILE}"
echo "$(date_time) START" >> "${LOG_FILE}"
# print an error message and exit with status 1
abort() {
echo -e "${RED}${1:-An unknown error occurred.\a}${NC}"
echo "$(date_time) ${1:-An unknown error occurred.}" >> "${LOG_FILE}"
echo "$(date_time) END" >> "${LOG_FILE}"
exit 1
}
# print a minimal help message and exit with status 0
print_prompt() {
echo "$(date_time) print prompt" >> "${LOG_FILE}"
cat << EOF
Help:
${SCRIPT_NAME} -h
EOF
echo "$(date_time) END" >> "${LOG_FILE}"
exit 0
}
# print the help message and exit with status 0
print_help() {
echo "$(date_time) print help" >> "${LOG_FILE}"
cat << EOF
Usage:
${SCRIPT_NAME} -i -o
${SCRIPT_NAME} -h | -x
Options:
-i input file (stream based) or folder (single-image based)
-o output file
-h this help
-x advanced options with their default arguments
Dependencies:
ffmpeg and ffprobe
See also:
man ${SCRIPT_NAME}
https://avpres.net/Bash_AVpres/
About:
Abstract: Make an MP4/H.264 file
Version: ${VERSION}
EOF
echo "$(date_time) END" >> "${LOG_FILE}"
exit 0
}
# display advanced options with their default arguments and exit with status 0
print_options() {
echo "$(date_time) print options" >> "${LOG_FILE}"
cat << EOF
FFmpeg default binaries:
--ffmpeg='${ffmpeg_bin}'
--ffprobe='${ffprobe_bin}'
Global ffmpeg option:
--ff_glo_opt='${ff_glo_opt}'
Input file default parameters (used only for single-image based input files):
--f='${f}'
--framerate='${framerate}'
Video codec default parameters:
--filter_v='${filter_v}'
--c_v='${c_v}'
--preset='${preset}'
--crf='${crf}'
--qp='${qp}'
--pix_fmt='${pix_fmt}'
Audio codec default parameters (only used when audio stream is present):
--filter_a='${filter_a}'
--c_a='${c_a}'
Output file default parameters:
--movflags='${movflags}'
--suffix='${suffix}'
--extension='${extension}'
EOF
echo "$(date_time) END" >> "${LOG_FILE}"
exit 0
}
# check if external ffmpeg and ffprove commands are running
check_ffmpeg() {
if command -v "${ffmpeg_bin}" &> /dev/null; then
echo "$(date_time) '${ffmpeg_bin}' found" >> "${LOG_FILE}"
else
abort "Error: 'ffmpeg' binary not found."
fi
if command -v "${ffprobe_bin}" &> /dev/null; then
echo "$(date_time) '${ffprobe_bin}' found" >> "${LOG_FILE}"
else
abort "Error: 'ffprobe' binary not found."
fi
}
# verify if the input file is valid
verify_input() {
local in_file="${1}"
echo "$(date_time) verify input" >> "${LOG_FILE}"
if [[ ! "${in_file}" || "${in_file}" == "_." ]]; then
abort "Error: No input file or folder provided via '-i' or '--input'."
elif [[ ! -f "${in_file}" ]]; then
abort "Error: '${input_path}' is not a valid input."
elif "${ffprobe_bin}" "${in_file}" 2>&1 | grep "Invalid data found" > /dev/null; then
abort "Error: '$(basename ${input_path})' is not an AV file."
fi
return 0
}
# verify if the output file is valid
verify_output() {
local out_file="${1}"
echo "$(date_time) verify output" >> "${LOG_FILE}"
if [[ ! "${out_file}" ]]; then
abort "Error: No output file provided via '-o' or '--output'."
elif ! : > "${out_file}.txt" &> /dev/null; then
abort "Error: Cannot create an output file '${out_file}'."
else
rm "${out_file}.txt"
fi
if [[ "${input_path}" == "${output_path}" ]]; then
abort "Error: '${input_path}' cannot be both input and output."
fi
return 0
}
# prepare the parameters for the ffmpeg command
prepare_command() {
if [[ -d "${input_path}" ]]; then
if [[ "${start_number}" == $(basename "${first_file%.*}") ]]; then
input_file_regex="%0${#start_number}d.${first_file##*.}"
else
input_file_regex="${first_file%_*}_%0${#start_number}d.${first_file##*.}"
fi
ff_in_opt+=" -f ${f} -framerate ${framerate}"
if (( $(echo "${start_number}" | bc -l) > 1 )); then
ff_in_opt+=" -start_number ${start_number}"
fi
ff_out_opt+=" -an"
elif [[ $("${ffprobe_bin}" "${input_path}" -show_streams -select_streams a -loglevel quiet) == '' ]]; then
ff_out_opt+=" -an"
else
if [[ "${filter_a}" ]]; then
ff_out_opt+=" -filter:a ${filter_a}"
fi
ff_out_opt+=" -c:a ${c_a}"
fi
if [[ "${filter_v}" ]]; then
ff_out_opt+=" -filter:v ${filter_v}"
fi
ff_out_opt+=" -c:v ${c_v} -preset ${preset} -pix_fmt ${pix_fmt}"
if [[ "${crf}" != '#' ]]; then
ff_out_opt+=" -crf ${crf}"
elif [[ "${qp}" ]]; then
ff_out_opt+=" -qp ${qp}"
fi
if [[ "${movflags}" != '#' ]]; then
ff_out_opt+=" -movflags ${movflags}"
fi
if [[ -d "${input_path}" ]]; then
input_file="${input_path}/${input_file_regex}"
else
input_file="${input_path}"
fi
if [[ "${suffix}" == '#' ]]; then
if [[ "${output_path}" == "${output_path##*.}" ]]; then
output_file="${output_path%.*}.${extension}"
else
output_file="${output_path}"
fi
else
if [[ "${output_path}" == "${output_path##*.}" ]]; then
output_file="${output_path%.*}${suffix}.${extension}"
else
output_file="${output_path%.*}${suffix}.${output_path##*.}"
fi
fi
}
# transcode to MP4/H.264
transcode() {
local in_file="${1}"
local out_file="${2}"
echo "$(date_time) Generating '${out_file}'" >> "${LOG_FILE}"
echo -e "${BLUE}Please wait while generating '${out_file}'...${NC}"
[[ "${ff_glo_opt}" == '#' ]] && ff_glo_opt=''
echo "$(date_time) ${ffmpeg_bin} ${ff_glo_opt} ${ff_in_opt} -i ${in_file} \
${ff_out_opt} ${out_file}" >> "${LOG_FILE}"
if ! "${ffmpeg_bin}" ${ff_glo_opt} ${ff_in_opt} -i "${in_file}" \
${ff_out_opt} "${out_file}" >> "${LOG_FILE}" 2>&1
then
abort "Fatal error, see '${LOG_FILE}'."
fi
echo -e "${BLUE}... done.${NC}"
return 0
}
# parse and process provided input
(( $# == 0 )) && print_prompt
while getopts ":i:o:-:hx" opt; do
case "${opt}" in
i) if [[ "${OPTARG:0:1}" == '-' ]]; then
abort "Error: The option '-i' requires an argument."
else
input_path="${OPTARG}"
fi ;;
o) if [[ "${OPTARG:0:1}" == '-' ]]; then
abort "Error: The option '-o' requires an argument."
else
output_path="${OPTARG}"
fi ;;
-) case "${OPTARG}" in
input=?*) input_path="${OPTARG#*=}" ;;
output=?*) output_path="${OPTARG#*=}" ;;
ffmpeg=?*) ffmpeg_bin="${OPTARG#*=}" ;;
ffprobe=?*) ffprobe_bin="${OPTARG#*=}" ;;
ff_glo_opt=?*) ff_glo_opt="${OPTARG#*=}" ;;
f=?*) f="${OPTARG#*=}" ;;
framerate=?*) framerate="${OPTARG#*=}" ;;
filter_v=?*) filter_v="${OPTARG#*=}" ;;
c_v=?*) c_v="${OPTARG#*=}" ;;
preset=?*) preset="${OPTARG#*=}" ;;
crf=?*) crf="${OPTARG#*=}" ;;
qp=?*) qp="${OPTARG#*=}" ;;
pix_fmt=?*) fix_fmt="${OPTARG#*=}" ;;
filter_a=?*) filter_a="${OPTARG#*=}" ;;
c_a=?*) c_a="${OPTARG#*=}" ;;
movflags=?*) movflags="${OPTARG#*=}" ;;
suffix=?*) suffix="${OPTARG#*=}" ;;
extension=?*) extension="${OPTARG#*=}" ;;
help) print_help ;;
options) print_options ;;
*) abort "Error: The option '--${OPTARG}' is not valid." ;;
esac ;;
h) print_help ;;
x) print_options ;;
:) abort "Error: The option '-${OPTARG}' requires an argument." ;;
*) abort "Error: The option '-${OPTARG}' is not valid." ;;
esac
done
# check if FFmpeg is running
check_ffmpeg
# check if provided input file is valid
if [[ -d "${input_path}" ]]; then
first_file=$(ls "${input_path}" | sort | head -n 1)
start_number=$(echo "${first_file}" | grep -oE '_[0-9]+\.' | grep -oE '[0-9]+')
if [[ ! "${start_number}" ]]; then
start_number=$(echo "${first_file}" | grep -oE '[0-9]+')
fi
if [[ "${start_number}" == $(basename "${first_file%.*}") ]]; then
verify_input "${input_path}/${start_number}.${first_file##*.}"
else
verify_input "${input_path}/${first_file%_*}_${start_number}.${first_file##*.}"
fi
else
verify_input "${input_path}"
fi
# check if provided output file is valid
verify_output "${output_path}"
# transcode from input file to output file
prepare_command
transcode "${input_file}" "${output_file}"
# end log file
echo "$(date_time) END" >> "${LOG_FILE}"