FFmpeg is not in the Rocky Linux 10 base repositories, so a working install needs EPEL, CRB and RPM Fusion. On a clean 4 vCPU test box, RPM Fusion’s ffmpeg 7.1.5 (installed November 2026 on Rocky Linux 10.2) pulled 238 packages and 162 MB, and the whole transaction finished in 71 seconds. The same repo stack applies to AlmaLinux 10 and RHEL 10.
We install FFmpeg first and then work through a tested ffmpeg commands cheat sheet. It covers probing, transcoding, trimming and joining, scaling, audio extraction and subtitles. It also shows what hardware acceleration does on a CPU-only host and which errors you will hit along the way. Every command and output below came from that box, including encode timings for four codecs across six encodes.
FFmpeg Cheat Sheet (Quick Reference)
Every row below is a complete command that ran on the Rocky Linux 10 test box against sample.mp4, a 20 second 1080p clip that Step 6 creates. In the lab each ffmpeg row also carried -hide_banner -loglevel error to keep the logs short. Those two flags change only what is printed, never the output file. Swap in your own file names and the commands work as written.
Probe a file
| Command | What it does |
|---|---|
ffprobe -hide_banner sample.mp4 | Container, duration, bitrate and every stream |
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 sample.mp4 | Duration in seconds, nothing else |
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt -of csv=p=0 sample.mp4 | Video codec, size, pixel format and frame rate as CSV |
ffprobe -v error -show_entries format=format_name,duration,size,bit_rate -of json sample.mp4 | Format fields as JSON for scripts |
ffprobe -v error -select_streams v:0 -skip_frame nokey -show_entries frame=pts_time -of csv=p=0 sample.mp4 | Keyframe timestamps (where a stream-copy cut can land) |
ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of csv=p=0 sample.mp4 | Exact frame count (decodes the whole stream) |
ffmpeg -hide_banner -i sample.mp4 2>&1 | grep -E "Duration|Stream" | Stream summary from ffmpeg itself; needs the stderr redirect |
Transcode and remux
| Command | What it does |
|---|---|
ffmpeg -i sample.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k -movflags +faststart out-h264.mp4 | H.264 + AAC MP4, index at the front for web playback |
ffmpeg -i sample.mp4 -c:v libx264 -crf 23 -preset veryfast -c:a aac -b:a 128k out-h264-veryfast.mp4 | Same quality target, 38% less wall time |
ffmpeg -i sample.mp4 -c:v libx265 -crf 28 -preset medium -tag:v hvc1 -c:a copy out-h265.mp4 | HEVC with the hvc1 tag Apple players expect |
ffmpeg -i sample.mp4 -c:v libsvtav1 -crf 35 -preset 8 -c:a libopus -b:a 96k out-av1.mkv | AV1 (SVT-AV1) + Opus in Matroska |
ffmpeg -i sample.mp4 -c:v libvpx-vp9 -crf 32 -b:v 0 -row-mt 1 -c:a libopus out-vp9.webm | VP9 + Opus WebM, constant quality mode |
ffmpeg -i sample.mp4 -c copy sample.mkv | Remux MP4 to MKV without re-encoding |
ffmpeg -i sample.mp4 -threads 2 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k out-h264-t2.mp4 | Cap encoding at 2 threads on a shared host |
Trim, split and concatenate
| Command | What it does |
|---|---|
ffmpeg -ss 00:00:05 -i sample.mp4 -t 4 -c copy cut-copy.mp4 | Instant cut, snaps to keyframes (came out 4.07 s) |
ffmpeg -ss 00:00:05 -i sample.mp4 -t 4 -c:v libx264 -crf 20 -c:a aac cut-exact.mp4 | Frame-accurate cut by re-encoding (exactly 4.00 s) |
ffmpeg -i sample.mp4 -ss 00:00:12 -to 00:00:15 -c:v libx264 -c:a aac cut-to.mp4 | Cut from one timestamp to another |
printf "file '%s'\n" part1.mp4 part2.mp4 > list.txt | Build the list file for the concat demuxer |
ffmpeg -f concat -safe 0 -i list.txt -c copy joined.mp4 | Join same-codec files without re-encoding |
ffmpeg -i sample.mp4 -c copy -f segment -segment_time 5 -reset_timestamps 1 seg_%03d.mp4 | Split at the first keyframe after each 5 s mark (8.3 s pieces here) |
Scale, crop and grab frames
| Command | What it does |
|---|---|
ffmpeg -i sample.mp4 -vf scale=1280:-2 -c:a copy out-720p.mp4 | Resize to 1280 wide, height kept even (1280×720) |
ffmpeg -i sample.mp4 -vf "scale=-2:480,fps=24" -c:a copy out-480p.mp4 | 480p at 24 fps (854×480) |
ffmpeg -i sample.mp4 -vf "crop=1080:1080" -c:a copy square.mp4 | Centre crop to a square |
ffmpeg -ss 00:00:07 -i sample.mp4 -frames:v 1 -q:v 2 thumb.jpg | One JPEG thumbnail at 7 s |
ffmpeg -i sample.mp4 -vf fps=1/5 frame_%02d.png | One PNG every 5 seconds |
ffmpeg -ss 2 -t 3 -i sample.mp4 -vf "fps=10,scale=480:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" clip.gif | 3 second GIF with a generated palette |
Extract and fix audio
| Command | What it does |
|---|---|
ffmpeg -i sample.mp4 -vn -c:a copy audio.m4a | Pull the AAC track out untouched |
ffmpeg -i sample.mp4 -vn -c:a libmp3lame -q:a 2 audio.mp3 | Audio to VBR MP3 |
ffmpeg -i sample.mp4 -vn -ac 1 -ar 16000 -c:a pcm_s16le speech.wav | 16 kHz mono WAV for speech-to-text tools |
ffmpeg -i sample.mp4 -an -c:v copy silent.mp4 | Drop the audio, keep the video bitstream |
ffmpeg -i sample.mp4 -af volumedetect -vn -f null - 2>&1 | grep -E "mean_volume|max_volume" | Measure loudness without writing a file |
ffmpeg -i sample.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11 -c:v copy normalized.mp4 | EBU R128 loudness normalisation, video copied |
ffmpeg -i silent.mp4 -i audio.mp3 -map 0:v -map 1:a -c:v copy -c:a aac -shortest dubbed.mp4 | Replace the audio track with another file |
Subtitles
| Command | What it does |
|---|---|
ffmpeg -i sample.mp4 -i subs.srt -map 0 -map 1 -c copy -c:s mov_text -metadata:s:s:0 language=eng soft-subs.mp4 | Soft subtitles in MP4 (SRT converted to mov_text) |
ffmpeg -i sample.mp4 -i subs.srt -map 0 -map 1 -c copy soft-subs.mkv | Soft subtitles in MKV (SRT kept as subrip) |
ffmpeg -i sample.mp4 -vf subtitles=subs.srt -c:a copy burned.mp4 | Burn subtitles into the picture (libass) |
ffmpeg -i soft-subs.mkv -map 0:s:0 extracted.srt | Extract the first subtitle stream to SRT |
Hardware acceleration checks
| Command | What it does |
|---|---|
ffmpeg -hide_banner -hwaccels | Methods compiled in (not what the host can use) |
ffmpeg -hide_banner -encoders | grep -E "nvenc|vaapi|qsv|amf|vulkan" | Hardware encoders in this build |
ls -l /dev/dri | Render nodes for VAAPI and QSV; missing on a plain VM |
ffmpeg -hwaccel auto -i sample.mp4 -t 2 -f null - | Tries each method, falls back to software, exits 0 |
ffmpeg -benchmark -i sample.mp4 -f null - 2>&1 | tail -2 | CPU decode baseline (2.6 s for the 20 s clip) |
Overwrites, optional streams and batches
| Command | What it does |
|---|---|
ffmpeg -y -i sample.mp4 -c copy sample.mkv | Overwrite the output without asking |
ffmpeg -n -i sample.mp4 -c copy sample.mkv | Never overwrite; skips and still exits 0 |
ffmpeg -i silent.mp4 -map 0:v -map 0:a? -c copy maybe-audio.mp4 | Map audio only if the input has any |
for f in part*.mp4; do ffmpeg -n -i "$f" -c:v libvpx-vp9 -crf 34 -b:v 0 -c:a libopus "webm/${f%.mp4}.webm"; done | Batch convert a folder to WebM (create webm/ first) |
Below the tables we install FFmpeg and explain the commands that need it. That includes why a stream-copy cut came out 67 ms long, why -n is a trap in scripts, and what each hardware path printed on a host with no GPU.
Prerequisites
You need a Rocky Linux 10, AlmaLinux 10 or RHEL 10 host with sudo and outbound HTTPS to the EPEL and RPM Fusion mirrors. Transcoding is CPU-bound, so the timings in Step 7 scale with core count. Treat our numbers as a floor for planning, not a target.
- Test box: Rocky Linux 10.2 (kernel 6.12), 4 vCPU on an Intel Core i7-1165G7 host, 4 GB RAM allocated (3.6 GB visible to
free), SELinux enforcing. AlmaLinux 10 uses the same commands; RHEL 10 differs only in how EPEL and CRB are enabled (see Step 2). - Disk: the FFmpeg transaction installs 238 packages, a 162 MB download that expands to 578 MB on disk, so leave at least 1 GB free on
/before you start. - A server to run it on: DigitalOcean gives new accounts $200 in credits, enough for a Rocky 10 droplet for several months of FFmpeg work.
Step 1: Confirm the base repos do not ship FFmpeg
Rocky Linux 10 ships BaseOS, AppStream and Extras, and none of them carries FFmpeg. Red Hat leaves it out by policy because several codecs it links against (x264, x265) are patent-encumbered in some jurisdictions. A search on the fresh box shows it.
dnf search ffmpeg
After the metadata refresh there is no match at all, which confirms the repos have to be added by hand:
Rocky Linux 10 - BaseOS 6.8 MB/s | 42 MB 00:06
Rocky Linux 10 - AppStream 1.2 MB/s | 2.9 MB 00:02
Rocky Linux 10 - Extras 5.2 kB/s | 6.1 kB 00:01
No matches found.
FFmpeg comes from three other repositories instead. EPEL supplies codec libraries such as SVT-AV1 and a cut-down ffmpeg-free, CRB supplies the devel libraries EPEL builds against, and RPM Fusion supplies the full ffmpeg build with x264 and x265.
Step 2: Enable EPEL and CRB
Rocky ships epel-release in its Extras repo, so one install command adds EPEL.
sudo dnf install -y epel-release
Its scriptlet prints a reminder that most EPEL packages need CRB, which is the next command.
Many EPEL packages require the CodeReady Builder (CRB) repository.
It is recommended that you run /usr/bin/crb enable to enable the CRB repository.
Installed:
epel-release-10-7.el10_1.noarch
Complete!
Enable CRB with the helper that epel-release installs. EPEL packages that link against CRB libraries do not resolve without it.
sudo /usr/bin/crb enable
A one-line confirmation names the repo.
Enabling CRB repo
CRB repo is enabled and named: crb
Our EPEL setup guide for Rocky Linux and AlmaLinux covers the RHEL variant. On RHEL 10, epel-release is not in any Red Hat repo, so the EPEL getting started page has you enable CodeReady Builder through subscription-manager and install the epel-release-latest-10 RPM from the Fedora mirror instead. We ran the Rocky commands above, not the RHEL ones.
Step 3: Add RPM Fusion free and nonfree
RPM Fusion is the community repository that packages what Red Hat and Fedora exclude. Its EL 10 release RPMs install directly from the mirror URL. Add the free repo first, since it carries ffmpeg itself.
sudo dnf install -y https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-10.noarch.rpm
Nonfree is optional for the CLI, but it holds the packages with restrictive licences, so most media hosts add it at the same time.
sudo dnf install -y https://mirrors.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-10.noarch.rpm
On an EL 9 host the same URLs end in -release-9.noarch.rpm, and the RPM Fusion setup guide lists both. Before installing anything, confirm the four repos are enabled.
dnf repolist enabled | grep -iE 'crb|epel|rpmfusion'
Four lines means the repo stack is complete. If one is missing, the install in Step 4 either fails or quietly pulls the wrong build:
crb Rocky Linux 10 - CRB
epel Extra Packages for Enterprise Linux 10 - x86_64
rpmfusion-free-updates RPM Fusion for EL 10 - Free - Updates
rpmfusion-nonfree-updates RPM Fusion for EL 10 - Nonfree - Updates
With CRB, EPEL and both RPM Fusion repos enabled, DNF can resolve the full FFmpeg build and every codec library it links against.
Step 4: Install FFmpeg
Install the binary and the development headers. The ffmpeg-devel package is not needed for the CLI, but anything that compiles against libavcodec (Python bindings, OBS plugins) needs it, and adding it now costs one extra package.
sudo dnf install -y ffmpeg ffmpeg-devel
Check the transaction summary first. Both ffmpeg packages must come from rpmfusion-free-updates, not from EPEL.
Installing:
ffmpeg x86_64 7.1.5-1.el10 rpmfusion-free-updates 1.9 M
ffmpeg-devel x86_64 7.1.5-1.el10 rpmfusion-free-updates 881 k
...
Install 238 Packages
Total download size: 162 M
Measured with time, the install took 1m11s of wall clock. Almost all of it was download, since user CPU was only 7.5 s. Among the 238 packages are x264-libs 0.164 and x265-libs 4.1 from RPM Fusion and svt-av1-libs 2.3.0 from EPEL. EPEL also carries ffmpeg-free 7.1.2, a different build with a different encoder set; the Troubleshooting section shows what changes if DNF picks it.
Step 5: Verify the build and its encoders
Start with the version banner. It names the build, and the long configuration: line lists every library compiled in. The RPM Fusion build on the test box carries --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libass --enable-nvenc among 84 --enable options, and its compiler flags target -march=x86-64-v3, the same baseline RHEL 10 itself requires.
ffmpeg -version
Then filter the encoder list for the four that most jobs use. ffmpeg -encoders prints the list on stdout, so a plain pipe to grep works here.
ffmpeg -hide_banner -encoders | grep -E "libx264|libx265|aac |libmp3lame"
Both checks, plus the codec library versions behind them, came out like this on the test box.

Read the six-character column on the left position by position. V or A marks video or audio, and the trailing D means the encoder supports direct rendering method 1, an internal buffer feature. It does not mean hardware decoding. The full build lists 232 encoders in total.
Step 6: Create a test clip and probe it
A synthetic clip makes every command in this guide reproducible without downloading media. The lavfi inputs generate a 20 second 1080p30 test pattern and a 440 Hz tone, which the command encodes to H.264 and AAC.
ffmpeg -hide_banner -f lavfi -i testsrc2=duration=20:size=1920x1080:rate=30 -f lavfi -i "sine=frequency=440:duration=20" -c:v libx264 -c:a aac -pix_fmt yuv420p sample.mp4
You get a 15 MB file at 6.2 Mb/s. For scripts, ffprobe with -v error and an explicit -show_entries list is the right tool, because it prints only the requested fields on stdout. The duration query returns 20.000000 and the stream query returns h264,1920,1080,yuv420p,30/1, both ready for a shell variable.
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt -of csv=p=0 sample.mp4
Keyframe positions matter more than they look. The query listed keyframes at 0, 8.33 and 16.67 seconds, which is the libx264 default of one keyframe every 250 frames at 30 fps, and those three timestamps decide where a stream-copy cut in Step 8 can start:

One trap catches almost everyone who greps ffmpeg output. ffmpeg writes its stream information to stderr, so ffmpeg -i sample.mp4 | grep Stream filters nothing: the whole banner still reaches the terminal, followed by At least one output file must be specified and exit code 1. Redirect stderr first and the filter works:
ffmpeg -hide_banner -i sample.mp4 2>&1 | grep -E "Duration|Stream"
Only the three matching lines come back, which is what a script parsing the duration or codec wants:
Duration: 00:00:20.00, start: 0.000000, bitrate: 6226 kb/s
Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 6148 kb/s, 30 fps, 30 tbr, 15360 tbn (default)
Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 69 kb/s (default)
The same rule applies to every ffmpeg log line you filter, including the volumedetect output in Step 10: send stderr into the pipe, or the filter sees nothing.
Step 7: Transcode and measure the encoder cost
Codec choice is mostly a CPU budget decision, so we timed each transcode row in the cheat sheet on the same 20 second 1080p clip with the bash time keyword. Each encoder ran once, one after another, on an otherwise idle 4 vCPU box.
| Encoder and settings | Wall time | User CPU | Output size |
|---|---|---|---|
Remux, -c copy to MKV | 0.18 s | 0.10 s | 15,192 KB |
| libx264, CRF 23, veryfast | 9.8 s | 20.2 s | 12,565 KB |
| libx264, CRF 23, medium | 15.8 s | 37.0 s | 14,396 KB |
libx264, CRF 23, medium, -threads 2 | 21.2 s | 32.8 s | 14,381 KB |
| libx265, CRF 28, medium | 38.7 s | 84.0 s | 10,727 KB |
| libsvtav1, CRF 35, preset 8 | 40.8 s | 65.3 s | 12,640 KB |
libvpx-vp9, CRF 32, -row-mt 1 | 156.3 s | 274.8 s | 16,164 KB |
libx264 at the medium preset encodes this clip at 1.26x real time on 4 vCPU, so a one hour 1080p file needs roughly 48 minutes. HEVC took 2.4 times as long as H.264 and AV1 at preset 8 took 2.6 times. VP9 is the outlier at 156 seconds, ten times the H.264 cost, even with row multithreading on. The remux row is the reason to try -c copy before anything else: when only the container has to change, the job takes milliseconds and the bitstream is untouched.

Read our size column with care. testsrc2 is a moving synthetic pattern with sharp edges, which is hard to compress, so VP9 at CRF 32 came out larger than the source. Treat the timings as the result and re-check sizes against your own footage. Two flags in the H.264 and HEVC rows are worth keeping. -movflags +faststart moves the MP4 index in front of the media data. A trace probe of out-h264.mp4 shows the moov atom before mdat, and that lets browsers start playback before the download ends. -tag:v hvc1 writes the HEVC sample entry as hvc1 (ffprobe reports hevc,hvc1), which Apple players expect.
Step 8: Trim, split and join clips
A stream-copy cut cannot start between keyframes, because the frames after the cut point reference a keyframe that would no longer exist. Asking for 4 seconds from the 5 second mark with -c copy returned a 4.067 s file, while the re-encoded version came out at exactly 4.000 s. The copy keeps the whole group of pictures from the keyframe at 0 s and the MP4 muxer hides the frames before 5 s behind an edit list, so a player that ignores edit lists shows them.
ffmpeg -ss 00:00:05 -i sample.mp4 -t 4 -c copy cut-copy.mp4
ffmpeg -ss 00:00:05 -i sample.mp4 -t 4 -c:v libx264 -crf 20 -c:a aac cut-exact.mp4
We pick between the two by what happens to the clip next. Putting -ss before -i seeks the input quickly, and both cuts above use that order.
- Stream copy (4.067 s): instant and lossless, so it suits trimming a recording to roughly the right length before archiving it.
- Re-encode (4.000 s): costs a full encode but lands on the exact frame, which matters for clips you join or sync to audio later.
Joining files uses the concat demuxer with a list file. The list was built with printf so the quoting is correct, and the two 10 second halves joined back into a 20.09 s file without re-encoding.
printf "file '%s'\n" part1.mp4 part2.mp4 > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy joined.mp4
Keep -safe 0 in the command. Without it, a list with absolute paths fails before any data is read, as the last command in the capture shows:

Concat with -c copy requires every part to share codec, resolution and timebase. Parts cut from one source, like these, always do. Files from different cameras or phones usually do not, so re-encode each one to the same settings first, then concatenate.
Step 9: Scale, crop and grab frames
Use -2, not -1, for the dimension ffmpeg should calculate. Both keep the aspect ratio, but -2 also rounds to an even number, and libx264 with 4:2:0 chroma refuses odd sizes. Scaling the 1080p clip with scale=-1:361 produced 642×361 and stopped at the encoder.
[libx264 @ 0x55e76880b640] height not divisible by 2 (642x361)
[vost#0:0/libx264 @ 0x55e76886f080] Error while opening encoder - maybe incorrect parameters such as bit_rate, rate, width or height.
With -2 the same filter returned 1280×720 for scale=1280:-2 and 854×480 at 24/1 fps for scale=-2:480,fps=24. A filter chain separates filters with commas. A centre crop to 1080×1080 needs no offsets, because crop centres by default.

For stills, -frames:v 1 writes one image and -vf fps=1/5 writes one every 5 seconds, which gave four PNGs for the 20 second clip. The GIF row runs palettegen and paletteuse in one pass, so the 3 second, 480 px wide GIF came out at 451 KB without the banding of the default 256-colour palette.
Step 10: Extract and replace audio
-vn drops video and -an drops audio. The rest is a choice between copying the existing track and encoding a new one. Copying AAC into audio.m4a kept it at aac,44100,1 with no quality loss. Speech-to-text tools such as Whisper want 16 kHz mono PCM, and the -ac 1 -ar 16000 -c:a pcm_s16le row returned exactly pcm_s16le,16000,1:
ffmpeg -i sample.mp4 -vn -ac 1 -ar 16000 -c:a pcm_s16le speech.wav
Measure before you normalise. The volumedetect filter with -f null - reads the whole track and writes nothing. The test tone measured a mean of -21.1 dB and a peak of -12.4 dB. As with probing, those lines arrive on stderr, so the cheat sheet row redirects with 2>&1 before the grep. The loudnorm row then brings a track to the EBU R128 target of -16 LUFS while copying the video stream unchanged.
Replacing a soundtrack needs explicit maps, because by default ffmpeg picks one stream of each type from all inputs. -map 0:v -map 1:a takes video from the first file and audio from the second, and -shortest trims the output to the shorter of the two. The dubbed file probed as h264 plus aac. For MP3 bitrate options and batch extraction, the video to MP3 conversion guide goes further.
Step 11: Add, burn and extract subtitles
Soft subtitles are a separate stream the player can switch off, while burned subtitles are drawn into the picture and cannot be removed. The test file used a two-cue SRT created with printf.
printf '1\n00:00:01,000 --> 00:00:04,000\nTested on Rocky Linux 10\n\n2\n00:00:05,000 --> 00:00:08,000\nffmpeg cheat sheet demo\n' > subs.srt
MP4 cannot store SRT as is. Copying it with a plain -c copy fails with Could not find tag for codec subrip in stream #2, so the MP4 command adds -c:s mov_text to convert just the subtitle stream. Matroska stores subrip natively, so the MKV version needs no conversion.

Burning uses the subtitles filter, which needs libass and at least one font. The RPM Fusion build has libass 0.17.3, and after the FFmpeg install the box had 37 fonts registered with fontconfig, so -vf subtitles=subs.srt worked on the first run. Extracting the stream back from the MKV produced cues shifted by 23 ms (00:00:01,023 instead of 00:00:01,000), so check timings after a round trip if frame accuracy matters.
Step 12: Check hardware acceleration on a CPU-only host
ffmpeg -hwaccels lists what the binary was compiled with, not what the host can run. The RPM Fusion build lists seven methods (vdpau, cuda, vaapi, qsv, drm, opencl, vulkan) and 21 hardware encoders across NVENC, VAAPI, QSV, AMF and Vulkan. On the test VM, which has no GPU passed through, /dev/dri does not exist, and every one of those paths fails differently.

Each failure points at a missing driver or device, never at a missing ffmpeg feature.
-hwaccel cudaandh264_nvenc:Cannot load libcuda.so.1. The NVIDIA driver library is absent; the encoder exists in the build.h264_vaapi:No VA display found for device /dev/dri/renderD128, then exit code 234 before any input is read.h264_qsv:Error creating a MFX session: -9. No Intel GPU is visible to the guest.-hwaccel auto: probes CUDA, then render nodes renderD128 upward, gives up, decodes in software at 7.38x and exits 0.
Remember the last one. -hwaccel auto never fails a job, so a pipeline that “uses the GPU” can run on the CPU for months without an error. Only -v verbose output or the speed figure gives it away. Pin an explicit method in production so a missing driver fails loudly. The software decode baseline from -benchmark was 2.6 s real time for the 20 second clip. For choosing hardware that does accelerate this, the GPU guide for Jellyfin and Plex transcoding compares Quick Sync and Arc.
Static build fallback
When system repos are off limits (a locked-down RHEL host, or a need to pin one upstream release), John Van Sickle’s static build runs on any x86-64 Linux without dependencies. The release offered on that page in November 2026 is still 7.0.2, older than the 7.1.5 RPM Fusion package. The minimal Rocky 10 cloud image does not include wget, so install it first.
sudo dnf install -y wget
Then download and extract the release tarball into a directory you own, so nothing lands in the system paths yet:
mkdir -p ~/ffmpeg-static
cd ~/ffmpeg-static
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
tar -xf ffmpeg-release-amd64-static.tar.xz
Copy both binaries into /usr/local/bin. On the test box that directory comes before /usr/bin in $PATH, so the static binary takes over the plain ffmpeg command even with the RPM installed.
sudo install -m755 ffmpeg-*-amd64-static/ffmpeg /usr/local/bin/
sudo install -m755 ffmpeg-*-amd64-static/ffprobe /usr/local/bin/
Check the version string for the static origin. This capture is from the April 2026 run of this guide on Rocky Linux 10.1:
/usr/local/bin/ffmpeg -version | head -3
Expect output like this, with the build date and the Debian toolchain that compiled it:
ffmpeg version 7.0.2-static https://johnvansickle.com/ffmpeg/ Copyright (c) 2000-2024 the FFmpeg developers
built with gcc 8 (Debian 8.3.0-6)
configuration: --enable-gpl --enable-version3 --enable-static --disable-debug ...
John Van Sickle’s build has no NVENC, VAAPI or QSV support, because those need driver libraries that a static binary cannot assume. If the host has a GPU, the RPM Fusion package is the better choice.
Troubleshooting common FFmpeg errors
We reproduced each error below on the test box. ffmpeg’s exit codes are not consistent across failure types, which is why the first one matters for anyone scripting it.
“File ‘sample.mkv’ already exists. Overwrite? [y/N] Not overwriting – exiting”
When stdin is not a terminal (cron, CI, or stdin redirected from /dev/null), ffmpeg cannot ask, so it answers no. It prints the message and exits with code 0, as does -n when it skips an existing file. A script that checks $? records a success for a file that was never written.

Pass -y or -n explicitly in every script, and when skipping matters, test for the output file yourself rather than trusting the exit code. The same capture shows the real failures exiting non-zero: a missing input returned 254 and a text file renamed to .mp4 returned 183 with moov atom not found.
“Unknown encoder ‘libx264′”
You have the EPEL build, ffmpeg-free, installed instead of RPM Fusion’s ffmpeg. Swapping to it on the test box and running the H.264 command reproduced the error with exit code 8. The two builds carry different encoders: ffmpeg-free 7.1.2 has libopenh264 and libfdk_aac but no x264 or x265, while RPM Fusion has x264 and x265 but no libfdk_aac. Swap back in one transaction:
sudo dnf swap -y ffmpeg-free ffmpeg --allowerasing
Our swap away from RPM Fusion removed ffmpeg-devel as a dependent package and the swap back does not reinstall it, so run sudo dnf install -y ffmpeg-devel afterwards if anything compiles against libavcodec.
“Unknown encoder ‘libfdk_aac'”
Older guides use -c:a libfdk_aac, and the RPM Fusion build answers with Error opening output files: Encoder not found and exit code 8. Use the native encoder, -c:a aac -b:a 128k, which every row in the cheat sheet uses.
“height not divisible by 2 (642×361)”
libx264 needs even dimensions for 4:2:0 output. Replace -1 with -2 in the scale filter so the calculated side is rounded to an even value (Step 9).
“Could not find tag for codec subrip in stream #2, codec not currently supported in container”
An SRT stream was copied into MP4, which has no subrip mapping. Add -c:s mov_text, or write to .mkv instead. The failed run ended with Conversion failed! and exit code 234.
“Unsafe file name” from the concat demuxer
Your list file contains absolute paths or names with characters the demuxer treats as unsafe. Add -safe 0 before -i list.txt. Without it the run ended with Operation not permitted and exit code 255.
“Stream map ” matches no streams”
A -map 0:a pointed at a file with no audio track. ffmpeg suggests the fix in the next line: a trailing question mark, -map 0:a?, makes the map optional, and the same command then wrote a video-only file.
“GPG check FAILED” on RPM Fusion packages
RPM Fusion’s release RPMs drop their keys in /etc/pki/rpm-gpg/ and DNF imports them on first use. If that import was skipped, import both keys by hand and retry the install.
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-el-10
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-el-10
On the test box both keys then showed up in rpm -q gpg-pubkey as the RPM Fusion free and nonfree keys for EL (10), so a retried install can verify the packages.
SELinux denies writes under /var/www
Rocky Linux 10 runs SELinux in enforcing mode, and a web service that transcodes uploads cannot write to a directory labelled var_t. Label the media directory instead of disabling SELinux.
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/media(/.*)?"
sudo restorecon -Rv /var/www/media
On the test box restorecon reported the relabel from var_t to httpd_sys_rw_content_t, and the rule persists across relabels because it lives in the policy, not on the inode.
Running FFmpeg on a shared host: what thread caps cost
Left alone, libx264 on the 4 vCPU box defaults to 6 encoding threads (threads=6 in the x264 options line when the sample clip was made) and used 37.0 s of CPU in 15.8 s of wall time, an average of 2.3 cores busy. Capping it with -threads 2 stretched the job to 21.2 s, 34% longer, while total CPU fell to 32.8 s and the output size stayed within 0.1%. On a host that also serves a database or web stack, that trade is usually right: the encode finishes a few seconds later and the other services keep their cores.
Preset is the bigger lever in our runs. Moving libx264 from medium to veryfast cut wall time by 38% and CPU time by 45%, at the cost of slightly less efficient compression on real footage. For background jobs that outlive an SSH session, run ffmpeg under tmux or a systemd unit and send stderr to a log file, since that is where its progress and errors go. The Rocky Linux 10 post-install tips cover the swap and SELinux baselines a media host needs, and for Fedora desktops the FFmpeg on Fedora guide compares the same RPM Fusion and ffmpeg-free split there.