Files
ffmpeg-builder/lib/encode.sh
2025-09-26 12:22:33 -05:00

430 lines
9.3 KiB
Bash

#!/usr/bin/env bash
unmap_streams() {
local file="$1"
local unmapFilter='bin_data|jpeg|png'
local unmap=()
local streamsStr
streamsStr="$(get_num_streams "${file}")" || return 1
mapfile -t streams <<<"${streamsStr}" || return 1
for stream in "${streams[@]}"; do
if [[ "$(get_stream_codec "${file}" "${stream}")" =~ ${unmapFilter} ]]; then
unmap+=("-map" "-0:${stream}")
fi
done
echo "${unmap[@]}"
}
set_audio_bitrate() {
local file="$1"
local bitrate=()
for stream in $(get_num_audio_streams "${file}"); do
local numChannels codec
numChannels="$(get_num_audio_channels "${file}" "${stream}")" || return 1
local channelBitrate=$((numChannels * 64))
codec="$(get_stream_codec "${file}" "${stream}")" || return 1
if [[ ${codec} == 'opus' ]]; then
bitrate+=(
"-c:${stream}"
"copy"
)
else
bitrate+=(
"-filter:${stream}"
"aformat=channel_layouts=7.1|5.1|stereo|mono"
"-c:${stream}"
"libopus"
"-b:${stream}"
"${channelBitrate}k"
)
fi
done
echo "${bitrate[@]}"
}
convert_subs() {
local file="$1"
local convertCodec='eia_608'
local convert=()
for stream in $(get_num_streams "${file}"); do
if [[ "$(get_stream_codec "${file}" "${stream}")" == "${convertCodec}" ]]; then
convert+=("-c:${stream}" "srt")
fi
done
echo "${convert[@]}"
}
encode_version() (
cd "${REPO_DIR}" || exit 1
echo "encode=$(git rev-parse --short HEAD)"
)
ffmpeg_version() {
local output="$(ffmpeg 2>&1)"
local commit=''
local version=''
while read -r line; do
if line_contains "${line}" 'ffmpeg version'; then
read -r _ _ commit _ <<<"${line}"
fi
if line_contains "${line}" 'ffmpeg='; then
IFS='=' read -r _ version <<<"${line}"
fi
done <<<"${output}"
echo "ffmpeg=${version}-${commit}"
}
video_enc_version() {
local output="$(ffmpeg -hide_banner 2>&1)"
while read -r line; do
if line_contains "${line}" 'libsvtav1_psy='; then
echo "${line}"
break
elif line_contains "${line}" 'libsvtav1='; then
echo "${line}"
break
fi
done <<<"${output}"
}
audio_enc_version() {
local output="$(ffmpeg -hide_banner 2>&1)"
while read -r line; do
if line_contains "${line}" 'libopus='; then
echo "${line}"
break
fi
done <<<"${output}"
}
encode_usage() {
echo "encode -i input [options] output"
echo -e "\t[-P NUM] set preset (default: ${PRESET})"
echo -e "\t[-C NUM] set CRF (default: ${CRF})"
echo -e "\t[-g NUM] set film grain for encode"
echo -e "\t[-p] print the command instead of executing it (default: ${PRINT_OUT})"
echo -e "\t[-c] use cropdetect (default: ${CROP})"
echo -e "\t[-d] disable dolby vision (default: ${DISABLE_DV})"
echo -e "\t[-v] Print relevant version info"
echo -e "\t[-s] use same container as input, default is mkv"
echo -e "\n\t[output] if unset, defaults to ${HOME}/"
echo -e "\n\t[-u] update script (git pull at ${REPO_DIR})"
echo -e "\t[-I] system install at ${ENCODE_INSTALL_PATH}"
echo -e "\t[-U] uninstall from ${ENCODE_INSTALL_PATH}"
return 0
}
encode_update() {
cd "${REPO_DIR}" || return 1
git pull
}
set_encode_opts() {
local opts='vi:pcsdg:P:C:uIU'
local numOpts=${#opts}
# default values
PRESET=3
CRF=25
GRAIN=""
CROP=false
PRINT_OUT=false
DISABLE_DV=false
ENCODE_INSTALL_PATH='/usr/local/bin/encode'
local sameContainer="false"
# only using -I/U
local minOpt=1
# using all + output name
local maxOpt=$((numOpts + 1))
test $# -lt ${minOpt} && echo_fail "not enough arguments" && encode_usage && return 1
test $# -gt ${maxOpt} && echo_fail "too many arguments" && encode_usage && return 1
local optsUsed=0
local OPTARG OPTIND
while getopts "${opts}" flag; do
case "${flag}" in
u)
encode_update
exit $?
;;
I)
echo_warn "attempting install"
sudo ln -sf "${SCRIPT_DIR}/encode.sh" \
"${ENCODE_INSTALL_PATH}" || return 1
echo_pass "succesfull install"
exit 0
;;
U)
echo_warn "attempting uninstall"
sudo rm "${ENCODE_INSTALL_PATH}" || return 1
echo_pass "succesfull uninstall"
exit 0
;;
v)
encode_version
ffmpeg_version
video_enc_version
audio_enc_version
exit 0
;;
i)
if [[ $# -lt 2 ]]; then
echo_fail "wrong arguments given"
encode_usage
return 1
fi
INPUT="${OPTARG}"
optsUsed=$((optsUsed + 2))
;;
p)
PRINT_OUT=true
optsUsed=$((optsUsed + 1))
;;
c)
CROP=true
optsUsed=$((optsUsed + 1))
;;
d)
DISABLE_DV='enable'
optsUsed=$((optsUsed + 1))
;;
s)
sameContainer=true
optsUsed=$((optsUsed + 1))
;;
g)
if ! is_positive_integer "${OPTARG}"; then
encode_usage
return 1
fi
GRAIN="film-grain=${OPTARG}:film-grain-denoise=1:adaptive-film-grain=1:"
optsUsed=$((optsUsed + 2))
;;
P)
if ! is_positive_integer "${OPTARG}"; then
encode_usage
return 1
fi
PRESET="${OPTARG}"
optsUsed=$((optsUsed + 2))
;;
C)
if ! is_positive_integer "${OPTARG}" || test ${OPTARG} -gt 63; then
echo_fail "${OPTARG} is not a valid CRF value (0-63)"
usage
exit 1
fi
CRF="${OPTARG}"
OPTS_USED=$((OPTS_USED + 2))
;;
*)
echo_fail "wrong flags given"
encode_usage
return 1
;;
esac
done
# allow optional output filename
if [[ $(($# - optsUsed)) == 1 ]]; then
OUTPUT="${*: -1}"
else
local basename="$(bash_basename "${INPUT}")"
OUTPUT="${HOME}/av1-${basename}"
fi
# use same container for output
if [[ $sameContainer == "true" ]]; then
local fileFormat
fileFormat="$(get_file_format "${INPUT}")" || return 1
FILE_EXT=''
if [[ ${fileFormat} == 'MPEG-4' ]]; then
FILE_EXT='mp4'
elif [[ ${fileFormat} == 'Matroska' ]]; then
FILE_EXT='mkv'
else
echo "unrecognized input format"
return 1
fi
else
FILE_EXT="mkv"
fi
OUTPUT="${OUTPUT%.*}"
OUTPUT+=".${FILE_EXT}"
if [[ ! -f ${INPUT} ]]; then
echo "${INPUT} does not exist"
efg_usage
return 1
fi
echo
echo_info "INPUT: ${INPUT}"
echo_info "GRAIN: ${GRAIN}"
echo_info "OUTPUT: ${OUTPUT}"
echo
}
# shellcheck disable=SC2034
# shellcheck disable=SC2155
# shellcheck disable=SC2016
gen_encode_script() {
test -d "${TMP_DIR}" || mkdir -p "${TMP_DIR}"
local genScript="${TMP_DIR}/$(bash_basename "${OUTPUT}").sh"
# single string params
local params=(
INPUT
OUTPUT
PRESET
CRF
crop
videoEncoder
ffmpegVersion
videoEncVersion
audioEncVersion
svtAv1Params
svtAv1ParamsMetadata
)
local crop=''
if [[ $CROP == "true" ]]; then
crop="$(get_crop "${INPUT}")" || return 1
fi
local videoEncoder='libsvtav1'
local ffmpegVersion="$(ffmpeg_version)"
local videoEncVersion="$(video_enc_version)"
local audioEncVersion="$(audio_enc_version)"
svtAv1ParamsArr=(
"tune=0"
"complex-hvs=1"
"spy-rd=1"
"psy-rd=1"
"sharpness=3"
"enable-overlays=1"
"scd=1"
"fast-decode=1"
"enable-variance-boost=1"
"enable-qm=1"
"qm-min=4"
"qm-max=15"
)
IFS=':'
local svtAv1Params="${GRAIN}${svtAv1ParamsArr[*]}"
unset IFS
local svtAv1ParamsMetadata='svtav1_params=${svtAv1Params}'
# arrays
local arrays=(
unmap
audioBitrate
videoParams
videoParamsMetadata
metadata
convertSubs
ffmpegParams
)
local videoParams=(
"-crf" '${CRF}' "-preset" '${PRESET}' "-g" "240"
)
local ffmpegParams=(
'-i' '${INPUT}'
'-y' '-map' '0'
'-c:s' 'copy'
)
# these values may be empty
local unmapStr audioBitrateStr convertSubsStr
unmapStr="$(unmap_streams "${INPUT}")" || return 1
audioBitrateStr="$(set_audio_bitrate "${INPUT}")" || return 1
convertSubsStr="$(convert_subs "${INPUT}")" || return 1
unmap=(${unmapStr})
if [[ ${unmap[*]} != '' ]]; then
ffmpegParams+=('${unmap[@]}')
fi
audioBitrate=(${audioBitrateStr})
if [[ ${audioBitrate[*]} != '' ]]; then
ffmpegParams+=('${audioBitrate[@]}')
fi
convertSubs=(${convertSubsStr})
if [[ ${convertSubs[*]} != '' ]]; then
ffmpegParams+=('${convertSubs[@]}')
fi
if [[ ${crop} != '' ]]; then
ffmpegParams+=('-vf' '${crop}')
fi
ffmpegParams+=(
'-pix_fmt' 'yuv420p10le' '-c:V' '${videoEncoder}' '${videoParams[@]}'
'-svtav1-params' '${svtAv1Params}'
'${metadata[@]}'
)
local videoParamsMetadata='video_params=${videoParams[*]}'
local metadata=(
'-metadata' '${ffmpegVersion}'
'-metadata' '${videoEncVersion}'
'-metadata' '${audioEncVersion}'
'-metadata' '${svtAv1ParamsMetadata}'
'-metadata' '${videoParamsMetadata[@]}'
)
{
echo '#!/usr/bin/env bash'
echo
# add normal params
for param in "${params[@]}"; do
declare -n value="${param}"
if [[ ${value} != '' ]]; then
echo "${param}=\"${value[*]}\""
fi
done
for arrName in "${arrays[@]}"; do
declare -n arr="${arrName}"
if [[ -v arr ]]; then
echo "${arrName}=("
printf '\t"%s"\n' "${arr[@]}"
echo ')'
fi
done
# actually do ffmpeg commmand
echo
if [[ ${DISABLE_DV} == false ]]; then
echo 'ffmpeg "${ffmpegParams[@]}" -dolbyvision 1 "${OUTPUT}" || \'
fi
echo 'ffmpeg "${ffmpegParams[@]}" -dolbyvision 0 "${OUTPUT}" || exit 1'
# track-stats and clear title
if [[ ${FILE_EXT} == 'mkv' ]]; then
{
echo
echo "mkvpropedit \"${OUTPUT}\" --add-track-statistics-tags"
echo "mkvpropedit \"${OUTPUT}\" --edit info --set \"title=\""
}
fi
echo
} >"${genScript}"
if [[ ${PRINT_OUT} == true ]]; then
echo_info "${genScript} contents:"
echo "$(<"${genScript}")"
else
bash -x "${genScript}" || return 1
rm "${genScript}"
fi
}
FB_FUNC_NAMES+=('encode')
# shellcheck disable=SC2034
FB_FUNC_DESCS['encode']='encode a file using libsvtav1_psy and libopus'
encode() {
set_encode_opts "$@" || return 1
gen_encode_script || return 1
}