-
-
Save anttiryt/800a53e17732a54d6278fc9068adfe07 to your computer and use it in GitHub Desktop.
Multi-threaded minterpolate in ffmpeg
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
#!/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