Practical FFmpeg Recipes: Convert, Trim, Compress, and Extract
Almost every ffmpeg problem is a position problem. A command reads in one direction — global options, input, output options, output file — and ffmpeg applies each flag to whatever file comes next. The same flag before -i and after -i can mean two different things, so learning to read the shape of a command pays off more than memorising individual recipes.
The canonical shape is ffmpeg [global options] -i input.mp4 [output options] output.mp4. Read it as four zones:
- Global options come first:
-yoverwrites the output without asking,-hide_bannerdrops the build dump. - Input options sit just before their
-i:-sshere seeks through the container index, so it stays fast no matter how long the file is. - Output options are everything after the last input: codecs (
-c:v,-c:a), filters (-vf,-filter_complex), stream selection (-map), duration limits (-t,-to). - Stream specifiers narrow a flag to specific streams:
:vis video,:ais audio, and0:a:0is the first audio stream of the first input.
The second decision behind every recipe below is copy versus re-encode. -c copy hands the compressed packets straight to the new container: near-instant, bit-for-bit lossless, and unable to change resolution, codec, or cut anywhere but a keyframe. Anything with a filter or an encoder decodes every frame, encodes a new one, and costs CPU time plus a generation of quality. Try copy first; re-encode only when copy cannot do the job.
// note: -ss before -i is an input seek: ffmpeg jumps through the index to the nearest keyframe, so it is fast on any file size. -ss after -i is an output seek: ffmpeg decodes and discards every frame up to the cut point, which gets slower the deeper the cut. When you re-encode, input seeking is both fast and frame-exact, because -accurate_seek is on by default and decodes from that keyframe up to your exact timestamp.
## Convert Between Containers
Going from MKV to MP4 is usually not a conversion at all. The video and audio inside are already H.264 and AAC, and the only thing that has to change is the wrapper — so rewrap it and skip the encode entirely. You only need a real transcode when the target container refuses the codecs the file already carries.
ffmpeg -i input.mkv -c copy output.mp4ffmpeg -i input.avi -c:v libx264 -crf 23 -c:a aac output.mp4-c copy is short for -codec copy and applies to every stream ffmpeg selects. Default stream selection picks only the single best video and audio stream, so extra audio tracks, subtitles, and attachments are silently dropped — add -map 0 to carry the whole file across. The fallback encodes video with libx264 at -crf 23 (x264's own default) and audio with the built-in aac encoder, the combination that phones, browsers, and TVs all accept. One catch: MP4 has no tag for SubRip or ASS subtitles, so -map 0 on an MKV carrying soft subs aborts the mux — convert them with -c:s mov_text or leave them behind.
## Trim a Clip Without Re-encoding
Cutting a range out of a long file is where stream copy pays off most: nothing is decoded, nothing is encoded, and the output is written at disk speed. Use -to when you know the end timestamp and -t when you know the duration you want.
ffmpeg -ss 00:01:00 -to 00:02:00 -i input.mp4 -c copy output.mp4
ffmpeg -ss 00:05:00 -i input.mp4 -t 30 -c copy output.mp4The first command extracts the 1:00–2:00 range. The second seeks to 5:00 and takes the next 30 seconds — -t is a length, not an end position. Both put -ss before -i, so the seek uses the index and finishes almost instantly whatever the source size. Swap -c copy for -c:v libx264 -c:a aac when the boundaries have to land on the exact frame you asked for.
// note: Stream copy cannot create a keyframe, so the start snaps back to the nearest keyframe at or before your timestamp: the clip begins slightly early and its length is not exactly what you requested. Re-encode when the cut has to be frame-exact — for example when the pieces will be stitched back together later.
## Compress a Video to a Sensible Size
CRF (Constant Rate Factor) is the default answer to "make this smaller". You set a quality target and the encoder spends whatever bitrate each scene actually needs, which beats guessing a bitrate in nearly every case where you do not have a hard size limit.
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -c:a aac output.mp4ffmpeg -i input.mp4 -c:v libx265 -crf 28 -tag:v hvc1 -c:a aac output.mp4For libx264, CRF runs from 0 (lossless) to 51, with 18–28 the useful range and 23 the default; moving CRF by about 6 roughly halves or doubles the bitrate. The numbers are not comparable across encoders, which is why the libx265 command uses 28 for comparable-looking output. Add -preset slow to buy a smaller file at the same quality with more encode time, and keep -tag:v hvc1 on HEVC or Safari and QuickTime will refuse to play the result.
## Extract the Audio Track
Two paths: copy the existing audio out untouched, or re-encode it into a format something else can read. Copying is instant and lossless, but you do not get to pick the codec — it is whatever the video already contained.
ffmpeg -i input.mp4 -vn -c:a copy audio.m4a
ffmpeg -i input.mp4 -vn -c:a libmp3lame -q:a 2 audio.mp3-vn drops the video stream. The first command copies the AAC track straight into an M4A, which is just an MP4 container holding audio only. The second re-encodes with LAME at -q:a 2, a VBR setting that averages around 190 kbps and is close to transparent. With -c:a copy the output container must accept the source codec: point that same command at audio.mp3 and the muxer fails with "Invalid audio stream. Exactly one MP3 audio stream is required."
## Remove or Replace the Audio Track
Muting a video and dubbing a new track over it are the same operation from two sides: one drops audio streams, the other picks them out of a second input.
ffmpeg -i input.mp4 -an -c:v copy output.mp4
ffmpeg -i input.mp4 -i audio.mp3 -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 -shortest output.mp4-an removes every audio stream, not just the default one, so a multi-language file loses all of them at once. -c:v copy in both commands leaves the video bit-for-bit identical. In the second, -map 0:v:0 takes video from the first input and -map 1:a:0 takes audio from the second — without explicit maps, ffmpeg's automatic selection can grab the original soundtrack instead of the new one. -shortest ends the output when the shorter input runs out, instead of padding the longer stream out to its own end.
## Resize or Scale Video
Scaling always re-encodes, so pair it with the CRF settings above when quality matters. The one real trap is aspect-ratio arithmetic producing an odd pixel count that the encoder then rejects.
ffmpeg -i input.mp4 -vf "scale=-2:720" -c:a copy output.mp4ffmpeg -i input.mp4 -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:-1:-1" output.mp4scale=-2:720 pins the height and computes the width from the source aspect ratio, rounding to an even number. That rounding is the point: H.264 with the standard yuv420p pixel format rejects odd dimensions with "width not divisible by 2", which is exactly what -1 eventually hands you. The second form shrinks the frame to fit inside a fixed 1280x720 box without cropping, then pads the leftover space with centred black bars. -c:a copy keeps the audio out of the re-encode.
## Make a GIF That Does Not Look Terrible
GIF allows 256 colours per frame, so the whole game is choosing those 256 well. The palettegen/paletteuse workflow builds a palette tuned to your specific footage and then maps every frame against it, which produces better colour and usually a smaller file than letting the encoder improvise.
ffmpeg -i input.mp4 -vf "fps=10,scale=320:-1:flags=lanczos,palettegen" palette.png
ffmpeg -i input.mp4 -i palette.png -lavfi "fps=10,scale=320:-1:flags=lanczos[x];[x][1:v]paletteuse" output.gifffmpeg -i input.mp4 -filter_complex "fps=10,scale=320:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" output.giffps=10 cuts the frame rate, which is the single biggest lever on GIF size; scale=320:-1:flags=lanczos downsizes with a sharp resampler; palettegen writes a tiny 16x16 PNG holding the 256 chosen colours. The second pass feeds that PNG in as input 1 so paletteuse can dither each frame against it. The one-liner does the same work in a single pass by using split to fork the filtered stream into the palette generator and its consumer. Keep fps and scale identical across both passes — the palette is optimised for those exact pixels.
// note: Skipping the palette entirely, as in ffmpeg -i input.mp4 -vf "fps=10,scale=320:-1" output.gif, leaves the GIF encoder to auto-quantise a generic 256-colour palette on the fly. Expect visible banding, dithering noise, and usually a larger file than the two-pass version.
## Pull Frames Out as Images
One frame for a poster image, or a whole strip of them for previews, contact sheets, and datasets. Both are one-liners; the difference is whether you seek to a timestamp or resample the timeline.
ffmpeg -ss 00:00:05 -i input.mp4 -frames:v 1 thumbnail.jpg
ffmpeg -i input.mp4 -vf fps=1 frame_%04d.png-frames:v 1 stops after a single video frame and the input seek puts that frame at the five-second mark. Because the frame is re-encoded rather than copied, -accurate_seek decodes forward from the keyframe and you really do get the frame at 00:00:05 — the nearest-keyframe limitation only bites with -c copy. In the second command, fps=1 resamples the stream to one frame per second and the %04d pattern writes frame_0001.png onward. That is a resample, not a seek, so frames land near each second rather than exactly on it, and the image2 muxer overwrites existing files without prompting.
## Concatenate Clips
When the clips came off the same camera or the same encode, the concat demuxer joins them by copying packets — no quality loss, no wait. It reads a plain text file listing the parts in playback order.
file 'part1.mp4'
file 'part2.mp4'
file 'part3.mp4'ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4ffmpeg -i input1.mp4 -i input2.mp4 -filter_complex "[0:v:0][0:a:0][1:v:0][1:a:0]concat=n=2:v=1:a=1[outv][outa]" -map "[outv]" -map "[outa]" output.mp4-f concat selects the demuxer and -safe 0 disables the check that would otherwise reject absolute paths in the list file. Each line needs the exact file '...' syntax, and a single quote inside a filename has to be escaped. The filter version instead decodes both inputs, feeds their streams in interleaved order to concat=n=2:v=1:a=1 (two segments, one video and one audio stream each), and maps the labelled [outv]/[outa] outputs — slower, but able to bridge clips that were encoded differently.
// note: The concat demuxer with -c copy assumes every input shares the same codec, resolution, timebase, and pixel format. A mismatch rarely produces a clean error — you get stutter, frozen frames, or drifting audio instead. The concat filter is more forgiving but still requires matching resolution, pixel format, and SAR, so normalise with scale and setsar first.
## Speed Up or Slow Down
Retiming touches video timestamps and audio samples through two unrelated filters, so both have to be set — and set to reciprocal values, or the streams drift apart. Every retime re-encodes; you cannot remap presentation times with a stream copy.
ffmpeg -i input.mp4 -vf "setpts=0.5*PTS" -af "atempo=2.0" output.mp4
ffmpeg -i input.mp4 -vf "setpts=2.0*PTS" -af "atempo=0.5" output.mp4setpts multiplies each frame's presentation timestamp: 0.5 packs the clip into half the runtime (2x), 2.0 stretches it to double the runtime (half speed). atempo time-stretches the audio while preserving pitch, unlike asetrate, which shifts it like a tape machine. Modern ffmpeg accepts a single atempo up to 100.0, so atempo=4.0 alongside setpts=0.25*PTS is fine; the lower bound is still 0.5, so chain atempo=0.5,atempo=0.5 for anything slower than half speed. Slowed footage looks juddery because setpts only holds existing frames longer — minterpolate synthesises in-between frames if you need smooth slow motion, at a large CPU cost.
Two habits head off most ffmpeg surprises: run ffprobe -v error -show_streams input.mp4 before a conversion so you know which codecs you are holding, and check whether -c copy can do the job before committing to a re-encode.