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 sudo privileges.
  • 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 medium trades speed for compression. veryfast is several times quicker with larger files; slow compresses better but takes longer.
  • -pix_fmt yuv420p ensures 8-bit 4:2:0 output, which is what most hardware decoders expect.
  • -movflags +faststart moves 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:

  • split and scale create two video streams from one decode, which is cheaper than running FFmpeg twice.
  • -b:v, -maxrate and -bufsize cap the bitrate of each rendition so players can predict bandwidth.
  • -g 60 -keyint_min 60 -sc_threshold 0 forces a keyframe every 2 seconds (60 frames at 30 fps). Set -g to twice the frame rate of your source so segments line up across renditions.
  • -var_stream_map pairs 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.