Skip to content

Instantly share code, notes, and snippets.

@Tynach
Last active September 14, 2024 16:33
Show Gist options
  • Save Tynach/b296cc6502808a57955e410c36f5ce83 to your computer and use it in GitHub Desktop.
Save Tynach/b296cc6502808a57955e410c36f5ce83 to your computer and use it in GitHub Desktop.
Uses FFMPEG to convert any file FFMPEG understands into a .mp4 file that Telegram will consider as a 'gif', at higher quality than Telegram converts gifs to itself.
#!/bin/bash
# Settings
outfolder="converted"
codec="libx264"
maxres="448"
#filter="crop=floor(in_w/2)*2:floor(in_h/2)*2:0:0:exact=1,colorspace=bt601-6-625:range=pc:irange=tv:iall=bt601-6-625:format=yuv444p12,zscale=rin=full:tin=601:t=linear,scale=if(gt(iw\,ih)\,min($maxres\,floor((iw+1)/2)*2)\,-2):if(gt(iw\,ih)\,-2\,min($maxres\,floor((ih+1)/2)*2)),zscale=rin=full:r=limited:tin=linear:t=601"
filter="scale=if(gt(iw\,ih)\,min($maxres\,floor((iw+1)/2)*2)\,-2):if(gt(iw\,ih)\,-2\,min($maxres\,floor((ih+1)/2)*2)):out_color_matrix=bt601:out_range=tv:flags=accurate_rnd+full_chroma_inp+full_chroma_int+bicublin"
preset="veryslow"
profile="high"
tune="film"
quality="18"
# Input Parameters
inparams=(
"-loglevel" "quiet"
"-stats"
)
# Output Parameters
outparams=(
"-flags" "+global_header"
"-movflags" "faststart"
"-c:v" "$codec"
"-profile:v" "$profile"
#"-tune:v" "$tune"
"-bf" "0"
"-copyts"
"-avoid_negative_ts" "disabled"
"-correct_ts_overflow" "0"
"-pix_fmt" "yuv420p"
"-color_primaries" "bt709"
"-color_trc" "iec61966_2_1"
"-colorspace" "bt470bg"
"-color_range" "tv"
)
# Make a folder if it doesn't already exist.
function make_folder
{
local folder="$1"
if [ ! -d "$folder" ]; then
mkdir "$folder"
fi
}
# Move files to where they belong.
function detect_2pass
{
local file="$1"
local converted="$outfolder/$2"
if [ ! -f "$converted" ]; then
echo "$2 was not created!"
return
fi
local size="$(stat -c%s "$converted")"
if [ "$size" -gt 10485760 ]; then
needs2pass+=("$file")
fi
}
function framerate
{
# If the file is a gif
if [ "${file##*.}" == "gif" ]; then
# Determine if the gif has a constant framerate
local cfr="$(identify -format "%T\n" "$1" | awk 'BEGIN{getline;d=$0;if($0==0){d=10}c=1} {t=$0;if(t==0){t=10}if(t==d){next}else{c=0;exit}} END{printf("%d",c)}')"
# If the framerate is variable
if [[ "$cfr" -eq 0 ]]; then
# Gets double the FPS based on the most commonly used frame duration.
#fps=$(identify -format "%T\n" "$1" | awk 'max<++c[$0]{if($0==0){max=c[10];line=10}else{max=c[$0];line=$0}} END{printf("%.2f",200/line)}')
# Gets double the minimum framerate.
#fps=$(identify -format "%T\n" "$1" | awk 'BEGIN{getline;m=$0} {d=$0;if($0==0){d=10}if(d<m){m=d}} END{printf("200/%d",m)}')
# Get size and duration of original gif
local size="$(identify -format "%wx%h" "$1[0]")"
local duration="$(identify -format "%T\n" "$1" | awk '{t+=$0;f++;if($0==0){t+=10}} END{printf("%.2f",t/100)}')"
# Fix inaccurate gif duration
# Commented out version had fixed some bug in the past related to off-by-one-frame duration issues.
# I'm not seeing this anymore though, so I've made a simpler version that *seems* to work just fine.
filter="color=s=$size:d=$duration:r=$fps:c=white[b];[0:v]fps=$fps:round=down[f];[b][f]overlay,$filter"
#local filter="color=c=white:s=$size:d=$duration:r=$fps,select=between(t\,0\,$duration-(1/$fps))[b];[0:v]fps=$fps,select=between(t\,0\,$duration-(1/$fps))[f];[b][f]overlay,$filter"
# Constant framerate
else
fps="$(identify -format "%T\n" "$1[0]" | awk '{d=$0;if(d==0){d=10}printf("100/%d",d)}')"
fi
if [[ "$fps" != *"/"* ]]; then
fps="${fps}/1"
fi
local tb="${fps#*/}/${fps%/*}"
outparams+=(
"-r" "$fps"
"-vsync" "cfr"
"-time_base" "$tb"
"-enc_time_base" "$tb"
"-video_track_timescale" "$fps"
)
else
duration="$(ffprobe -i "$1" -select_streams v:0 -show_entries stream=duration_ts -v quiet -of default=noprint_wrappers=1:nokey=1)"
frames="$(ffprobe -i "$1" -select_streams v:0 -show_entries stream=nb_frames -v quiet -of default=noprint_wrappers=1:nokey=1)"
tb="$(ffprobe -i "$1" -select_streams v:0 -show_entries stream=time_base -v quiet -of default=noprint_wrappers=1:nokey=1)"
#ctb="$duration/$(($frames * ${tb#*/}))"
#fps="$(($frames * ${tb#*/}))/$duration"
local test="$(ffprobe -i "$1" -select_streams v:0 -show_entries stream=duration -v quiet -of default=noprint_wrappers=1:nokey=1)"
if [ "$test" == "N/A" ]; then
fps="$(ffprobe -i "$1" -select_streams v:0 -show_entries stream=r_frame_rate -v quiet -of default=noprint_wrappers=1:nokey=1)"
else
fps="$(ffprobe -i "$1" -select_streams v:0 -show_entries stream=time_base -v quiet -of default=noprint_wrappers=1:nokey=1 | sed -r 's/([0-9]+)\/([0-9]+)/\2\/\1/')"
fi
outparams+=(
#"-time_base" "$tb"
#"-enc_time_base" "-1"
"-video_track_timescale" "$fps"
"-vsync" "passthrough"
)
fi
}
# Convert all files with standard settings.
function conv
{
local outparams=("${outparams[@]}")
# Set fallback framerate of 25.
local fps="25"
local filter="$filter"
local tags=""
framerate "$1"
# Qualaity and preset are different for 2-pass encoding, so can't keep them in the base array.
# Filter gets changed by other parameters, so can't be either.
outparams+=(
"-qp" "$quality"
"-preset" "$preset"
"-lavfi" "$filter"
)
if [ -f "backup/$2" ]; then
if [ "$(attr "backup/$2" -ql)" == "xdg.tags" ]; then
tags="$(attr "backup/$2" -qg xdg.tags)"
fi
fi
echo "$1 -> $2"
ffmpeg "${inparams[@]}" -i "$1" "${outparams[@]}" -an "$outfolder/$2"
if [ "$tags" != "" ]; then
attr "$outfolder/$2" -qs xdg.tags -V "$tags"
fi
detect_2pass "$1" "$2"
}
# Attempt 2-pass conversion on files that are too large.
function run_2pass
{
if [ "${1##*.}" == "gif" ]; then
local duration="$(identify -format "%T\n" "$1" | awk '{t+=$0;f++;if($0==0){t+=10}} END{printf("%.2f",t/100)}')"
else
local duration="$(ffprobe -i "$1" -select_streams v:0 -show_entries format=duration -v quiet -of csv=p=0)"
fi
local bitrate="$((49807360/(625*${duration%.*})))k"
local outparams=("${outparams[@]}")
local fps="25"
local tags=""
framerate "$1"
outparams+=(
"-passlogfile" "pass/ffmpeg2pass"
"-b:v" "$bitrate"
"-preset" "placebo"
"-lavfi" "$filter"
)
if [ -f "backup/$2" ]; then
if [ "$(attr "backup/$2" -ql)" == "xdg.tags" ]; then
tags="$(attr "backup/$2" -qg xdg.tags)"
fi
fi
make_folder "pass"
echo "$1 -> $2; pass 1"
ffmpeg "${inparams[@]}" -i "$1" "${outparams[@]}" -pass 1 -an -y -f mp4 /dev/null
echo "$1 -> $2; pass 2"
ffmpeg "${inparams[@]}" -i "$1" "${outparams[@]}" -pass 2 -an -y "$outfolder/$2"
if [ "$tags" != "" ]; then
attr "$outfolder/$2" -qs xdg.tags -V "$tags"
fi
detect_2pass "$1" "$2"
}
needs2pass=()
make_folder "$outfolder"
shopt -s nullglob
for file in *.*; do
shopt -u nullglob
converted="${file%.*}.mp4"
if [ -f "$outfolder/$converted" ]; then
detect_2pass "$file" "$converted"
else
conv "$file" "$converted"
fi
done
for file in "${needs2pass[@]}"; do
converted="${file%.*}.mp4"
needs2pass=("${needs2pass[@]:1}")
run_2pass "$file" "$converted"
done
if [ ${#needs2pass[@]} -gt 0 ]; then
make_folder "$outfolder/toobig"
for failed in "${needs2pass[@]}"; do
converted="${failed%.*}.mp4"
mv "$outfolder/$converted" "$outfolder/toobig/$converted"
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment