Skip to content

Instantly share code, notes, and snippets.

@cclecle
Last active April 4, 2023 21:10
Show Gist options
  • Save cclecle/5c74014b3226cd259c1e19459d78fd98 to your computer and use it in GitHub Desktop.
Save cclecle/5c74014b3226cd259c1e19459d78fd98 to your computer and use it in GitHub Desktop.
make a video smooth (60fp) using ffmpeg
#!/usr/bin/env bash
##############################################################
# Demo script to make a any-fps movie to 60fps
#
# Usage: > ./smoothify.sh <FilePath> (<tmpdir>)
#
# Output file will be the input one _SMOOTHED, at the same dir
#
#########################################
set -e # stop on first error
#set -x
Required_commands=("ffmpeg" "awk" "sed" "ffprobe" "getconf")
for cmd in "${commands[@]}"
do
if ! command -v "$cmd" >/dev/null 2>&1 ; then
echo "Command '$cmd' not available, please install it."
exit 1
fi
done
get_numberOfCPU(){
getconf _NPROCESSORS_ONLN
}
get_keyFrames(){
local FileName="$1"
ffprobe -select_streams v:0 -skip_frame nokey -v quiet -of default=noprint_wrappers=1 -show_entries frame=best_effort_timestamp_time "$FileName" | sed 's/best_effort_timestamp_time=//'
}
get_FrameRate(){
local FileName="$1"
ffprobe -select_streams v:0 -v quiet -of default=noprint_wrappers=1 -show_entries stream=r_frame_rate "$FileName" | sed 's/r_frame_rate=//'
}
get_FrameRateNumber(){
local FileName="$1"
FrameRate=$(get_FrameRate "$FileName")
SegmentSize=$(awk "BEGIN { print ($FrameRate) }")
echo "$SegmentSize"
}
get_FrameTime(){
local FileName="$1"
FrameRate=$(get_FrameRate "$FileName")
SegmentSize=$(awk "BEGIN { print (1/($FrameRate)) }")
echo "$SegmentSize"
}
get_NearestTime(){
local RequestedTime="$1"
shift #to reference dataset array at first arg position
local DataSet=("$@"); IFS=: # IFS is used to delimit array
# using awk to process the array in one command (using awk in a loop is too slow)
echo "$RequestedTime" | awk -v dataset="${DataSet[*]}" '{
n_dataset=split(dataset, _dataset, "[:]")
for (i=1; i<=length(_dataset); i++)
{
if ($RequestedTime < _dataset[i])
{
printf "%s\n%s",_dataset[i],_dataset[i]
exit
}
}
print "/!\ Can not find a NearestTime" > "/dev/stderr"
exit 1
}'
}
File="$1"
echo "using File: $File"
FileOut=$( echo "$File" | sed 's|\([^/]*\)\.\([^.]*\)$|\1_SMOOTHED.\2|g')
echo "using FileOut: $FileOut"
NumberOfCut=$(get_numberOfCPU)
TmpDirEx=$(mktemp -d)
TmpDir=${2:-$TmpDirEx}
echo "Using TmpDir: $TmpDir"
TmpConcatList=$(mktemp)
EncodeNice=10
tagetFPS=48
function finish {
rm -Rf $TmpDirEx || true #/!\ DO NOT rm user provided $TmpDir cause user can make mistake
rm "$TmpConcatList" || true
for (( CutIndex=0; CutIndex<"${#SegmentKeyFrames[@]}"; CutIndex=CutIndex+2 )); do
rm "$TmpDir/chunk_$CutIndex.mp4" >/dev/null 2>&1 || true
done
}
trap finish EXIT
#TranscodeOpt="-c:v copy" # <= DO NOT DO THAT ! codec have to be reencoded to allow time cut ... :-/
# normal transcode
TranscodeOptBase="-c:v libx264 -preset slower -tune film -crf 16"
# very fast
#TranscodeOptBase="-c:v libx264 -preset faster -crf 30"
# super cool but super slow
TranscodeOptFilter="-filter:v minterpolate=fps=$tagetFPS:scd=none:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:me=epzs:search_param=16:vsbmc=1"
# well.. do not change that much
#TranscodeOptFilter="-filter:v tblend -r $tagetFPS"
# disable
#TranscodeOptFilter=""
TranscodeOpt="$TranscodeOptBase $TranscodeOptFilter"
FrameTime=$(get_FrameTime "$File")
echo "[DEBUG] FrameTime is $FrameTime"
FrameRateNumber=$(get_FrameRateNumber "$File")
echo "[DEBUG] FrameRateNumber is $FrameRateNumber"
FrameRate=$(get_FrameRate "$File")
echo "[DEBUG] FrameRate is $FrameRate"
echo "[INFO] Expected number of cuts: $NumberOfCut"
echo "[INFO] Extracting Keyframes..."
mapfile -t keyframes < <(get_keyFrames "$File")
echo "[INFO] Done"
FirstValue=0
LastValue="${keyframes[-1]}"
echo "[INFO] Cutting file from $FirstValue sec to $LastValue sec, in $NumberOfCut segments."
SegmentSize=$(awk -v var1="$LastValue" -v var2="$NumberOfCut" 'BEGIN { print ( var1 / var2 ) }')
echo "[INFO] Computed approx segments size is $SegmentSize sec"
SegmentKeyFrames=(0)
for (( CutIndex=1; CutIndex<"$NumberOfCut"; CutIndex++ )); do
TargetKeyFrame=$(awk -v var1="$CutIndex" -v var2="$SegmentSize" 'BEGIN { print ( var1*var2 ) }')
echo "[DEBUG] TargetKeyFrame is $TargetKeyFrame"
SegmentKeyFrames+=($(get_NearestTime "$TargetKeyFrame" "${keyframes[@]}"))
done
SegmentKeyFrames+=("$LastValue")
echo "[INFO] Done."
echo "[INFO] Final SegmentKeyFrames are:"
for keytime in "${SegmentKeyFrames[@]}"; do
echo "[INFO] $keytime"
done
echo "[INFO] Cutting input file."
pids=""
RESULT=0
for (( CutIndex=0; CutIndex<"${#SegmentKeyFrames[@]}"; CutIndex=CutIndex+2 )); do
echo "[DEBUG] Processing Section from ${SegmentKeyFrames[CutIndex]} to ${SegmentKeyFrames[CutIndex+1]}..."
Duration=$(awk -v var1="${SegmentKeyFrames[CutIndex+1]}" -v var2="${SegmentKeyFrames[CutIndex]}" 'BEGIN { print ( var1-var2 ) }')
echo "[DEBUG] Duration is $Duration"
nice -n "$EncodeNice" ffmpeg -ss "${SegmentKeyFrames[CutIndex]}" -to "${SegmentKeyFrames[CutIndex+1]}" -i "$File" -map 0 -fflags +genpts -avoid_negative_ts 1 $TranscodeOpt "$TmpDir/chunk_$CutIndex.mp4" &>/dev/null &
pids="$pids $!"
done
echo "[INFO] Waiting Jobs to finish"
for pid in $pids; do
wait $pid || let "RESULT=1"
done
if [ "$RESULT" == "1" ];
then
exit 1
fi
echo "[INFO] Done."
echo "[INFO] Reconstructing file."
for (( CutIndex=0; CutIndex<"${#SegmentKeyFrames[@]}"; CutIndex=CutIndex+2 )); do
Duration=$(awk -v var1="${SegmentKeyFrames[CutIndex+1]} " -v var2="${SegmentKeyFrames[CutIndex]}" 'BEGIN { print ( var1-var2 ) }')
echo "file $TmpDir/chunk_$CutIndex.mp4" >> "$TmpConcatList"
echo "duration $Duration" >> "$TmpConcatList"
done
ffmpeg -f concat -safe 0 -i "$TmpConcatList" -c copy "$FileOut"
echo "[INFO] Done."
echo "[INFO] Cleaning"
echo "[INFO] Done."
echo "[INFO] Finished !"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment