Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 60 additions & 9 deletions vc2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
doneDir="$baseDir/done"
videoDir="/video"
tempDir="/video/temp"
# Serviio's account, kept for reference only: the script cannot chown to it
# (see encodeIt). Read access comes from $group, which both accounts share.
# shellcheck disable=SC2034
user="serviio"
group="video"
# Concurrent encodes. Defaults to 1 because concurrency buys nothing once
Expand Down Expand Up @@ -387,17 +390,36 @@ EOM
video_codec='hevc_nvenc'
# Decode on NVDEC in addition to encoding on NVENC. Decoded frames are
# downloaded to system memory, so the CPU filter chain (scale/fps/format)
# is unaffected. ffmpeg falls back to software decode per-stream if the
# source codec is not NVDEC-supported.
# is unaffected.
#
# This is applied per file, not globally: -hwaccel cuda is a decoder
# SELECTION directive, not a hint. It replaces the software decoder with
# the native hwaccel shim, which has no software path, so handing it a
# codec NVDEC cannot decode does not fall back -- ffmpeg fails every
# packet in a loop, forever, writing nothing. With -loglevel quiet that
# looks exactly like a hang. Seen on 2026-09-19 with an AV1 source:
# "Your platform doesn't support hardware accelerated AV1 decoding"
# followed by "Error submitting packet to decoder: Function not
# implemented" once per packet.
#
# -hwaccels only reports that the method exists, never which codecs the
# silicon handles, so the codec list is ours to keep. NVDEC gained AV1
# with Ampere GA10x (compute 8.6); Turing (7.5) has none.
if $ffmpeg_bin -hwaccels 2>/dev/null | grep -qw 'cuda'; then
hwaccel_args='-hwaccel cuda'
hwaccel_decode='-hwaccel cuda'
nvdec_codecs='h264 hevc vp8 vp9 mpeg1video mpeg2video mpeg4 vc1'
computeCap=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1)
if [[ -n $computeCap ]] && awk "BEGIN{exit !($computeCap >= 8.6)}" 2>/dev/null; then
nvdec_codecs+=' av1'
fi
fi
fi
fi

# Define Global variables
declare -a fullName fileName extension baseName baseDir outDir
declare vOpts vFilter aOpts aFilter sOpts outFile metaFile hwaccel_args
declare hwaccel_decode nvdec_codecs computeCap hwDecodeNote

## Defined Functions
logIt ()
Expand Down Expand Up @@ -778,8 +800,28 @@ declare vOpts vFilter aOpts aFilter sOpts outFile metaFile hwaccel_args
fi
}

buildHwaccelOpts ()
{
# Requires $vCodec from probeIt. See the nvdec_codecs note in the hardware
# detection block above for why this is gated on the source codec.
hwaccel_args=''
hwDecodeNote=''
[[ -z $hwaccel_decode ]] && return 0
# shellcheck disable=SC2154 # vCodec comes from cc_probe via .probe.rc
if [[ " $nvdec_codecs " == *" $vCodec "* ]]; then
hwaccel_args="$hwaccel_decode"
else
# Reported by encodeIt rather than here: setOpts runs under displayIt's
# spinner, which owns the current line.
hwDecodeNote="NVDEC has no ${vCodec} decoder; decoding in software"
traceIt $LINENO buildHwaccelOpts " info " "$hwDecodeNote"
fi
return 0
}

setOpts ()
{
buildHwaccelOpts
buildVideoFilter
buildVideoOpts
buildAudioOpts
Expand All @@ -800,8 +842,12 @@ declare vOpts vFilter aOpts aFilter sOpts outFile metaFile hwaccel_args
killWait 1 "Could not create directory ${outDir[$l]}"
exit $check
fi
chown $user:$group "${outDir[$l]}" 2>/dev/null
chmod 0775 "${outDir[$l]}" 2>/dev/null
# Only the group is set: chown to another user needs root, and the
# output shares are exported root_squash, so root cannot do it either.
# $group membership is what Serviio reads through. setgid so any season
# subdirectory created later inherits the group instead of our own.
chgrp "$group" "${outDir[$l]}" || logIt "WARN: chgrp $group failed on ${outDir[$l]}"
chmod 2775 "${outDir[$l]}" || logIt "WARN: chmod 2775 failed on ${outDir[$l]}"
fi
if [[ $hq != 1 ]]; then
outFile="${outDir[$l]}/${baseName[$l]}.mp4"
Expand All @@ -811,7 +857,9 @@ declare vOpts vFilter aOpts aFilter sOpts outFile metaFile hwaccel_args

ffmpeg_string="${ffmpeg_bin} "
ffmpeg_string+="-hide_banner -y "
ffmpeg_string+="-loglevel quiet -stats "
# Not "quiet": that hid a decoder failing every packet for seven minutes.
# -stats still provides the progress line.
ffmpeg_string+="-loglevel error -stats "
[[ -n $hwaccel_args ]] && ffmpeg_string+="$hwaccel_args "
ffmpeg_string+="-i \"$inFile\" "
ffmpeg_string+="-i \"$metaFile\" "
Expand All @@ -827,6 +875,8 @@ declare vOpts vFilter aOpts aFilter sOpts outFile metaFile hwaccel_args

traceIt $LINENO encodeIt " CMD " "> $ffmpeg_string $outFile"

[[ -n $hwDecodeNote ]] && echo -e " ${C8}${hwDecodeNote}${C0}"

jobLog=''
if (( maxJobs > 1 )); then
# Concurrent jobs would interleave ffmpeg's -stats output on the terminal,
Expand All @@ -843,7 +893,7 @@ declare vOpts vFilter aOpts aFilter sOpts outFile metaFile hwaccel_args
if (( STATUS > 0 )); then
logIt "Re-encoding of $inFile failed!"
traceIt $LINENO encodeIt "ERROR!" "STATUS=$STATUS, ffmpeg encode failed."
echo -e "> ${C1}ERROR (${baseName[$l]}): Run the following to see details why:\n${ffmpeg_string//-loglevel quiet -stats /} $tempOut${C0}\n"
echo -e "> ${C1}ERROR (${baseName[$l]}): Run the following to see details why:\n${ffmpeg_string//-loglevel error -stats /} $tempOut${C0}\n"
[[ -n $jobLog ]] && echo -e "> ${C1}ffmpeg output: $jobLog${C0}"
rm -f "$tempOut"
else
Expand Down Expand Up @@ -892,8 +942,9 @@ declare vOpts vFilter aOpts aFilter sOpts outFile metaFile hwaccel_args
} >> "$traceLog" 2>&1

logIt "outFile = $outFile"
chown $user:$group "$outFile" 2>/dev/null
chmod 0664 "$outFile" 2>/dev/null
# See the chgrp note above: user ownership is not ours to set here.
chgrp "$group" "$outFile" || logIt "WARN: chgrp $group failed on $outFile"
chmod 0664 "$outFile" || logIt "WARN: chmod 0664 failed on $outFile"
fi

# Logged here rather than in the main loop so the markers stay with the job
Expand Down
Loading