Christopher Peterson

February 13, 2018

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

instagram "video too short" notification

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

Instagram blank video preview

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"


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.
  exit 1


# Download the input file if given a URI
if [[ "${INFILE}" =~ "${urire}" ]]; then
  filename=$(basename "${INFILE}")
  wget -O "${filename}" "${INFILE}"

# Determine output filename
if [ -z "${OUTFILE}" ]; then
  OUTFILE=$(echo "${INFILE}" | sed 's/\.gif$//')

# Check if input is gif
if [[ $(file "${INFILE}") != *GIF* ]]; then
  echo "Input file '${INFILE}' is not a gif. Quitting."
  exit 1

# 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

# 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


# 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 insta.mp4

Happy giffing! :-D