Last active
April 4, 2023 21:10
-
-
Save cclecle/5c74014b3226cd259c1e19459d78fd98 to your computer and use it in GitHub Desktop.
make a video smooth (60fp) using 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
#!/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