FFmpeg GIF to MP4: Convert Animated GIFs to Video

March 23, 2026 ยท RenderIO

The command (and why the obvious one breaks)

If you search "ffmpeg gif to mp4," every result gives you the same one-liner:

ffmpeg -i animation.gif output.mp4

This works. Sort of. FFmpeg reads the GIF, encodes H.264 video, writes an MP4. But play it back on an Android phone or in Safari and you might get a black screen, a green mess, or no playback at all.

The problem is pixel format. GIFs store color as RGB with a 256-color palette. H.264 video uses YUV color space, and most players expect a specific variant: yuv420p. Without telling FFmpeg to convert to that format, it picks whatever it thinks is closest (often yuv444p or pal8) and half the devices on the planet refuse to play it.

Here is the command that actually works everywhere:

ffmpeg -i animation.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  output.mp4

Three flags, three problems solved. Let me break them down.

What each flag does

-pix_fmt yuv420p forces the output to use the YUV 4:2:0 pixel format. This is the format that every browser, phone, and video player supports. Skip it and your MP4 will play fine in VLC but fail on iOS Safari, Android Chrome, and most social media platforms. The tradeoff is that yuv420p has lower color fidelity than the source GIF. Colors get slightly desaturated. For most GIFs (memes, UI animations, screen recordings) you won't notice.

-movflags faststart moves the MP4's metadata (the "moov atom") from the end of the file to the beginning. Without this, a browser has to download the entire file before it can start playing. With it, playback begins as soon as the first few kilobytes arrive. Always include this for anything going on the web.

-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" is the weird-looking one. H.264 requires even dimensions, meaning both width and height must be divisible by 2. GIFs don't have this constraint. A 363x400 GIF will crash the encoder with width not divisible by 2. This filter takes whatever the input dimensions are, rounds them down to the nearest even number, and moves on. You lose at most 1 pixel on each axis.

You can verify the conversion worked correctly with ffprobe:

ffprobe -v error -show_entries stream=codec_name,pix_fmt,width,height \
  -of default=noprint_wrappers=1 output.mp4

Expected output:

codec_name=h264
pix_fmt=yuv420p
width=480
height=360

If pix_fmt shows anything other than yuv420p, you'll have compatibility problems.

Controlling quality with CRF

The default command produces reasonable quality, but FFmpeg picks its own encoding settings. If you want control over the quality-vs-size tradeoff, use CRF (Constant Rate Factor):

ffmpeg -i animation.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -c:v libx264 -crf 18 -preset medium \
  output.mp4

CRF runs from 0 (lossless, huge files) to 51 (unwatchable). The default is 23. For GIF-to-MP4 conversions, I usually drop it to 18 or 20 because GIFs are already low-fidelity and you don't want to lose more quality in the conversion. The file size difference between CRF 18 and 23 is small since GIF source material isn't complex enough for the encoder to work hard.

CRFQuality levelWhen to use it
17-18Near-losslessPreserving every detail from the source GIF
20HighGeneral use, good balance
23Medium (default)When file size matters more than perfect fidelity
28+LowPreviews, thumbnails, bandwidth-constrained delivery

The video compression guide goes deeper on CRF values, presets, and two-pass encoding if you want to squeeze every byte.

Choosing a codec: H.264 vs H.265 vs AV1

H.264 (libx264) is the safe default. It plays on everything. But for GIF-to-MP4 conversions specifically, the newer codecs can be worth considering.

H.265 (libx265) produces files 30-50% smaller than H.264 at the same visual quality. Browser support is solid in 2026: Chrome, Safari, and Edge all handle it, and Firefox added support in late 2024.

ffmpeg -i animation.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -c:v libx265 -crf 24 -preset medium -tag:v hvc1 \
  output.mp4

The -tag:v hvc1 is required for Safari and iOS playback. Without it, Apple devices won't play the file even though they support HEVC. The transcoding guide has a full breakdown of when H.265 is worth the slower encoding time.

AV1 (libsvtav1) compresses even further, roughly 30% smaller than H.265. Encoding is slower, but for GIF content (low resolution, short duration, limited colors), AV1 encoding finishes in seconds. Browser support is universal in 2026.

ffmpeg -i animation.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -c:v libsvtav1 -crf 30 -preset 6 \
  output.mp4

AV1's CRF scale is different from H.264. CRF 30 in AV1 is roughly equivalent to CRF 20 in H.264.

The FFmpeg cheat sheet has a quick reference for codec options and flags.

Handling GIF transparency

GIFs can have transparent pixels. MP4 cannot. There's no alpha channel in H.264, H.265, or AV1. When you convert a transparent GIF to MP4, FFmpeg fills the transparent areas with black by default.

If you want a different background color, use the color source filter to create a canvas and overlay the GIF on top:

ffmpeg -f lavfi -i color=white:s=800x600 -i transparent.gif \
  -filter_complex "[0][1]scale2ref[bg][gif];[bg]setsar=1[bg];[bg][gif]overlay=shortest=1" \
  -movflags faststart \
  -pix_fmt yuv420p \
  -c:v libx264 -crf 20 \
  output.mp4

Replace white with any color: 0x1a1a2e for dark backgrounds, green for chroma key workflows, or a hex value matching your site's background. The scale2ref filter automatically matches the canvas size to the GIF dimensions, so you don't need to know the exact resolution beforehand.

Most of the time, black is fine and you don't need the extra filter. But if you're converting UI mockups, logos, or product screenshots with transparency, the black fill looks wrong and this filter saves you from pre-processing in an image editor.

Setting frame rate

GIFs have variable frame timing, where each frame can have a different delay. MP4 uses a fixed frame rate. FFmpeg handles this conversion automatically, but the result can feel off. A GIF that pauses on certain frames for emphasis will play back at a constant speed in the MP4.

To set a specific frame rate:

ffmpeg -i animation.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -r 24 \
  output.mp4

Common values: 24fps for cinematic feel, 30fps for general use, 15fps if you want to keep the file tiny. Most animated GIFs run at 10-15fps originally, so setting 30fps just duplicates frames without adding smoothness.

Check the original GIF's frame rate first:

ffprobe -v error -show_entries stream=r_frame_rate,avg_frame_rate -of default=noprint_wrappers=1 animation.gif

If the GIF runs at 10fps, matching that with -r 10 avoids unnecessary frame duplication and keeps the file smaller.

Looping a GIF as MP4 video

Social platforms want video, not GIFs. But a 2-second MP4 feels too short. You can loop a GIF multiple times during the conversion:

ffmpeg -stream_loop 4 -i animation.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -c:v libx264 -crf 20 \
  -t 15 \
  output.mp4

-stream_loop 4 repeats the input 4 times (so 5 total plays). The -t 15 caps the output at 15 seconds in case the loops exceed what you want. Useful when preparing content for Instagram Reels, TikTok, or YouTube Shorts where you need a minimum video duration.

Batch converting GIFs to MP4

One GIF? Use the command line. Hundreds of GIFs sitting in a folder? You need a script.

Bash (Linux/Mac)

for f in *.gif; do
  ffmpeg -i "$f" \
    -movflags faststart \
    -pix_fmt yuv420p \
    -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
    -c:v libx264 -crf 20 \
    "${f%.gif}.mp4"
done

Node.js

const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const gifDir = './gifs';
const outDir = './mp4s';
fs.mkdirSync(outDir, { recursive: true });

const gifs = fs.readdirSync(gifDir).filter(f => f.endsWith('.gif'));

for (const gif of gifs) {
  const input = path.join(gifDir, gif);
  const output = path.join(outDir, gif.replace('.gif', '.mp4'));

  execSync(`ffmpeg -i "${input}" -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -c:v libx264 -crf 20 "${output}"`);

  console.log(`Converted: ${gif}`);
}

This works fine on your local machine. But if you're converting GIFs in a web application (user uploads, CMS workflows, automated pipelines), shelling out to FFmpeg on your server gets messy fast. You hit process limits, memory pressure, and real security concerns around running arbitrary binaries. That's where offloading to an FFmpeg API makes more sense.

The Node.js FFmpeg API guide covers error handling, polling, and concurrent job management if you're building this into a product.

Convert GIF to MP4 via API

If you don't want to install FFmpeg or manage servers, send the command to an API instead. Here's how with RenderIO:

curl

curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: your_api_key" \
  -d '{
    "ffmpeg_command": "-i {input} -movflags faststart -pix_fmt yuv420p -vf \"scale=trunc(iw/2)*2:trunc(ih/2)*2\" -c:v libx264 -crf 20 {output}",
    "input_files": { "input": "https://example.com/animation.gif" },
    "output_files": { "output": "animation.mp4" }
  }'

The API returns a command_id. Poll for the result:

curl https://renderio.dev/api/v1/commands/CMD_ID \
  -H "X-API-KEY: your_api_key"

When status is SUCCESS, the response includes a download URL for your MP4. The curl examples page has 20 more ready-to-paste commands like this.

Node.js

const response = await fetch('https://renderio.dev/api/v1/run-ffmpeg-command', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-KEY': process.env.RENDERIO_API_KEY,
  },
  body: JSON.stringify({
    ffmpeg_command: '-i {input} -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -c:v libx264 -crf 20 {output}',
    input_files: { input: gifUrl },
    output_files: { output: 'converted.mp4' },
  }),
});

const { command_id } = await response.json();

// Poll until done
let result;
do {
  await new Promise(r => setTimeout(r, 2000));
  const check = await fetch(`https://renderio.dev/api/v1/commands/${command_id}`, {
    headers: { 'X-API-KEY': process.env.RENDERIO_API_KEY },
  });
  result = await check.json();
} while (result.status === 'PROCESSING');

console.log('MP4 URL:', result.output_files.output.storage_url);

The Node.js guide covers webhook callbacks (so you don't have to poll), error handling, and running multiple conversions in parallel.

Python

import requests
import time

api_key = "your_api_key"
headers = {"Content-Type": "application/json", "X-API-KEY": api_key}

# Submit
resp = requests.post("https://renderio.dev/api/v1/run-ffmpeg-command", headers=headers, json={
    "ffmpeg_command": '-i {input} -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -c:v libx264 -crf 20 {output}',
    "input_files": {"input": "https://example.com/animation.gif"},
    "output_files": {"output": "animation.mp4"}
})
command_id = resp.json()["command_id"]

# Poll
while True:
    time.sleep(2)
    result = requests.get(f"https://renderio.dev/api/v1/commands/{command_id}", headers=headers).json()
    if result["status"] != "PROCESSING":
        break

print("MP4 URL:", result["output_files"]["output"]["storage_url"])

The Python guide has a more complete version with retries and batch processing patterns.

For batch processing via the API, check the commands list with API examples. You can run up to 10 conversions in parallel using the /run-multiple-ffmpeg-commands endpoint.

Automate it with n8n or Zapier

If you want to trigger GIF-to-MP4 conversions from a workflow (say, whenever a new GIF lands in a Google Drive folder or a Slack channel), n8n can handle that with an HTTP Request node. Point it at the RenderIO API and you've got automated conversion without writing code. The Zapier video conversion guide covers the same approach with Zapier's interface.

GIF vs MP4: file size comparison

The whole reason to convert GIFs to MP4 is file size. Here's what the difference actually looks like:

Source GIFGIF sizeMP4 (H.264, CRF 20)MP4 (H.265, CRF 24)Reduction
Screen recording, 5s, 800x6004.2 MB180 KB95 KB96-98%
Animated meme, 3s, 480x3601.8 MB85 KB48 KB95-97%
UI animation, 2s, 400x300680 KB32 KB18 KB95-97%
Photo-quality animation, 8s, 1024x76812 MB420 KB210 KB96-98%

Typical compression ratio is 10:1 to 50:1. The more frames and the higher the resolution, the bigger the gap. Worth noting: for very small GIFs under 500 KB, the MP4 container overhead means you might not save much. Below that threshold, the GIF might actually be fine as-is.

If you need the reverse direction (converting MP4 video back to GIF with proper palette optimization), there's a complete guide to MP4 to GIF conversion that covers palette generation and size reduction.

Common errors and fixes

"width not divisible by 2": Add -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2". This is the most common error people hit because H.264's macroblock encoding needs even pixel counts on both axes.

Black screen on playback: You're missing -pix_fmt yuv420p. The encoder chose a pixel format your player doesn't support. Add the flag and re-encode. Verify with ffprobe -v error -show_entries stream=pix_fmt output.mp4.

Video plays but colors look wrong: Expected. The conversion from GIF's indexed RGB palette to YUV 4:2:0 loses some color accuracy. If color fidelity matters more than compatibility, try -pix_fmt yuv444p, but know that many players won't handle it.

Output is larger than the GIF: Happens with very short, very small GIFs. The MP4 container itself has overhead (moov atom, codec headers). If your GIF is under 500 KB, the conversion might not save space.

Variable speed playback: GIF frame delays aren't uniform. Use -r 15 or -r 24 to force a constant frame rate.

"No such encoder" or encoder errors: Your FFmpeg build may not include the codec you specified. Run ffmpeg -encoders | grep libx264 to check what's available. Pre-built packages from most Linux distros include libx264 and libx265. For AV1 (libsvtav1), you might need a newer build or compile from source. Running commands through an API service sidesteps this since the service maintains a full-featured FFmpeg build.

FAQ

How do I convert a GIF to MP4 without losing quality?

You can't avoid some quality change. GIF uses indexed RGB colors and MP4 uses YUV color space, so the color representation shifts no matter what. But you can minimize visible loss with a low CRF value. CRF 17 or 18 with libx264 is considered visually lossless for most content. Since GIF source material is already limited to 256 colors, the encoder doesn't have much complexity to deal with, so the file stays small even at high quality settings.

Can I convert a GIF to MP4 while keeping the transparency?

No. MP4 containers with H.264, H.265, or AV1 codecs don't support alpha channels. Transparent areas get filled with a solid color (black by default). You can choose a different background color during conversion; see the transparency section above. If you absolutely need transparency in a video format, WebM with VP9 supports alpha, but browser and player compatibility are limited.

Why is my converted MP4 larger than the original GIF?

This happens with very small or very short GIFs. The MP4 container adds overhead for metadata, codec headers, and the moov atom. For GIFs under 500 KB, the container overhead can outweigh the compression savings. In those cases, the GIF is small enough that conversion isn't worth it.

What frame rate should I use for GIF to MP4?

Match the original GIF's frame rate when possible. Most animated GIFs run at 10-15 fps. Setting a higher frame rate like 30 or 60 just duplicates frames and inflates file size without making the animation smoother. Check the original rate with ffprobe and use -r to match it.

Does the GIF to MP4 conversion work on Windows?

Yes. The same FFmpeg commands work on Windows, macOS, and Linux. On Windows, download FFmpeg from ffmpeg.org/download.html, extract it, and either add it to your PATH or use the full path to the executable. The only difference: if using the batch script, swap the bash for loop for a Windows equivalent or use PowerShell.

How do I convert multiple GIFs to MP4 at once?

Use the bash loop or Node.js script from the batch processing section above. For larger volumes (hundreds or thousands of GIFs), send conversion jobs to an API and process them in parallel. The FFmpeg API complete guide covers batch endpoints and concurrent processing.

Quick reference

The go-to command for most situations:

ffmpeg -i input.gif \
  -movflags faststart \
  -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
  -c:v libx264 -crf 20 \
  output.mp4

Want to run this without installing FFmpeg? Get a RenderIO API key and send the command as JSON. The REST API tutorial walks through the full workflow, and the commands list pairs every common command with a ready-to-use API call.