FFmpeg is the standard command-line tool for converting, compressing and packaging audio and video. On a server it is the engine behind media libraries, video platforms and live streaming pipelines. In this tutorial you will install FFmpeg on Ubuntu 24.04, transcode video to H.264 and H.265 with sensible quality settings, package it as adaptive HLS for web playback, use GPU encoders when available, and automate transcoding of an incoming folder with a robust script.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS. Software encoding is CPU bound, so more vCPUs mean faster jobs; 4 vCPUs and 4 GB of RAM are a reasonable starting point.
- A non-root user with
sudoprivileges. - Free disk space of at least twice the size of the files you plan to process.
- Optional: an Intel GPU (VAAPI) or NVIDIA GPU (NVENC) for Step 6.
Step 1 - Installing FFmpeg
Ubuntu 24.04 ships FFmpeg 6.1 with the common encoders (x264, x265, VP9, Opus, MP3) already built in, so there is no need to compile it or install extra codec packages:
sudo apt update
sudo apt install -y ffmpeg
Check the installed version:
ffmpeg -hide_banner -version | head -n 1
ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers
List the video and audio encoders used in this guide to confirm they are available:
ffmpeg -hide_banner -encoders | grep -E 'libx264|libx265|libvpx-vp9|aac|libopus'
V....D libx264 libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (codec h264)
V....D libx265 libx265 H.265 / HEVC (codec hevc)
V....D libvpx-vp9 libvpx VP9 (codec vp9)
A....D aac AAC (Advanced Audio Coding)
A....D libopus libopus Opus (codec opus)
Step 2 - Creating a test file
To try the commands without your own media, generate a 30 second 1080p sample with a test pattern and a tone:
mkdir -p ~/transcode && cd ~/transcode
ffmpeg -hide_banner -f lavfi -i testsrc2=size=1920x1080:rate=30 -f lavfi -i sine=frequency=440 \
-t 30 -c:v libx264 -pix_fmt yuv420p -c:a aac input.mp4
Inspect any file with ffprobe, which ships with FFmpeg. This prints the codec and resolution of the first video stream:
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate -of compact input.mp4
stream|codec_name=h264|width=1920|height=1080|r_frame_rate=30/1
Use the same command on every output below to verify the result.
Step 3 - Transcoding to H.264 with CRF
H.264 is the most compatible codec: every browser, phone and TV can play it. For files that will be stored or downloaded, use CRF (constant rate factor) mode, which targets a constant visual quality and lets the bitrate vary. Lower CRF means higher quality and larger files; 18 to 23 is typical for 1080p.
ffmpeg -hide_banner -i input.mp4 \
-c:v libx264 -preset medium -crf 21 -pix_fmt yuv420p \
-c:a aac -b:a 160k \
-movflags +faststart \
output_h264.mp4
What each option does:
-preset mediumtrades speed for compression.veryfastis several times quicker with larger files;slowcompresses better but takes longer.-pix_fmt yuv420pensures 8-bit 4:2:0 output, which is what most hardware decoders expect.-movflags +faststartmoves the index to the start of the file so browsers can begin playback before the download finishes.
To downscale at the same time, add a scale filter. -2 keeps the aspect ratio and rounds the width to an even number:
ffmpeg -hide_banner -i input.mp4 \
-vf "scale=-2:720" \
-c:v libx264 -preset medium -crf 22 -pix_fmt yuv420p \
-c:a aac -b:a 128k -movflags +faststart \
output_720p.mp4
Verify the new resolution:
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height -of compact output_720p.mp4
stream|codec_name=h264|width=1280|height=720
Step 4 - Transcoding to H.265 and VP9
H.265 (HEVC) produces files roughly 30 to 50 percent smaller than H.264 at similar quality, but encoding is slower and browser support is limited. It suits archives and players such as Jellyfin or Plex. Its CRF scale differs from x264: 24 to 28 is a good range.
ffmpeg -hide_banner -i input.mp4 \
-c:v libx265 -preset medium -crf 26 -tag:v hvc1 \
-c:a copy \
output_h265.mp4
-tag:v hvc1 makes the file playable in Apple players, and -c:a copy keeps the original audio without re-encoding it.
VP9 is royalty-free and widely supported in browsers, usually paired with Opus audio in a WebM container. With -b:v 0, the -crf value controls quality:
ffmpeg -hide_banner -i input.mp4 \
-c:v libvpx-vp9 -crf 32 -b:v 0 -row-mt 1 \
-c:a libopus -b:a 128k \
output_vp9.webm
-row-mt 1 enables row-based multithreading, which makes libvpx use more CPU cores.
Step 5 - Packaging adaptive HLS for web playback
HLS splits video into short segments and publishes playlists for several quality levels, so players switch bitrate as the network changes. The following command produces a 1080p and a 720p rendition from a single input, plus a master playlist:
mkdir -p ~/transcode/hls && cd ~/transcode/hls
ffmpeg -hide_banner -i ../input.mp4 \
-filter_complex "[0:v]split=2[v1][v2];[v1]scale=-2:1080[v1out];[v2]scale=-2:720[v2out]" \
-map "[v1out]" -c:v:0 libx264 -b:v:0 5000k -maxrate:v:0 5350k -bufsize:v:0 7500k \
-map "[v2out]" -c:v:1 libx264 -b:v:1 2800k -maxrate:v:1 3000k -bufsize:v:1 4200k \
-map 0:a:0 -map 0:a:0 -c:a aac -b:a 128k -ac 2 \
-preset veryfast -pix_fmt yuv420p \
-g 60 -keyint_min 60 -sc_threshold 0 \
-f hls -hls_time 4 -hls_playlist_type vod \
-hls_segment_filename "stream_%v_%03d.ts" \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,a:0 v:1,a:1" \
stream_%v.m3u8
The important parts:
splitandscalecreate two video streams from one decode, which is cheaper than running FFmpeg twice.-b:v,-maxrateand-bufsizecap the bitrate of each rendition so players can predict bandwidth.-g 60 -keyint_min 60 -sc_threshold 0forces a keyframe every 2 seconds (60 frames at 30 fps). Set-gto twice the frame rate of your source so segments line up across renditions.-var_stream_mappairs each video rendition with an audio copy.
List the output:
ls
master.m3u8 stream_0.m3u8 stream_0_000.ts ... stream_1.m3u8 stream_1_000.ts ...
The master playlist references both variants:
cat master.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=5640800,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
stream_0.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3440800,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
stream_1.m3u8
Serve the directory with any web server (Nginx, Caddy or object storage behind a CDN) and point an HLS player such as hls.js or Video.js at master.m3u8.
Step 6 - Using hardware encoders (optional)
GPU encoders are much faster and use little CPU, at the cost of slightly larger files for the same quality. Most VPS plans have no GPU, so skip this step unless ls /dev/dri or nvidia-smi shows a device.
Intel VAAPI
Install the Intel media driver and the vainfo tool, and add your user to the render group so it can open the GPU (log out and back in afterwards):
sudo apt install -y intel-media-va-driver-non-free vainfo
sudo usermod -aG render "$USER"
Check that the driver loads and supports encoding:
vainfo --display drm --device /dev/dri/renderD128 | grep -i enc
VAProfileH264Main : VAEntrypointEncSlice
VAProfileHEVCMain : VAEntrypointEncSlice
Decode, scale and encode on the GPU. Keeping frames in GPU memory (-hwaccel_output_format vaapi) avoids copying them back to the CPU:
ffmpeg -hide_banner -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i input.mp4 \
-vf "scale_vaapi=w=1280:h=720" \
-c:v h264_vaapi -qp 23 \
-c:a aac -b:a 128k \
output_vaapi.mp4
Use hevc_vaapi instead of h264_vaapi for H.265.
NVIDIA NVENC
Install the NVIDIA driver recommended for your card, reboot, and confirm the GPU is visible:
sudo ubuntu-drivers install
sudo reboot
nvidia-smi
ffmpeg -hide_banner -encoders | grep nvenc
V....D h264_nvenc NVIDIA NVENC H.264 encoder (codec h264)
V....D hevc_nvenc NVIDIA NVENC hevc encoder (codec hevc)
Encode with NVENC in constant quality mode. Presets range from p1 (fastest) to p7 (best quality):
ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda \
-i input.mp4 \
-vf "scale_cuda=-2:720" \
-c:v h264_nvenc -preset p5 -rc vbr -cq 23 -b:v 0 \
-c:a aac -b:a 128k \
output_nvenc.mp4
While a job runs, nvidia-smi shows the ffmpeg process and encoder utilization.
Step 7 - Automating batch transcoding
A common server task is to convert every file dropped into an incoming folder. The following script transcodes each video to H.264 MP4, writes to a temporary name so half-finished files are never picked up, and moves the source to a done folder only after success.
Create the working directories:
sudo mkdir -p /srv/transcode/{incoming,output,done}
sudo chown -R "$USER":"$USER" /srv/transcode
Create the script:
sudo nano /usr/local/bin/transcode-incoming
#!/usr/bin/env bash
set -euo pipefail
BASE="/srv/transcode"
IN="$BASE/incoming"
OUT="$BASE/output"
DONE="$BASE/done"
find "$IN" -maxdepth 1 -type f \( -iname '*.mkv' -o -iname '*.mov' -o -iname '*.avi' -o -iname '*.mp4' \) -print0 |
while IFS= read -r -d '' src; do
name="$(basename "${src%.*}")"
dst="$OUT/$name.mp4"
tmp="$OUT/.$name.partial.mp4"
[[ -e "$dst" ]] && continue
echo "Transcoding: $src"
if ffmpeg -nostdin -hide_banner -loglevel error -y -i "$src" \
-c:v libx264 -preset medium -crf 21 -pix_fmt yuv420p \
-c:a aac -b:a 160k -movflags +faststart \
"$tmp"; then
mv "$tmp" "$dst"
mv "$src" "$DONE/"
echo "Done: $dst"
else
rm -f "$tmp"
echo "Failed: $src" >&2
fi
done
-nostdin stops FFmpeg from reading the loop's input, which would otherwise swallow file names, and -print0 with read -d '' handles spaces and special characters in file names. Make the script executable and test it:
sudo chmod 755 /usr/local/bin/transcode-incoming
cp ~/transcode/input.mp4 /srv/transcode/incoming/test.mp4
transcode-incoming
Transcoding: /srv/transcode/incoming/test.mp4
Done: /srv/transcode/output/test.mp4
Schedule it every 10 minutes in your user's crontab. flock -n skips a run if the previous one is still working:
crontab -e
*/10 * * * * /usr/bin/flock -n /tmp/transcode-incoming.lock /usr/local/bin/transcode-incoming >> /srv/transcode/transcode.log 2>&1
Troubleshooting
Unknown encoder 'h264_vaapi' or No VA display found. Check that /dev/dri/renderD128 exists, that your user is in the render group (id should list it; log in again after usermod) and that vainfo succeeds.
Output has no audio. The source may have several audio tracks. List them with ffprobe -v error -select_streams a -show_entries stream=index,codec_name:stream_tags=language -of compact input.mkv and select one explicitly, for example -map 0:v:0 -map 0:a:1.
Transcoding is too slow. Use a faster preset (-preset veryfast), copy audio with -c:a copy when it is already AAC, and check CPU usage with top. On CPU-only servers, several jobs in parallel rarely beat one job using all cores.
Conclusion
You installed FFmpeg on Ubuntu 24.04, encoded H.264, H.265 and VP9 with quality-based settings, packaged adaptive HLS, used GPU encoders where available and automated an incoming folder with a safe script. As next steps, serve the HLS output behind Nginx or a CDN, add an AV1 rendition if your players support it, or feed FFmpeg into a streaming server such as Icecast or an RTMP ingest.
