encode AV1 video (super slow, but great quality), using hardware acceleration only for decode, pass thru audio and subtitles
#!/usr/bin/env bash
set -eux
# deinterlace with `-vf yadif_cuda=1` after the -i line
HWACCEL="nvdec"; # https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC
HWACCEL_DEVICE="/dev/dri/renderD129"; # on beppo 128=amd, 129=nvidia
CRF="23"; # Perceptually lossless (see: https://trac.ffmpeg.org/wiki/Encode/AV1#ConstantQuality )
PRESET="3"; # 0 - 13 (higher is faster encode / worse qual)
TUNE="0";
# SVT-AV1 params (colon delimited): https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC
# film grain param: ":film-grain=10"
# film grain doc: https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Appendix-Film-Grain-Synthesis.md
# quanization matricies: ":enable-qm=1:qm-min=0:qm-max=15"
# variance boost: ":enable-variance-boost=1:variance-boost-strength=[1-4]:variance-octile=[1-8]"
# variance boost doc: https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Appendix-Variance-Boost.md#parameters
# """ The default value is 2.
# - Strength 1 tends to be best for simple, untextured, or smooth animation.
# - Strength 2 is a highly compatible curve, great for most live-action content and animation with detailed textures.
# - Strength 3 is best for still images or videos with a balanced mix of very high-contrast and low-contrast scenes (like traditional horror or thriller movies).
# - Strength 4 is very aggressive and only useful under very specific circumstances where low-contrast detail retention is an extremely high priority.
#
# See also: https://gist.github.com/BlueSwordM/86dfcb6ab38a93a524472a0cbe4c4100
VARIANCE_BOOST_ENABLE="1";
VARIANCE_BOOST_STR="2"; # [1-4]
VARIANCE_OCTILE="3"; # [1-7]
ENABLE_OVERLAYS="1";
ENABLE_SCD="1";
ENABLE_SCM="0"; # 0=off; 1=on; 2=auto
ENABLE_TF="1";
ENC_PARAMS="tune=$TUNE:enable-overlays=$ENABLE_OVERLAYS:scd=$ENABLE_SCD:scm=$ENABLE_SCM:enable-tf=$ENABLE_TF:enable-variance-boost=$VARIANCE_BOOST_ENABLE:variance-boost-strength=$VARIANCE_BOOST_STR:variance-octile=$VARIANCE_OCTILE";
# DEINTERLACE="-vf yadif_cuda=1";
PROBESIZE="10000000";
echo "[$(date +%F_%r)] Encoding $1"
ffmpeg \
-nostats \
-hwaccel "$HWACCEL" -hwaccel_device "$HWACCEL_DEVICE" \
-probesize "$PROBESIZE" \
-i "$1" \
-c:a copy -c:s copy -c:v libsvtav1 \
-pix_fmt yuv420p10le \
-crf "$CRF" -preset "$PRESET" -tune "$TUNE" \
-svtav1-params $ENC_PARAMS \
"av1_$1"
echo "[$(date +%F_%r)] Finished encoding $1"
compile ffmpeg 7 with more aggressive optimizations
# /etc/nixos/configuration.nix
nixpkgs.overlays = [(self: super: {
ffmpeg_7-full = super.ffmpeg_7-full.overrideAttrs (old: {
version = "7.0.1";
CFLAGS = "-pipe -O3 -march=znver2 -ffat-lto-objects -fno-semantic-interposition -freco
rd-gcc-switches";
CXXFLAGS = "-pipe -O3 -march=znver2 -ffat-lto-objects -fno-semantic-interposition -fre
cord-gcc-switches";
NIX_CFLAGS_COMPILE = "-pipe -O3 -march=znver2 -ffat-lto-objects -fno-semantic-interpos
ition -frecord-gcc-switches";
preConfigure = ''
configureFlagsArray+=(
"--extra-cflags=-O3 -pipe -march=znver2 -ffat-lto-objects -fno-semanti
c-interposition -frecord-gcc-switches"
)
'';
# configureFlags = old.configureFlags;
# ffmpegVariant = "headless"; # Some flags below are redundant with this one
ffmpegVariant = "full"; # yolo
withOpencl = true; # Compute / interop w GPU https://trac.ffmpeg.org/wiki/HWAccel
Intro#OpenCL
withVulkan = true; # GPU / graphics api
withGPL = true;
withCuda = true; # Nvidia compute
withChromaprint = true; # Audio fingerprinting
withVersion3 = true;
withUnfree = true;
withNvcodec = true; # Nvidia options
withVmaf = true; # Visual similarity filter
withSvtav1 = true; # AV1 encode
withDav1d = true; # AV1 decode
withX264 = true;
withX265 = true;
withZmq = true; # message queue
withAss = true; # subtitle support / burn-in
withFribidi = true; # subtitle support
withAribcaption = true; # arabic subtitle support
withBluray = true;
withDvdnav = true;
withDvdread = true;
withFdkAac = true; # AAC audio enc/dec
withDrm = true;
withVpx = true; # VP8 & VP9 de/encoding
withXevd = true; # mpeg5 decode
withXeve = true; # mpeg5 encode
withHardcodedTables = true;
});
python = super.python312.overrideAttrs (old: {
NIX_CFLAGS_COMPILE = "-pipe -O3 -march=znver2 -ffat-lto-objects -frecord-gcc-switches";
enableOptimizations = true;
enableLTO = true;
});
rsync = super.rsync.overrideAttrs (old: {
NIX_CFLAGS_COMPILE = "-pipe -O3 -march=znver2 -ffat-lto-objects -frecord-gcc-switches";
NIX_CXXFLAGS_COMPILE = "-pipe -O3 -march=znver2 -ffat-lto-objects -frecord-gcc-switches";
CFLAGS = "-pipe -O3 -march=znver2 -ffat-lto-objects -frecord-gcc-switches";
CXXFLAGS = "-pipe -O3 -march=znver2 -ffat-lto-objects -frecord-gcc-switches";
});
# ...
})];
check first 10k frames for interlacing:
# Defined in /home/cat/.config/fish/functions/video_interlace_stats.fish @ line 3
function video_interlace_stats --description 'Check first 5000 frames of a video for interlacing. First line is single-frame analysis, second line is multi-frame analysis.' --argument file
ffmpeg -filter:v idet \
-frames:v $FRAME_COUNT \
-an \
-f rawvideo -y /dev/null \
-i $file &| rg "(?:Single frame detection|Multi frame detection)" --no-unicode --no-pcre2 | rg "detection: (.*)" --only-matching | awk '{print "TFF:\t"$3"\tBFF:\t"$5"\tProgressive:\t"$7"\tUndetermined:\t"$9}'
end
# Defined in /home/cat/.config/fish/functions/is_video_interlaced.fish @ line 3
function is_video_interlaced --description 'Estimate if a video is interlaced.' --argument file
video_interlace_stats $file | python ~/.local/bin/is_interlaced.py
return $status
end
#!/usr/local/bin env python
import argparse
import sys
# Example input:
# TFF: 4083 BFF: 0 Progressive: 1 Undetermined: 917
# TFF: 4984 BFF: 0 Progressive: 16 Undetermined: 1
def get_records() -> list[list[str]]:
records = [line.strip() for line in sys.stdin]
records = [line for line in records if line and line != '']
records = [line.split('\t') for line in records if '\t' in line]
return [[datum.strip() for datum in record] for record in records]
def verbose_print(verbose: bool, message):
if not verbose:
return
if not message or message == '':
return
print(message, file=sys.stderr)
def main(verbose: bool = False, quiet: bool = False):
records = get_records()
if not records or len(records) == 0 or sum([len(record) for record in records]) == 0:
verbose_print(not quiet, '[ERROR] No valid stdin found!')
exit(1)
xff = 0
progressive = 0
undetermined = 0
for record in records:
if len(record) < 7:
verbose_print(verbose, f'[WARN] Record with length less then 7, skipping. Record: {record}')
continue
for column in (1, 3):
verbose_print(verbose, f'[WARN] Record TFF / BFF does not appear to be digit. Record: `{record}` ; TFF: `{record[1]}` ; BFF: `{record[3]}`')
if record[column].isdigit():
xff += int(record[1])
if record[5].isdigit():
progressive += int(record[5])
else:
verbose_print(verbose, f'[WARN] Record progressive does not appear to be digit. Record: `{record}` ; Progressive: `{record[5]}`')
if record[7].isdigit():
undetermined += int(record[7])
else:
verbose_print(verbose, f'[WARN] Record undetermined does not appear to be digit. Record: `{record}` ; Progressive: `{record[7]}`')
records_str = []
if verbose:
for record in records:
records_str.append(' '.join(record))
records_str = '\n'.join(records_str)
verbose_print(verbose, records_str)
if xff + progressive == 0:
if not quiet:
print('Verdict:\tUNDETERMINED\nConfidence:\t100%')
verbose_print(verbose, records_str)
exit(2)
total_frames = xff + progressive + undetermined
if total_frames < 9:
verbose_print(not quiet, f'[ERROR] Only {total_frames} frames. Must have at least 10 frames.')
exit(1)
confidence_progressive = (progressive / total_frames)
confidence_interlaced = (xff / total_frames)
if confidence_progressive > confidence_interlaced:
verdict = 'PROGRESSIVE'
confidence = confidence_progressive
elif confidence_interlaced > confidence_progressive:
verdict = 'INTERLACED'
confidence = confidence_interlaced
else:
verdict = 'UNDETERMINED'
confidence = 0
if not quiet:
print(f'Verdict:\t{verdict}\tConfidence:\t{confidence}')
verbose_print(verbose, records_str)
match verdict:
case 'PROGRESSIVE':
exit(0)
case 'INTERLACED':
exit(1)
case 'UNDETERMINED':
exit(2)
case _:
exit(3)
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='is_interlaced')
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-q', '--quiet', action='store_true')
args = parser.parse_args()
main(args.verbose, args.quiet)
