Video conversion shows up everywhere: a designer needs WebM for the web, an editor needs MP4 from iPhone MOV, a marketer needs a GIF from a clip. Each format makes different trade-offs between file size, quality, browser support, and codec compatibility — and simply renaming .mov to .mp4 does nothing.
Video format quick reference
| Format | Container | Typical codec | Best for | Avg. size (1 min, 1080p) |
|---|---|---|---|---|
| MP4 | MPEG-4 | H.264 / H.265 | Universal — web, mobile, social | 60–150 MB |
| MOV | QuickTime | ProRes / H.264 | Apple ecosystem, Premiere editing | 60–500 MB |
| WebM | WebM | VP8 / VP9 / AV1 | Web <video> tag, no royalties |
30–80 MB |
| MKV | Matroska | Any | Archiving, multi-track (subtitles/audio) | 60–400 MB |
| AVI | AVI | DivX / Xvid | Legacy Windows, DVD rips | 100–400 MB |
| GIF | GIF | LZW | Short looping clips, no audio | 2–20 MB |
| WEBP (anim) | WEBP | WebP | Animated images for web (better than GIF) | 1–10 MB |
Key rule: converting lossy → lossy (e.g., AVI → MP4) re-encodes and adds generation loss. Always start from the highest-quality source you have.
Converting with ffmpeg (CLI)
ffmpeg handles virtually every video conversion. Install it once and it works everywhere.
# MOV → MP4 (H.264, AAC audio)
ffmpeg -i input.mov -c:v libx264 -c:a aac -movflags +faststart output.mp4
# MP4 → WebM (VP9)
ffmpeg -i input.mp4 -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus output.webm
# MKV → MP4 (copy streams without re-encoding — fast)
ffmpeg -i input.mkv -c copy output.mp4
# MP4 → GIF (15 fps, 480px wide, palette optimized)
ffmpeg -i input.mp4 -vf "fps=15,scale=480:-1:flags=lanczos,palettegen" palette.png
ffmpeg -i input.mp4 -i palette.png -vf "fps=15,scale=480:-1:flags=lanczos,paletteuse" output.gif
# MP4 → extract audio only
ffmpeg -i input.mp4 -vn -c:a copy output.aac
ffmpeg flag cheat sheet:
| Flag | Meaning |
|---|---|
-c:v libx264 |
Encode video with H.264 |
-c:v libvpx-vp9 |
Encode video with VP9 |
-c:a aac |
Encode audio with AAC |
-c copy |
Copy streams without re-encoding |
-crf 23 |
Quality factor (0=lossless, 51=worst; 18–28 typical) |
-b:v 2M |
Target video bitrate (2 Mbps) |
-movflags +faststart |
Put MP4 metadata first — enables progressive playback |
-ss 00:00:10 -t 30 |
Start at 10 s, clip 30 s |
-vf scale=1280:-1 |
Scale width to 1280, keep aspect ratio |
JavaScript (Node.js with fluent-ffmpeg)
import ffmpeg from "fluent-ffmpeg";
/**
* Convert a video file from one format to another.
* @param {string} input - Source path (e.g. "input.mov")
* @param {string} output - Destination path (e.g. "output.mp4")
* @returns {Promise<void>}
*/
export function convertVideo(input, output) {
return new Promise((resolve, reject) => {
ffmpeg(input)
.outputOptions([
"-c:v libx264",
"-c:a aac",
"-movflags +faststart",
"-crf 23",
])
.on("end", resolve)
.on("error", reject)
.save(output);
});
}
// Usage
await convertVideo("input.mov", "output.mp4");
console.log("Done");
Install: npm install fluent-ffmpeg (requires ffmpeg in PATH).
For browser-side conversion use ffmpeg.wasm:
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util";
const ffmpeg = new FFmpeg();
// Load ffmpeg.wasm (runs in a Web Worker — non-blocking)
const baseURL = "https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd";
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"),
});
await ffmpeg.writeFile("input.mov", await fetchFile(file)); // file = File from <input>
await ffmpeg.exec(["-i", "input.mov", "-c:v", "libx264", "-c:a", "aac", "output.mp4"]);
const data = await ffmpeg.readFile("output.mp4");
const url = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
Note: ffmpeg.wasm runs entirely client-side. Files never leave the browser, but processing is slower than native ffmpeg.
Python (subprocess + ffmpeg)
import subprocess
import shlex
from pathlib import Path
def convert_video(
input_path: str,
output_path: str,
crf: int = 23,
video_codec: str = "libx264",
audio_codec: str = "aac",
) -> None:
"""
Convert a video file using ffmpeg.
Raises subprocess.CalledProcessError on failure.
"""
cmd = [
"ffmpeg",
"-i", input_path,
"-c:v", video_codec,
"-c:a", audio_codec,
"-crf", str(crf),
"-movflags", "+faststart",
"-y", # overwrite output without asking
output_path,
]
subprocess.run(cmd, check=True, capture_output=True)
def video_to_gif(input_path: str, output_path: str, fps: int = 15, width: int = 480) -> None:
"""Convert a video clip to an optimised GIF using a two-pass palette approach."""
palette = Path(output_path).with_suffix(".palette.png")
scale = f"scale={width}:-1:flags=lanczos"
# Pass 1 — generate colour palette
subprocess.run(
["ffmpeg", "-i", input_path, "-vf", f"fps={fps},{scale},palettegen",
"-y", str(palette)],
check=True, capture_output=True,
)
# Pass 2 — encode GIF with palette
subprocess.run(
["ffmpeg", "-i", input_path, "-i", str(palette),
"-vf", f"fps={fps},{scale},paletteuse",
"-y", output_path],
check=True, capture_output=True,
)
palette.unlink(missing_ok=True)
# Usage
convert_video("input.mov", "output.mp4")
video_to_gif("clip.mp4", "animation.gif", fps=12, width=360)
Go (exec wrapper)
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// VideoCodec returns the ffmpeg video codec flag for a given output extension.
func videoCodec(ext string) string {
switch strings.ToLower(ext) {
case ".webm":
return "libvpx-vp9"
case ".ogv":
return "libtheora"
default:
return "libx264"
}
}
// ConvertVideo converts src to dst using ffmpeg.
// dst extension determines output format.
func ConvertVideo(src, dst string) error {
vcodec := videoCodec(filepath.Ext(dst))
args := []string{
"-i", src,
"-c:v", vcodec,
"-c:a", "aac",
"-movflags", "+faststart",
"-crf", "23",
"-y",
dst,
}
cmd := exec.Command("ffmpeg", args...)
cmd.Stderr = os.Stderr
return cmd.Run()
}
func main() {
if err := ConvertVideo("input.mov", "output.mp4"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("Done")
}
PHP (exec + escapeshellarg)
<?php
/**
* Convert a video file using ffmpeg.
*
* @param string $input Source path
* @param string $output Destination path
* @param int $crf Quality (0–51; lower = better; 23 is default)
* @param string $videoCodec ffmpeg video codec name
* @param string $audioCodec ffmpeg audio codec name
* @throws RuntimeException on ffmpeg failure
*/
function convertVideo(
string $input,
string $output,
int $crf = 23,
string $videoCodec = 'libx264',
string $audioCodec = 'aac'
): void {
// ALWAYS escapeshellarg() on user-supplied paths to prevent injection
$cmd = sprintf(
'ffmpeg -i %s -c:v %s -c:a %s -crf %d -movflags +faststart -y %s 2>&1',
escapeshellarg($input),
escapeshellarg($videoCodec),
escapeshellarg($audioCodec),
$crf,
escapeshellarg($output)
);
exec($cmd, $outputLines, $exitCode);
if ($exitCode !== 0) {
throw new \RuntimeException(
"ffmpeg failed (exit $exitCode): " . implode("\n", $outputLines)
);
}
}
// Usage
convertVideo('input.mov', 'output.mp4');
// WebM (VP9)
convertVideo('input.mp4', 'output.webm', crf: 30, videoCodec: 'libvpx-vp9', audioCodec: 'libopus');
Quick reference
| Task | ffmpeg one-liner |
|---|---|
| MOV → MP4 | ffmpeg -i in.mov -c:v libx264 -c:a aac -movflags +faststart out.mp4 |
| MP4 → WebM (VP9) | ffmpeg -i in.mp4 -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus out.webm |
| MKV → MP4 (no re-encode) | ffmpeg -i in.mkv -c copy out.mp4 |
| MP4 → GIF | two-pass palettegen → paletteuse (see above) |
| Trim clip | ffmpeg -ss 00:00:05 -to 00:00:15 -i in.mp4 -c copy clip.mp4 |
| Mute video | ffmpeg -i in.mp4 -an -c:v copy muted.mp4 |
| Scale to 720p | ffmpeg -i in.mp4 -vf scale=-1:720 -c:a copy out.mp4 |
| Extract thumbnail | ffmpeg -i in.mp4 -ss 00:00:03 -frames:v 1 thumb.jpg |
6 common mistakes
1. Renaming the file instead of converting it
Changing video.mov to video.mp4 does not re-encode it. The container changes but the codec stays the same. Some players can handle this, many cannot.
2. Forgetting -movflags +faststart for web streaming
Without it the MP4 metadata block goes at the end. The browser must download the entire file before starting playback. Always add -movflags +faststart for any video served over HTTP.
3. Using -c copy when the codec is incompatible
-c copy skips re-encoding. This is fast and lossless but only works when the source codec is supported in the destination container. Copying H.264 into WebM will fail.
4. Two-pass GIF skipping the palette step
A single-pass GIF conversion (ffmpeg -i input.mp4 output.gif) uses a generic 256-colour palette that looks terrible. Always use the two-pass palettegen / paletteuse pipeline for sharp-looking GIFs.
5. Converting lossy to lossy repeatedly
Every lossy re-encode (AVI → MP4 → WebM) degrades quality. Keep the original source. If you need multiple output formats, always transcode from the original.
6. Not checking the output codec with -c copy and MKV
MKV containers can hold almost any codec, but MP4 cannot hold some (e.g., FLAC audio, HEVC in some cases). Run ffprobe input.mkv first to see what's inside, then decide whether -c copy is safe.
Frequently asked questions
Q: What's the best format for web video?
MP4 (H.264) has the widest device support. WebM (VP9) gives 20–40% smaller files but requires a fallback. Use <video> with both sources:
<video>
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
</video>
Q: Can I convert video without installing ffmpeg? Browser-side conversion is possible with ffmpeg.wasm — no server required. It's slower but works entirely in the browser. For online conversion without any code, use the Media Converter tool.
Q: How do I reduce file size without visible quality loss?
Lower the CRF value slightly (-crf 28 instead of 23), scale down to a smaller resolution, or drop to 24 fps if the source is 60 fps. For web, VP9 or H.265 give noticeably smaller files than H.264 at the same quality.
Q: Why is my GIF huge — larger than the original MP4?
GIF is uncompressed (only LZW, no inter-frame compression). A 10-second 480px GIF can easily hit 10 MB while the MP4 is 1 MB. Consider using <video autoplay loop muted playsinline> instead — it looks identical but uses proper video compression.
Q: How do I convert only a portion of a video?
Use -ss (start time) and -t (duration) or -to (end time):
ffmpeg -ss 00:00:30 -to 00:01:00 -i input.mp4 -c copy clip.mp4
Put -ss before -i for fast seeking (keyframe accuracy); after -i for frame-accurate seeking.
Q: Is there a lossless video format?
Yes: ffmpeg -i input.mp4 -c:v libx264 -crf 0 lossless.mp4 (lossless H.264) or use MKV with FFV1 for archiving. Lossless files are very large — typically 10× the size of a good-quality lossy encode.