diff --git a/lib/build.sh b/lib/build.sh index de7806b..ae6157a 100644 --- a/lib/build.sh +++ b/lib/build.sh @@ -184,7 +184,7 @@ get_build_conf() { ffmpeg 8.0 tar.gz https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n${ver}.${ext} hdr10plus_tool 1.7.1 tar.gz https://github.com/quietvoid/hdr10plus_tool/archive/refs/tags/${ver}.${ext} dovi_tool 2.3.0 tar.gz https://github.com/quietvoid/dovi_tool/archive/refs/tags/${ver}.${ext} -libsvtav1 3.1.1 tar.gz https://gitlab.com/AOMediaCodec/SVT-AV1/-/archive/v${ver}/SVT-AV1-v${ver}.${ext} +libsvtav1 3.1.2 tar.gz https://gitlab.com/AOMediaCodec/SVT-AV1/-/archive/v${ver}/SVT-AV1-v${ver}.${ext} libsvtav1_psy 3.0.2-A tar.gz https://github.com/BlueSwordM/svt-av1-psyex/archive/refs/tags/v${ver}.${ext} dovi_tool,hdr10plus_tool,cpuinfo librav1e 0.8.1 tar.gz https://github.com/xiph/rav1e/archive/refs/tags/v${ver}.${ext} libaom 3.12.1 tar.gz https://storage.googleapis.com/aom-releases/libaom-${ver}.${ext} @@ -317,7 +317,7 @@ do_build() { # check for any patches for patch in "${PATCHES_DIR}/${build}"/*.patch; do test -f "${patch}" || continue - patch -p1 -i "${patch}" || return 1 + echo_if_fail patch -p1 -i "${patch}" || return 1 done echo_if_fail build_"${build}" retval=$? diff --git a/lib/common.sh b/lib/common.sh index 4a0426c..0d51f40 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -162,3 +162,12 @@ line_starts_with() { is_darwin() { line_contains "$(print_os)" darwin } + +is_positive_integer() { + local input="$1" + if [[ ${input} != ?(-)+([[:digit:]]) || ${input} -lt 0 ]]; then + echo_fail "${input} is not a positive integer" + return 1 + fi + return 0 +} diff --git a/lib/docker.sh b/lib/docker.sh index d5d9567..4dfd4f3 100644 --- a/lib/docker.sh +++ b/lib/docker.sh @@ -226,6 +226,10 @@ docker_run_image() { -u "$(id -u):$(id -g)" \ "${image_tag}" \ ./scripts/build.sh || return 1 + + docker system prune -f + + return 0 done } diff --git a/lib/encode.sh b/lib/encode.sh new file mode 100644 index 0000000..c5b772e --- /dev/null +++ b/lib/encode.sh @@ -0,0 +1,490 @@ +#!/usr/bin/env bash + +get_duration() { + local file="$1" + ffprobe \ + -v error \ + -show_entries format=duration \ + -of default=noprint_wrappers=1:nokey=1 \ + "${file}" +} + +get_crop() { + local file="$1" + local duration="$(get_duration "${file}")" + # don't care about decimal points + IFS='.' read -r duration _ <<<"${duration}" + # get crop value for first half of input + local timeEnc=$((duration / 2)) + ffmpeg \ + -y \ + -hide_banner \ + -ss 0 \ + -discard 'nokey' \ + -i "${file}" \ + -t "${timeEnc}" \ + -map '0:v:0' \ + -filter:v:0 'cropdetect=limit=100:round=16:skip=2:reset_count=0' \ + -codec:v 'wrapped_avframe' \ + -f 'null' '/dev/null' 2>&1 | + grep -o crop=.* | + sort -bh | + uniq -c | + sort -bh | + tail -n1 | + grep -o "crop=.*" +} + +get_stream_codec() { + local file="$1" + local stream="$2" + ffprobe \ + -v error \ + -select_streams "${stream}" \ + -show_entries stream=codec_name \ + -of default=noprint_wrappers=1:nokey=1 \ + "${file}" +} + +get_file_format() { + local file="$1" + local probe="$(ffprobe \ + -v error \ + -show_entries format=format_name \ + -of default=noprint_wrappers=1:nokey=1 \ + "${file}")" + if line_contains "${probe}" 'matroska'; then + echo mkv + else + echo mp4 + fi +} + +get_streams() { + file="$1" + ffprobe \ + -v error \ + -show_entries stream=index \ + -of default=noprint_wrappers=1:nokey=1 \ + "${file}" +} + +get_audio_streams() { + file="$1" + ffprobe \ + -v error \ + -select_streams a \ + -show_entries stream=index \ + -of default=noprint_wrappers=1:nokey=1 \ + "${file}" +} + +get_num_audio_channels() { + file="$1" + stream="$2" + ffprobe \ + -v error \ + -select_streams "${stream}" \ + -show_entries stream=channels \ + -of default=noprint_wrappers=1:nokey=1 \ + "${file}" + +} + +unmap_streams() { + local file="$1" + local unmapFilter='bin_data|jpeg|png' + local unmap=() + for stream in $(get_streams "${file}"); 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_audio_streams "${file}"); do + local numChannels="$(get_num_audio_channels "${file}" "a:${stream}")" + local channelBitrate=$((numChannels * 64)) + local codec="$(get_stream_codec "${file}" "a:${stream}")" + if [[ ${codec} == 'opus' ]]; then + bitrate+=( + "-c:a:${stream}" + "copy" + ) + else + bitrate+=( + "-filter:a:${stream}" + "aformat=channel_layouts=7.1|5.1|stereo|mono" + "-c:a:${stream}" + "libopus" + "-b:a:${stream}" + "${channelBitrate}k" + ) + fi + done + echo "${bitrate[@]}" +} + +convert_subs() { + local file="$1" + local convertCodec='eia_608' + local convert=() + for stream in $(get_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 + 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 "$(bash_basename "$0") -i input_file [options] " + echo -e "\t[-P NUM] set preset (default: ${PRESET})" + echo -e "\t[-C NUM] set CRF (default: ${CRF})" + 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[-g NUM] set film grain for encode" + echo -e "\t[-s] use same container as input, default is mkv" + echo -e "\n\t[output_file] if unset, defaults to ${HOME}/" + echo -e "\n\t[-I] Install this as /usr/local/bin/encode" + echo -e "\t[-U] Uninstall this from /usr/local/bin/encode" + return 0 +} + +set_encode_opts() { + local opts='vi:pcsdg:P:IU' + local numOpts="${#opts}" + # default values + PRESET=3 + CRF=25 + GRAIN="" + CROP='false' + PRINT_OUT='false' + DISABLE_DV='false' + local SAME_CONTAINER="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 + while getopts "${opts}" flag; do + case "${flag}" in + I) + echo_warn "attempting install" + sudo ln -sf "$(pwd)/scripts/recc_encode.sh" \ + /usr/local/bin/encode || return 1 + echo_pass "succesfull install" + exit 0 + ;; + U) + echo_warn "attempting uninstall" + sudo rm /usr/local/bin/encode || 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) + SAME_CONTAINER='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 [[ $SAME_CONTAINER == "true" ]]; then + local fileFormat="$(get_file_format "${INPUT}")" + 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}" + + 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="-vf $(get_crop "${INPUT}")" + 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' + ) + + local unmap=($(unmap_streams "${INPUT}")) + if [[ ${unmap[*]} != '' ]]; then + ffmpegParams+=('${unmap[@]}') + fi + + local audioBitrate=($(set_audio_bitrate "${INPUT}")) + if [[ ${audioBitrate[*]} != '' ]]; then + ffmpegParams+=('${audioBitrate[@]}') + fi + + if [[ ${crop} != '' ]]; then + ffmpegParams+=('${crop}') + fi + + ffmpegParams+=( + '-c:s' 'copy' + ) + local convertSubs=($(convert_subs "${INPUT}")) + if [[ ${convertSubs[*]} != '' ]]; then + ffmpegParams+=('${convertSubs[@]}') + 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 +} diff --git a/scripts/encode.sh b/scripts/encode.sh new file mode 120000 index 0000000..44b7711 --- /dev/null +++ b/scripts/encode.sh @@ -0,0 +1 @@ +entry.sh \ No newline at end of file