Compare commits

...

No commits in common. "v0.11.0" and "master" have entirely different histories.

2 changed files with 186 additions and 54 deletions

42
README.md Normal file
View file

@ -0,0 +1,42 @@
# burnSubs
You found my burnSubs tool. The goal for this tool is to ease the
conversion of arbitrary video files with soft-subtitles in the SSA/ASS
Substation Alpha format into simple stereo hard-subtitled video files.
## Features
* softsub to hardsub conversion
* embedded font files
* selecting specific audio streams
* selecting specific video streams
* automatic selection of language preferences
* default surround sound to stereo down-mixing
* opt out CLI flag available
* anti-clobbering default behavior
* auto-cleanup on error
## Prerequisite Tools
Firstly, the tool will yell at you if the tools it needs don't exist.
Feel free to just run the tool, and it will let you know what you're
missing.
* [`ffmpeg`](https://ffmpeg.org/) - the one and only
* `ffprobe` - usually comes with ffmpeg
* [`jq`](https://stedolan.github.io/jq/) - file/pipe based JSON processor
## How does it work?
`burnSubs` takes an input video file and tries to figure out what
audio streams and subtitle streams exist within the file. It stores
metadata in `/tmp` while it runs. When running it will pull the
streams within the input file, and try to select Japanese language
audio streams, and a non-signs subtitle stream (i.e. a full language
subtitle stream) to add to the output video.
Before transcoding it will then extract the subtitle file to pass into
`ffmpeg`'s subtitle burn in filter, and will try to down-mix any
surround sound input streams to stereo. Downmixing, track selection,
clobbering behavior, and verbosity can all be controlled to a limited
extent by CLI flags.
## Can you make it do *XYZ*.
Give me an enhancement request in github and I'll take a look.

198
burnSubs
View file

@ -5,18 +5,18 @@ set -o errexit
################################################################################ ################################################################################
# burnSubs # burnSubs
# version 0.11.0 # version 0.13.3
#################3 #################
# Wishlist: # Wishlist:
# queue encodes # queue encodes
# finish TODOs # finish TODOs
# finish help flag # finish help flag
# audio recode flag # audio recode flag
# #
# Changes
# automatically select JPN audio if more than one audio channel found.
################################################################################ ################################################################################
DEFAULTS_OPTS_CRF=18
function machineSetup() { function machineSetup() {
# Default setup # Default setup
export FF_ENC="libx264" export FF_ENC="libx264"
@ -24,7 +24,7 @@ function machineSetup() {
export FILT_PFX="" export FILT_PFX=""
export FILT_SFX="" export FILT_SFX=""
CRF=${OPTS_CRF:-23} CRF=${OPTS_CRF:-$DEFAULTS_OPTS_CRF}
export FF_STD="-preset myTranscoderPreset -crf $CRF -tune animation \ export FF_STD="-preset myTranscoderPreset -crf $CRF -tune animation \
-preset medium -movflags +faststart" -preset medium -movflags +faststart"
export FF_EXT="-profile:v high -level 4.0" export FF_EXT="-profile:v high -level 4.0"
@ -55,7 +55,7 @@ function machineSetup() {
#export FFMPEG="/opt/ffmpeg-nvenc/bin/ffmpeg" #export FFMPEG="/opt/ffmpeg-nvenc/bin/ffmpeg"
export LD_LIBRARY_PATH="/opt/ffmpeg-nvenc/lib" export LD_LIBRARY_PATH="/opt/ffmpeg-nvenc/lib"
export FF_EXT="${FF_EXT} -pix_fmt yuv420p" export FF_EXT="${FF_EXT} -pix_fmt yuv420p"
elif [[ "$(hostname)" == "grad-heo-lappy" ]]; then elif [[ "$(hostname)" == "random-vaapi-intel-laptop" ]]; then
echo " > Hostname: $(hostname)" echo " > Hostname: $(hostname)"
export OPTS_ENC="vaapi" export OPTS_ENC="vaapi"
else else
@ -64,20 +64,6 @@ function machineSetup() {
export FF_EXT="${FF_EXT} -pix_fmt yuv420p" export FF_EXT="${FF_EXT} -pix_fmt yuv420p"
fi fi
fi fi
# Configure audio filtergraph if needed.
if [[ "${OPTS_TRANS_AUDIO}" == true ]]; then
FILT_AUDIO="-c:a aac"
if [[ "${OPTS_LPF_AUDIO}" == true ]]; then
FILT_AUDIO="-filter:a highpass=f=7 ${FILT_AUDIO}"
fi
if [[ "$(hostname)" == "Ram-the-Red" ]]; then
FILT_AUDIO="${FILT_AUDIO} -strict -2"
fi
else
FILT_AUDIO="-c:a copy"
fi
# Configure the encoder based upon the specific encoder chain required # Configure the encoder based upon the specific encoder chain required
## NVIDIA GPU Encode/Decode ## NVIDIA GPU Encode/Decode
@ -151,9 +137,9 @@ function setupBins() {
setAndValidateBin "ffmpeg" "FFMPEG" setAndValidateBin "ffmpeg" "FFMPEG"
setAndValidateBin "ffprobe" "FFPROBE" setAndValidateBin "ffprobe" "FFPROBE"
setAndValidateBin "jq" "JQ" setAndValidateBin "jq" "JQ"
setAndValidateBin "python3" "PYTHON3" #setAndValidateBin "python3" "PYTHON3"
setAndValidateBin "awk" "AWK" #setAndValidateBin "awk" "AWK"
setAndValidateBin "date" "DATE" #setAndValidateBin "date" "DATE"
} }
# Used by the above function to evaluate overrides if they are set, and # Used by the above function to evaluate overrides if they are set, and
@ -187,6 +173,7 @@ function doCleanup() {
echo "=> Cleaning up." echo "=> Cleaning up."
rm -r "$TMP" rm -r "$TMP"
else else
set +x
echo "tmp dir: $TMP" echo "tmp dir: $TMP"
fi fi
@ -291,13 +278,48 @@ function parseStreams() {
# Extract subtitles # Extract subtitles
#$item.codec_type == "subtitle" \&\& #$item.codec_type == "subtitle" \&\&
# shellcheck disable=SC2016 # shellcheck disable=SC2016
"$JQ" 'reduce .streams[] as $item ([]; if ($item.codec_name == "ass") then [.[],$item] else . end) | reduce .[] as $item ([]; [.[],{t:($item.tags.title),i:($item.index),lang:$item.tags.language, disposition:$item.disposition}]) | reduce .[] as $item ([]; [.[],{t:($item.t // ($item.lang + "-" + ($item.i | tostring))),i:$item.i,lang:$item.lang,disposition:$item.disposition}])' "${STREAMS_ALL}" > "${STREAMS_SUB}" "$JQ" 'reduce .streams[] as $item ([]; if ($item.codec_name == "ass" or $item.codec_name == "dvd_subtitle") then [.[],$item] else . end) | reduce .[] as $item ([]; [.[],{t:($item.tags.title),i:($item.index),lang:$item.tags.language, disposition:$item.disposition, codec:($item.codec_name)}]) | reduce .[] as $item ([]; [.[],{t:($item.t // ($item.lang + "-" + ($item.i | tostring))),i:$item.i,lang:$item.lang,disposition:$item.disposition,codec:$item.codec}])' "${STREAMS_ALL}" > "${STREAMS_SUB}"
export SUB_COUNT=$("$JQ" 'length' "${STREAMS_SUB}") export SUB_COUNT=$("$JQ" 'length' "${STREAMS_SUB}")
# shellcheck disable=SC2016 # shellcheck disable=SC2016
"$JQ" 'reduce .streams[] as $item ([]; if ($item.codec_type == "audio") then [.[],$item] else . end) | reduce .[] as $item ([]; [.[],{t:($item.tags.title),i:($item.index),lang:$item.tags.language, disposition:$item.disposition}]) | reduce .[] as $item ([]; [.[],{t:($item.t // ($item.lang + "-" + ($item.i | tostring))),i:$item.i,lang:$item.lang,disposition:$item.disposition}])' "${STREAMS_ALL}" > "${STREAMS_AUDIO}" "$JQ" 'reduce .streams[] as $item ([]; if ($item.codec_type == "audio") then [.[],$item] else . end) | reduce .[] as $item ([]; [.[],{t:($item.tags.title),i:($item.index),lang:$item.tags.language, disposition:$item.disposition, chan:{channels:$item.channels, channel_layout:$item.channel_layout}}]) | reduce .[] as $item ([]; [.[],{t:($item.t // ($item.lang + "-" + ($item.i | tostring))),i:$item.i,lang:$item.lang,disposition:$item.disposition, chan:$item.chan}])' "${STREAMS_ALL}" > "${STREAMS_AUDIO}"
export AUDIO_COUNT=$("$JQ" 'length' "${STREAMS_AUDIO}") export AUDIO_COUNT=$("$JQ" 'length' "${STREAMS_AUDIO}")
}
function setupAudioTranscode() {
if [[ $AUDIO_COUNT -ne 1 ]]; then
CHANNEL_COUNT=$($JQ '.[] | select(.i == '$AUDIO_INDEX') | .chan.channels' "$STREAMS_AUDIO")
CHANNEL_LAYOUT=$($JQ '.[] | select(.i == '$AUDIO_INDEX') | .chan.channel_layout' "$STREAMS_AUDIO")
else
CHANNEL_COUNT=$($JQ '.[] | .chan.channels' "$STREAMS_AUDIO")
CHANNEL_LAYOUT=$($JQ '.[] | .chan.channel_layout' "$STREAMS_AUDIO")
fi
if [[ "${CHANNEL_COUNT}" == "6" && "${CHANNEL_LAYOUT}" == '"5.1"' ]]; then
# check if we're 5.1 and if so flag transcode.
export OPTS_TRANS_AUDIO=true
elif [[ "${CHANNEL_COUNT}" != "2" && "${CHANNEL_LAYOUT}" != '"5.1"' ]]; then
echo "ERROR: Trying to enocde non 5.1 and non-stereo audio stream."
exit 1
fi
# Configure audio filtergraph if needed.
if [[ "${OPTS_TRANS_AUDIO}" == true ]]; then
FILT_AUDIO="-c:a aac"
if [[ "${OPTS_LPF_AUDIO}" == true ]]; then
FILT_AUDIO="-filter:a highpass=f=7 ${FILT_AUDIO}"
fi
if [[ "${OPTS_SURROUND_PRESERVE}" == false ]]; then
# From https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg
# Nightmode Formula
FILT_AUDIO="-filter:a pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR ${FILT_AUDIO}"
fi
else
FILT_AUDIO="-c:a copy"
fi
} }
function listSubtitles() { function listSubtitles() {
@ -310,7 +332,11 @@ function listSubtitles() {
X_TITLE=$($JQ '.['$iSUB'].t' $STREAMS_SUB | tr -d '"') X_TITLE=$($JQ '.['$iSUB'].t' $STREAMS_SUB | tr -d '"')
X_INDEX=$($JQ '.['$iSUB'].i' $STREAMS_SUB | tr -d '"') X_INDEX=$($JQ '.['$iSUB'].i' $STREAMS_SUB | tr -d '"')
X_LANG=$($JQ '.['$iSUB'].lang' $STREAMS_SUB) X_LANG=$($JQ '.['$iSUB'].lang' $STREAMS_SUB)
X_CODEC=$($JQ '.['$iSUB'].codec' $STREAMS_SUB)
printf " %2d: %4s %s\n" $X_INDEX $X_LANG "$X_TITLE" printf " %2d: %4s %s\n" $X_INDEX $X_LANG "$X_TITLE"
if [[ "$X_CODEC" != "ass" ]]; then
printf " format: %s\n" $X_CODEC
fi
done done
echo "" echo ""
return return
@ -342,18 +368,29 @@ function selectSubs() {
export SUBTITLE_INDEX=-1 export SUBTITLE_INDEX=-1
else else
if [[ $OPTS_SELSUB -lt 0 && $SUB_COUNT -gt 1 ]]; then if [[ $OPTS_SELSUB -lt 0 && $SUB_COUNT -gt 1 ]]; then
OPTS_SELSUB_LANG="${OPTS_SELSUB_LANG:-eng}"
LANG_TEST=$($JQ '[.[].lang] | index("'${OPTS_SELSUB_LANG}'")' "$STREAMS_SUB")
echo " > WARNING: Multiple subtitles!" echo " > WARNING: Multiple subtitles!"
printf " Using default selection rules... " printf " Using default selection rules... "
if [[ "$LANG_TEST" == "null" ]]; then OPTS_SELSUB_LANG="${OPTS_SELSUB_LANG:-eng}"
LANG_TEST=$($JQ '[.[].lang | match("'${OPTS_SELSUB_LANG}'")] | length' "$STREAMS_SUB")
if [[ "$LANG_TEST" == "0" ]]; then
echo "English not found!" echo "English not found!"
echo " ==> Reverting to first subtitle file." echo " ==> Reverting to first subtitle file."
export SUBTITLE_INDEX=$($JQ '.[0].i' "$STREAMS_SUB") export SUBTITLE_INDEX=$($JQ '.[0].i' "$STREAMS_SUB")
else elif [[ "$LANG_TEST" == "1" ]]; then
LANG_SINGLE_SELECT=$($JQ '[.[].lang] | index("'${OPTS_SELSUB_LANG}'")' "$STREAMS_SUB")
# we found english # we found english
echo "English found" echo "English found"
export SUBTITLE_INDEX=$($JQ '.['$LANG_TEST'].i' "$STREAMS_SUB") export SUBTITLE_INDEX=$($JQ '.['$LANG_SINGLE_SELECT'].i' "$STREAMS_SUB")
else
# try to avoid a signs and lyrics track
LANG_SINGLE_SELECT=$($JQ '[.[].lang] | index("'${OPTS_SELSUB_LANG}'")' "$STREAMS_SUB")
echo "Multiple english tracks found."
export SUBTITLE_INDEX=$($JQ 'reduce .[] as $trk ([]; if ($trk.lang == "'${OPTS_SELSUB_LANG}'" and (( ($trk.t | test("lyrics";"i")) or ($trk.t | test("signs";"i")) or ($trk.t | test("dub";"i")) )|not) ) then [.[],{t:$trk.t,i:$trk.i}] else . end) | .[].i' "$STREAMS_SUB")
# And display rejected subtitles too.
SUBTITLE_REJECT_LIST=($($JQ 'reduce .[] as $trk ([]; if ($trk.lang == "'${OPTS_SELSUB_LANG}'" and (( ($trk.t | test("lyrics";"i")) or ($trk.t | test("signs";"i")) or ($trk.t | test("dub";"i")) )) ) then [.[],{t:$trk.t,i:$trk.i}] else . end) | .[].t' "$STREAMS_SUB"))
for REJECT_SUB in ${SUBTITLE_REJECT_LIST[@]}; do
echo " > rejecting ${REJECT_SUB}"
done
fi fi
else else
if [[ $SUB_COUNT -eq 1 ]]; then if [[ $SUB_COUNT -eq 1 ]]; then
@ -375,12 +412,14 @@ function selectSubs() {
printf " Using default selection rules... " printf " Using default selection rules... "
if [[ "$LANG_TEST" == "null" ]]; then if [[ "$LANG_TEST" == "null" ]]; then
echo "Japanese audio not found!" echo "Japanese audio not found!"
echo " ==> Reverting to first subtitle file." echo " ==> Reverting to first audio stream."
export AUDIO_INDEX=$($JQ '.[0].i' "$STREAMS_AUDIO") export AUDIO_INDEX=$($JQ '.[0].i' "$STREAMS_AUDIO")
else else
# we found english # we found english
echo "Japanese audio found" echo "Japanese audio found"
export AUDIO_INDEX=$($JQ '.['$LANG_TEST'].i' "$STREAMS_AUDIO") export AUDIO_INDEX=$($JQ '.['$LANG_TEST'].i' "$STREAMS_AUDIO")
AUDIO_STREAM_TITLE=$($JQ '.['$LANG_TEST'].t' "$STREAMS_AUDIO")
echo " ==> stream has title ${AUDIO_STREAM_TITLE}"
fi fi
else else
AUDIO_INDEX=$OPTS_SELAUDIO AUDIO_INDEX=$OPTS_SELAUDIO
@ -409,17 +448,21 @@ function extractSubs() {
function doTranscode() { function doTranscode() {
echo "=> Starting transcode:" echo "=> Starting transcode:"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
echo "$FFMPEG" ${FF_HW} -i "${INPUT_VIDEO}" -sn ${LIM_TIME} \ echo "$FFMPEG" ${FF_VERBOSITY} ${FF_HW} -i "${INPUT_VIDEO}" \
-sn ${LIM_TIME} \
-filter:v "${FILT_PFX}ass=${SUBTITLE_FILE}${FILT_SFX}" \ -filter:v "${FILT_PFX}ass=${SUBTITLE_FILE}${FILT_SFX}" \
${FILT_AUDIO} -c:v "${FF_ENC}" ${FF_STD} ${FF_EXT} ${FF_AUDIO} \ ${FILT_AUDIO} \
-c:v "${FF_ENC}" ${FF_STD} ${FF_EXT} ${FF_AUDIO} \
"${OUTPUT_VIDEO}" "${OUTPUT_VIDEO}"
if [[ "$OPTS_DRYRUN" == true ]]; then if [[ "$OPTS_DRYRUN" == true ]]; then
return return
fi fi
# shellcheck disable=SC2086 # shellcheck disable=SC2086
"$FFMPEG" ${FF_HW} -i "${INPUT_VIDEO}" -sn ${LIM_TIME} \ "$FFMPEG" ${FF_VERBOSITY} ${FF_HW} -i "${INPUT_VIDEO}" \
-sn ${LIM_TIME} \
-filter:v "${FILT_PFX}ass=${SUBTITLE_FILE}${FILT_SFX}" \ -filter:v "${FILT_PFX}ass=${SUBTITLE_FILE}${FILT_SFX}" \
${FILT_AUDIO} -c:v "${FF_ENC}" ${FF_STD} ${FF_EXT} ${FF_AUDIO} \ ${FILT_AUDIO} \
-c:v "${FF_ENC}" ${FF_STD} ${FF_EXT} ${FF_AUDIO} \
"${OUTPUT_VIDEO}" "${OUTPUT_VIDEO}"
export FINAL_STATUS=$? export FINAL_STATUS=$?
} }
@ -477,6 +520,8 @@ OPTS_DEBUG=false
OPTS_LPF_AUDIO=false OPTS_LPF_AUDIO=false
OPTS_TRANS_AUDIO=false OPTS_TRANS_AUDIO=false
OPTS_derived_NO_OUTPUT=false OPTS_derived_NO_OUTPUT=false
OPTS_VERBOSITY=1
OPTS_SURROUND_PRESERVE=false
unset OPT_CRF unset OPT_CRF
# this is the --icon flag passed to notify-send at the end of the transcode # this is the --icon flag passed to notify-send at the end of the transcode
NOTIFY_ICON="face-tired" NOTIFY_ICON="face-tired"
@ -496,7 +541,8 @@ FINAL_STATUS=1
###### ######
# Reformat and organize the input strings # Reformat and organize the input strings
OPT_STRING=$(getopt -o 'hkls:dt:' --long 'help,psoft,soft,dry,crf:,audio,audiofix,alocale:,slocale:' -- "$@") OPT_STRING=$(getopt -o 'hkls:a:dt:vq' \
--long 'help,psoft,soft,dry,crf:,audio,audiofix,alocale:,slocale:,verbose,quiet,keep-surround' -- "$@")
# reassign them as positional arguments # reassign them as positional arguments
eval set -- "$OPT_STRING" eval set -- "$OPT_STRING"
@ -572,6 +618,13 @@ while true; do
shift shift
continue continue
;; ;;
"--keep-surround")
OPTS_SURROUND_PRESERVE=true
OPTS_TRANS_AUDIO=true
echo ">> !! preserving 5.1/7.1 surround sound if available."
shift
continue
;;
"--soft") "--soft")
OPTS_FORCESOFT=true OPTS_FORCESOFT=true
echo ">> !! forcing software decoding/encoding." echo ">> !! forcing software decoding/encoding."
@ -597,6 +650,22 @@ while true; do
shift shift
continue continue
;; ;;
"--verbose"|"-v")
if [[ ${OPTS_VERBOSITY} -ne 0 ]]; then
OPTS_VERBOSITY=2
echo ">> !! print ffmpeg header."
else
echo ">> !! Ignoring verbosity flag. Quiet takes precidence."
fi
shift
continue
;;
"--quiet"|"-q")
OPTS_VERBOSITY=0
echo ">> !! print ffmpeg header."
shift
continue
;;
"--") "--")
shift # all arguments parsed shift # all arguments parsed
break break
@ -608,27 +677,31 @@ while true; do
#echo "TODO: HELP!" # Display HELP #echo "TODO: HELP!" # Display HELP
cat << _EOT cat << _EOT
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
-k auto-klobber when ffmpeg asks -k auto-klobber when ffmpeg asks
-t <t> ffmpeg encoding time limit -t <t> ffmpeg encoding time limit
--crf <#> override CRF setting --crf <#> override CRF setting
--soft force software decode and encode default: $DEFAULTS_OPTS_CRF
--psoft use software encoding (allow hardware decode when available) --soft force software decode and encode
--audiofix transcode audio --psoft use software encoding (allow hardware decode when available)
--lpf transcode audio, and low-pass filter as well --audiofix transcode audio
--keep-surround try to preserve surround sound rather than downmixing to stereo.
--lpf transcode audio, and low-pass filter as well
-l list subtitles and audio tracks (no encoding) -l list subtitles and audio tracks (no encoding)
-s <#> select specific subtitle track number -s <#> select specific subtitle track number
#TODO: verify legal subtitle track number convention #TODO: verify legal subtitle track number convention
-a <#> select specific audio track number -a <#> select specific audio track number
#TODO: verify legal audio track number convention #TODO: verify legal audio track number convention
--slocale 'eng' select specific subtitle track number --slocale 'eng' select specific subtitle track number
#TODO: verify legal subtitle track number convention #TODO: verify legal subtitle track number convention
--alocale 'jpn' select specific audio track number --alocale 'jpn' select specific audio track number
#TODO: verify legal audio track number convention #TODO: verify legal audio track number convention
-d debug (no cleanup) -d debug (no cleanup)
--dry dry run (no encoding) --dry dry run (no encoding)
-q, --quiet make ffmpeg shutup
-v, --verbose show all FFMPEG details (except that ruddy header)
_EOT _EOT
exit exit
;; ;;
@ -654,6 +727,19 @@ else
exit exit
fi fi
# Actual verbosity parsing
if [[ ${OPTS_VERBOSITY} -le 0 ]]; then
FF_VERBOSITY="-hide_banner -loglevel error"
elif [[ ${OPTS_VERBOSITY} -eq 1 ]]; then
FF_VERBOSITY="-hide_banner -loglevel error -stats"
else # ie [[ ${OPTS_VERBOSITY} -ge 2 ]]; then
FF_VERBOSITY="-hide_banner"
fi
if [[ "$OPTS_DEBUG" == "true" ]]; then
set -x
fi
############### ###############
# Configure the encoder based upon the hostname # Configure the encoder based upon the hostname
machineSetup machineSetup
@ -682,6 +768,10 @@ fi
selectSubs selectSubs
# extract the selected subtitle file # extract the selected subtitle file
extractSubs $SUBTITLE_INDEX extractSubs $SUBTITLE_INDEX
# Configure the audio straem
setupAudioTranscode
# Set the bitrate if that function wasn't disabled # Set the bitrate if that function wasn't disabled
runExtraProc "h264_vaapi" runExtraProc "h264_vaapi"