Convert GIF to MP4 for Instagram with FFmpeg
I made a script to account for Instagram’s idiosyncrasies when converting a GIF to MP4 using FFmpeg. Here it is:
And now the details…
Doing a conversion of GIF to MP4 with FFmpeg seems like it should be simple enough:
ffmpeg -i something.gif out.mp4
But it isn’t! This can be insufficient in a couple of ways..
Problem 1: Video too short
Solution: Use a filter to loop the input enough times to meet the 3s minimum time requirement
-filter_complex "loop=<NUMBER_OF_LOOPS>"
Problem 2: Wrong color encoding
Given a color encoding that it doesn’t understand, Instagram just kinda poops out :/
Solution: By default, my FFmpeg used yuv444p
, which Instagram wasn’t happy with. I haven’t done an exhaustive survey of the color encoding that Instagram will accept, but here is one: yuv420p
.
-pix_fmt yuv420p
In addition, the conversion requires the file’s height to be divisible by 2, so we need yet another filter:
-filter_complex "scale=trunc(iw/2)*2:trunc(ih/2)*2"
Automation
Now since so many GIFs that I wish to post to Instagram are actually shorter than 3s, I automated everything above and here is the script. To see if I made any changes since posting this, check the version I’m currently using in my dotfiles.
#!/usr/bin/env bash
# Required:
# bc
# ffmpeg
# wget
set -e
# Usage
if [ $# -eq 0 ]; then
cat <<-EOF
Usage: $0 infile outfile
Convert a gif to mp4 with ffmpeg, looping it enough times to ensure it meets
Instagram's minimum video length limit.
infile | A valid gif file to convert. If given a URI, this script will
try to download it for you and then convert it.
outfile | A target filename for the result. If the output filename is not
specified, it will be placed alongside the input file with the
extension '.insta.mp4' added.
EOF
exit 1
fi
# MAIN
INFILE=$1
OUTFILE=$2
MINTIME=3
# Download the input file if given a URI
urire='(https?|ftp|file)://[-A-Za-z0-9\+&@#/%?=~_|!:,.;]*[-A-Za-z0-9\+&@#/%=~_|]'
if [[ "${INFILE}" =~ "${urire}" ]]; then
filename=$(basename "${INFILE}")
wget -O "${filename}" "${INFILE}"
INFILE="${filename}"
fi
# Determine output filename
if [ -z "${OUTFILE}" ]; then
OUTFILE=$(echo "${INFILE}" | sed 's/\.gif$//')
OUTFILE="${OUTFILE}.insta.mp4"
fi
# Check if input is gif
if [[ $(file "${INFILE}") != *GIF* ]]; then
echo "Input file '${INFILE}' is not a gif. Quitting."
exit 1
fi
# Get info on the gif
# (A simple -i or ffprobe would be sufficient to get the information we need for
# a video file, but not a gif. Basically as far as I can tell you need to
# process the file in a manner similar to this to get the frame count, and it
# then also provides us with the fps which we need anyway)
ffoutput=$(ffmpeg -i "${INFILE}" -f null /dev/null 2>&1)
framecount=$(echo "${ffoutput}" | grep -Po 'frame=\s+[0-9]+\s+' | egrep -o '[0-9]+')
vstreaminfo=$(echo "${ffoutput}" | grep -P 'Video:\s+gif')
fps=$(echo "${vstreaminfo}" | grep -Po '[0-9]+(?=\s+fps)')
# dims=$(echo "${vstreaminfo}" | grep -Po '[0-9]+x[0-9]+')
# w=$(echo "${dims}" | cut -d 'x' -f 1)
# h=$(echo "${dims}" | cut -d 'x' -f 2)
len=$(echo "${framecount} / ${fps}" | bc -l | sed 's/0\{1,\}$//')
if (( $(echo "${len} >= 60" | bc -l ) == 1 )); then
echo "Clip is too long for Instagram! The limit is 60s. Quitting."
exit 1
fi
# How many loops do we need to meet instagram's min time? Round up with awk.
loops=$(echo "${MINTIME} / ${len}" | bc -l | awk '{print ($0-int($0)>0)?int($0)+1:int($0)}')
# Convert
ffmpeg -i "${INFILE}" \
-filter_complex "loop=${loops}:32767:0,scale=trunc(iw/2)*2:trunc(ih/2)*2" \
-f mp4 \
-y \
-preset slow \
-pix_fmt yuv420p \
"${OUTFILE}" >/dev/null 2>&1
Usage
# To convert `yup.gif` to `yup.mp4`
instagif yup.gif yup.mp4
# To convert yup.gif into yup.insta.mp4
instagif yup.gif
# To create an insta.mp4 directly from a remote GIF
instagif https://media.giphy.com/media/lgcUUCXgC8mEo/giphy.gif insta.mp4
Happy giffing! :-D