Created
October 22, 2021 06:50
-
-
Save alanorth/ce1e4938db73d8ef800179c86d4b31ac to your computer and use it in GitHub Desktop.
Script to convert video to constant quality (CRF) mode AV1 attempting to match a target VMAF.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# | |
# v2021-10-21 | |
# | |
# I run it like this (using zsh syntax): | |
# | |
# $ for video (~/Pictures/2021/**/*.mp4(ND.)); do ~/Downloads/av1-tests/convert-av1.sh "$video"; done | |
# | |
# Reference: | |
# - https://ffmpeg.org/ffmpeg-codecs.html#libaom_002dav1 | |
# - https://trac.ffmpeg.org/wiki/Encode/AV1 | |
# | |
# Changes: | |
# 2021-10-21: I tested 2-pass and found it doesn't do anything for constant | |
# quality mode. cpu-used 2 still takes ~5x longer than cpu-used 4 | |
# at all tiling levels. 4x4 tiles much faster than 2x1 and 2x2 on | |
# all cpu-used levels, with quality less than 0.5 VMAF difference. | |
# Using 4x4 creates a grid of 16 tiles which just seems wrong... | |
# especially if you don't have many threads (I was using 12), so | |
# I will default to 2x2 as a compromise. | |
# 2021-10-20: use tiles instead of columns and rows | |
# 2021-10-16: use tile-columns 2 and tile-rows 2 (oops, this is 4x4 tiles!) | |
# 2021-08-31: strip spaces and commas from VMAF log file name as well | |
# 2021-05-14: benchmarked two pass and file sizes are larger by .5-1MB | |
# 2021-04-29: make sure input file exists | |
# 2021-03-07: benchmarked 10-bit and it's only .5 or less improvment to VMAF | |
# 2021-02-22: allow overriding threads | |
# 2021-02-15: fix handling of apostrophes in ffmpeg's log_path | |
# 2021-01-27: round VMAF before comparing, simplifies code a bit | |
# 2021-01-25: stop trying lower CRFs if VMAF is not likely to improve | |
# 2021-01-19: embed encoding parameters in webm metadata with mkvpropedit | |
# 2021-01-18: use 1-pass encoding (2-pass is only for trying to hit a target | |
# bitrate!) | |
# 2021-01-18: allow overriding variables | |
# 2021-01-17: detect if video already processed | |
# 2021-01-08: use cpu-used 4 | |
# 2021-01-07: use tile-columns 2 and tile-rows 2 with cpu-used 4 | |
# 2021-01-07: use tile-columns 2 and tile-rows 2 with cpu-used 3 | |
# 2021-01-07: use -g 300 (30fps x 10) | |
# exit on first error | |
set -o errexit | |
if [[ -z "$1" ]]; then | |
echo "No input file specified." | |
exit 1 | |
elif [[ ! -r "$1" ]]; then | |
echo "Input file missing or unreadable: $1" | |
exit 1 | |
fi | |
INPUT_FILE_BASENAME=$(basename "$1") | |
INPUT_FILE_EXTENSION=${INPUT_FILE_BASENAME##*.} | |
INPUT_FILE_DIRNAME=$(dirname "$1") | |
# Check if an output file name was specified (like if we are calling from the | |
# benchmarking script, in which case we want to exit as soon as possible). If | |
# not then we can continue with a simple filename based on the input file's. | |
if [[ -z $_output_file ]]; then | |
_output_file="${INPUT_FILE_BASENAME/.*/}.webm"; | |
_benchmark_mode="false" | |
else | |
_benchmark_mode="true" | |
fi | |
# aq-mode not recommended in AV1 yet | |
_aq_mode=${_aq_mode:-0} | |
# 2021-01-08 (libaom 2.0.1): realistic range between 3 and 5, lower takes *much* | |
# longer with very little boost to VMAF. In my experience, cpu-used 2 is *three* | |
# times slower than cpu-used 4 at the same CRF for only a 0.5% boost in VMAF. 6 | |
# is the same as 5 in all my tests *shrug*. | |
_cpu_used=${_cpu_used:-4} | |
# 2021-10-20 -tile-columns and -tile-rows are for compatibility with libvpx. The | |
# new option for libaom is -tiles. Convert any columns/rows to tiles using 2^n. | |
if [[ ! -z $_tile_columns || ! -z $_tile_rows ]]; then | |
_tile_columns=${_tile_columns:-1} | |
_tile_rows=${_tile_rows:-1} | |
# Compute 2^n tile columns and tile rows (the power operator is ** in bash) | |
_tiles="$((2 ** $_tile_columns))x$((2 ** $_tile_rows))" | |
else | |
_tiles=${_tiles:-2x2} | |
fi | |
# 2021-02-22 use nproc number of threads unless threads is already set. | |
_nproc=$(nproc) | |
_threads=${_threads:-$_nproc} | |
# Range of CRFs to try, where higher is faster, but lower quality. Based on my | |
# testing 52 to 40 should cover most crappy phone videos. | |
_crf_max=${_crf_max:-52} | |
_crf_min=${_crf_min:-40} | |
_libaom_version=$(pacman -Qi aom | grep Version | awk '{print $3}') | |
# For grainy mobile phone videos 90 is fine | |
_target_vmaf=${_target_vmaf:-90} | |
# Don't bother trying lower CRF values if the CRF score is more than five points | |
# above the target. In my experience each successively lower CRF step gives you | |
# ~1 VMAF point. In these cases it isn't likely we'll ever reach the target, so | |
# we might as well just settle on the current CRF straight away. | |
_target_vmaf_threshold=${_target_vmaf_threshold:-5} | |
_acceptable_output_found="no" | |
# Change to the input file's directory | |
pushd "$INPUT_FILE_DIRNAME" >/dev/null | |
# We want the highest VMAF score possible with the highest CRF possible. Start | |
# with high CRF first to see if we can get an acceptable VMAF score as soon as | |
# possible. Step through values by 2. | |
for _crf in $(seq $_crf_max -2 $_crf_min); do | |
_processed=$(find . -maxdepth 1 -type f -iname "$_output_file" | wc -l) | |
if [[ $_processed -gt 0 ]]; then | |
echo "${INPUT_FILE_BASENAME}: already processed" | |
exit 0 | |
fi | |
if [[ $_benchmark_mode == "false" ]]; then | |
echo "Processing ${INPUT_FILE_BASENAME} with libaom $_libaom_version CRF ${_crf}..." | |
fi | |
chrt -b 0 nice ffmpeg -hide_banner -y -i "$INPUT_FILE_BASENAME" -c:v libaom-av1 -b:v 0 -crf $_crf \ | |
-aq-mode $_aq_mode -c:a libopus -b:a 16k \ | |
-sc_threshold 0 -cpu-used $_cpu_used \ | |
-tiles $_tiles -row-mt 1 \ | |
-auto-alt-ref 1 -lag-in-frames 25 \ | |
-g 300 -threads $_threads \ | |
-f webm "$_output_file" 2>/dev/null | |
# Return quickly if we are in benchmark mode so the benchmark script can get | |
# an accurate time and compute its own VMAF score. | |
if [[ $_benchmark_mode == "true" ]]; then | |
exit 0 | |
fi | |
_vmaf_log="${_output_file/.*/}.log" | |
# strip apostrophes, spaces, and commas (if any) from log file name because | |
# ffmpeg's log_path doesn't seem to be able to handle them. The // performs | |
# a global replace. | |
_vmaf_log="${_vmaf_log//\'}" | |
_vmaf_log="${_vmaf_log//\,}" | |
_vmaf_log="${_vmaf_log// }" | |
# Get VMAF score with harmonic mean to emphasize small outliers | |
# See: https://netflixtechblog.com/vmaf-the-journey-continues-44b51ee9ed12 | |
chrt -b 0 nice ffmpeg -hide_banner -y -i "$_output_file" -i "$INPUT_FILE_BASENAME" \ | |
-filter_complex "libvmaf=pool=harmonic_mean:log_path=${_vmaf_log}:log_fmt=json" \ | |
-f null - 2>/dev/null | |
_vmaf_score=$(jq '.["VMAF score"]' "$_vmaf_log") | |
# bash can't do floating point so we use bc | |
# See: https://stackoverflow.com/questions/8654051/how-to-compare-two-floating-point-numbers-in-bash | |
_vmaf_score_round=$(printf %.$2f $(echo "scale=0;(((10^0)*$_vmaf_score)+0.5)/(10^0)" | bc)) | |
#rm "$_vmaf_log" | |
# Check if rounded VMAF score is >= target VMAF | |
if [[ $_vmaf_score_round -ge $_target_vmaf ]]; then | |
printf "$INPUT_FILE_BASENAME: acceptable VMAF (%.3f) at AV1 CRF ${_crf}.\n" $_vmaf_score | |
# Set the title with information about the encoding in the MKV title | |
mkvpropedit --edit info --set "title=${INPUT_FILE_BASENAME/.*/}-libaom_${_libaom_version}-crf${_crf}-cpu${_cpu_used}-${_tiles}-tiles-vmaf_${_vmaf_score}" "$_output_file" >/dev/null | |
_acceptable_output_found="yes" | |
# Break from the for loop because we have an acceptable output | |
break | |
# Check if the VMAF score is anywhere near our target, otherwise just keep | |
# current output. | |
elif (($_target_vmaf - $_vmaf_score_round >= $_target_vmaf_threshold)); then | |
printf "$INPUT_FILE_BASENAME: unacceptable VMAF (%.3f) at AV1 CRF ${_crf}, unlikely to reach target soon.\n" $_vmaf_score | |
_acceptable_output_found="yes" | |
# Break from the for loop and keep the current output, even though it's | |
# unacceptable. | |
break | |
else | |
printf "$INPUT_FILE_BASENAME: unacceptable VMAF (%.3f) at AV1 CRF ${_crf}, continuing.\n" $_vmaf_score | |
# Clean up the unacceptable output | |
rm "$_output_file" | |
fi | |
done | |
# If we finished going over all the CRFs and still didn't get an acceptable VMAF | |
# score then the video is probably just really low quality so let's just convert | |
# using our max CRF and be done with it. | |
if [[ $_acceptable_output_found == "no" ]]; then | |
echo "${INPUT_FILE_BASENAME}: no acceptable output found, settling on AV1 CRF ${_crf_max}." | |
chrt -b 0 nice ffmpeg -hide_banner -y -i "$INPUT_FILE_BASENAME" -c:v libaom-av1 -b:v 0 -crf $_crf_max \ | |
-aq-mode $_aq_mode -c:a libopus -b:a 16k \ | |
-sc_threshold 0 -cpu-used $_cpu_used \ | |
-tiles $_tiles -row-mt 1 \ | |
-auto-alt-ref 1 -lag-in-frames 25 \ | |
-g 300 -threads $_threads \ | |
-f webm "$_output_file" 2>/dev/null | |
# Set the title with information about the encoding in the MKV title | |
mkvpropedit --edit info --set "title=${INPUT_FILE_BASENAME/.*/}-libaom_${_libaom_version}-crf${_crf}-cpu${_cpu_used}-${_tiles}-tiles" "$_output_file" >/dev/null | |
fi | |
# Change back to our starting directory | |
popd >/dev/null | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment