Skip to content

Instantly share code, notes, and snippets.

@anttiryt
Forked from majal/minterpolate
Last active May 5, 2024 14:04
Show Gist options
  • Save anttiryt/800a53e17732a54d6278fc9068adfe07 to your computer and use it in GitHub Desktop.
Save anttiryt/800a53e17732a54d6278fc9068adfe07 to your computer and use it in GitHub Desktop.
Multi-threaded minterpolate in ffmpeg
#!/bin/bash
# Multicore minterpolate in ffmpeg
# Just slice & process & concat
# NB: The concat points between slices may be weird
# Default arguments
bv=1
ff=/usr/bin/ffmpeg
fps=60
multip=30 # 30% increase in b/v when FPS doubles
nice=0
dur=00:00:10
dur=max
enc=libx264
[[ "${OSTYPE}" == "darwin"* ]] && task=$(( $(sysctl -n hw.logicalcpu) )) || task=$(( $(nproc --all) ))
# Reference: https://ffmpeg.org/ffmpeg-filters.html#minterpolate
m_args=":mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1"
ff_args=( -hide_banner -loglevel level+warning -nostats -y )
ff_args_enc=( -crf 20 -preset superfast -movflags +faststart )
################################################################################
# Usage notes
err_args () {
printf "\nUsage: %s -i INPUT_FILE [ARGUMENTS] OUTPUT_FILENAME\n" "$(basename "${0}")"
printf "\n"
printf "Arguments:\t\t\t\t\t[default values]\n"
echo
printf "\t-b str\tffmpeg binary path\t\t\t[%s]\n" "${ff}"
printf "\t-c int\tCalculate output bitrate from input\t[%d]\n" "${bv}"
printf "\t-d hms\tSlices duration [hour:min:sec]\t\t[%s]\n" "${dur}"
printf "\t-e str\tEncoder name\t\t\t\t[%s]\n" "${enc}"
printf "\t-f str\tffmpeg encoding arguments\t\t[%s]\n" "'${ff_args_enc[*]}'"
printf "\t-n int\tNice value\t\t\t\t[%s]\n" "${nice}"
printf "\t-p int\tProcess/thread count. 0 is max\t\t[%s]\n" "${task}"
printf "\t-r int\tTarget FPS\t\t\t\t[%s]\n" "${fps}"
printf "\t-m str\tminterpolate arguments\t\t\t[%s]\n" "'${m_args}'"
printf "\t-s hms\tStart point of source video\t\t[00:00:00]\n"
printf "\t-t hms\tEnd point of source video\t\t[until end of the video]\n"
echo
exit 1
}
# Clean slices and temporary files
clean () {
rm -f _cut.mp4
rm -f _s_?????.mp4
rm -f _m__s_?????.mp4
rm -f _list.txt
}
# calculate b:v from the original
calculate_b_v () {
if [ ! "${bv}" == 1 ] ; then
return
fi
# a few checks
if [ ! "${ss}"x == x ] ; then
echo "Calculate incompatible with begin time, skipping calculate"
return
fi
if [ ! "${to}"x == x ] ; then
echo "Calculate incompatible with end time, skipping calculate"
return
fi
if [ ! -e "${in}" ] ; then
echo "Cannot open INPUT_FILE ${in}, exiting.."
exit
fi
# Get FPS from source video
# fps count from https://stackoverflow.com/a/35404908/468921
origFPS=$(ffprobe -v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=avg_frame_rate ${in} 2> /dev/null)
if [ "${origFPS}" == "" ] ; then
return;
fi
# Get source file b/v
origRate=$(ffprobe -v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=bit_rate ${in} 2> /dev/null)
if [ "${origFPS}" == "" ] ; then
return;
fi
# Calculate new b/v
newFPS=${fps}
# version1 - Doubles the b/v when FPS doubles
#let 'newRate=newFPS/origFPS*origRate'
# version2 - quite close but floats not really supported by bash without bc
#let 'newRate=(((fpsAdd/origFPS)*multip/100)+1)*origRate'
# version3 - somewhere quite close to version 2.
#let 'newRate=((fpsAdd/origFPS)*multip*origRate/100)+origRate'
# fetch fps addition
let 'fpsAdd=newFPS-origFPS';
# calculate with version3
let 'newRate=((fpsAdd/origFPS)*multip*origRate/100)+origRate'
if [[ "${newRate}" == "" ]] ; then
echo "Mismatch in calculating new b:v rate, skipping parameter"
return;
fi
#echo "FPS $origFPS -> $newFPS"
#echo "Bytes/sec calculated $origRate -> $newRate"
# Clear old ff_args_enc
ff_args_enc=( -crf 20 -preset superfast -movflags +faststart )
for i in ${!ff_args_enc[@]}; do
ff_args_enc[$i]=""
done
# Create new ff_args_enc
ff_args_enc=( -preset superfast -movflags +faststart -b:v ${newRate} )
return
}
# Maximum slice time, e.g. 4 slices on 4-cpu machine
calculate_dur_max() {
if [ ! "${dur}" == "max" ] ; then
return
fi
if [ "${temp}" == "_cut.mp4" ] ; then
echo "dur max not compatible with -ss or -to, skipping dur max"
return
fi
# Get source file duration
srcDuration=$(ffprobe -v error -select_streams v -of default=noprint_wrappers=1:nokey=1 -show_entries stream=duration ${in} | cut -d. -f1 2> /dev/null)
if [ "${srcDuration}" == "" ] ; then
echo "Unable to get source duration, skipping dur max parameter"
return;
fi
# if no (v)cpu count set use nproc
if [[ "${task}" == "" ]] ; then
task=$(nproc --all)
fi
# srcDuration divided by (v)CPU + 1
let dur='srcDuration / task + 1'
return
}
# Main start
while getopts 'i:r:s:t:e:p:d:b:m:n:c:f:' OPTION; do
case $OPTION in
i)
in=$OPTARG # input file
;;
r)
fps=$OPTARG # target fps, fraction is also OK
;;
s)
ss=$OPTARG # begin time of input
;;
t)
to=$OPTARG # end time of input
;;
e)
enc=$OPTARG # encoder
;;
p)
task=$((OPTARG)) # process count, xargs use max when it is 0
;;
d)
dur=$OPTARG # parallel block length
;;
b)
ff=$OPTARG # ffmpeg binary
;;
m)
m_args=$OPTARG # minterpolate arguments
;;
n)
nice=$OPTARG # nice value
;;
c)
bv=$OPTARG # calculate target bytes/sec
;;
f)
# clear old ff_args_enc array
for i in ${!ff_args_enc[@]}; do
ff_args_enc[$i]=""
done
ff_args_enc=$OPTARG # ffmpeg encoding arguments
;;
*)
err_args
;;
esac
done
shift $((OPTIND - 1))
# Set output file
out="${1}"
# Select input file
temp="${in}"
# Check arguments and files
if [ -z "${in}" ]; then
echo
echo "$(basename ${0}): missing input file"
err_args
fi
if [ -z "${out}" ]; then
echo
echo "$(basename ${0}): missing output filename"
err_args
fi
# Set start and end duration for minterpolation
duration=()
if [ -n "${ss}" ]; then
duration+=(-ss)
duration+=("${ss}")
temp=_cut.mp4
fi
if [ -n "${to}" ]; then
duration+=(-to)
duration+=("${to}")
temp=_cut.mp4
fi
# set nice str
nicestr="/usr/bin/nice -n ${nice} "
# calculate b:v
calculate_b_v
# calculate maximum duration
calculate_dur_max
################################################################################
clean
echo
echo $(date +%X)" Processing ${in} ..."
echo
# Use origin file or cut a part from origin file
if [ "${temp}" == _cut.mp4 ]; then
"${ff}" "${ff_args[@]}" "${duration[@]}" -i "${in}" -c copy "${temp}"
fi
# Make slices for parallel use
"${ff}" "${ff_args[@]}" -i "${temp}" -c copy -f segment -segment_time "${dur}" '_s_%05d.mp4'
xargs_cmd="${nicestr} ${ff} ${ff_args[@]} -i _fname -vf minterpolate=fps=${fps}${m_args} -c:a copy -c:v ${enc} ${ff_args_enc[@]} _m__fname"
# Minterpolate the slices. Get coffee, go out and exercise, or sleep. This will a while.
find . -name '_s_?????.mp4' | sed 's@^.*/@@g' | xargs -I "_fname" -t -P ${task} -- ${xargs_cmd} && echo "Processed slice: $(ls _m__s_?????.mp4 | wc -l) of $(ls _s_?????.mp4 | wc -l) ..."
# Make a list of minterpolated slices
printf "file '%s'\n" ./_m__s_?????.mp4 > _list.txt
# Concatenate the result
"${ff}" "${ff_args[@]}" -f concat -safe 0 -i _list.txt -c copy "${out}"
echo
echo $(date +%X)" Processing of ${in} complete. Created ${out}."
echo
echo "################################################################################"
echo
clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment