majorwiki/04-streaming/plex/hevc-vaapi-batch-encode.md

6.2 KiB
Raw Blame History

title domain category tags status created updated
HEVC Batch Re-Encode for Plex Using VAAPI (AMD GPU) streaming plex
plex
ffmpeg
hevc
vaapi
amd
gpu
encode
storage
rx480
published 2026-05-15 2026-05-15

HEVC Batch Re-Encode for Plex Using VAAPI (AMD GPU)

Problem

Plex NVMe storage is filling up from a large library of H.264-encoded video files (YouTube downloads, stream archives, etc.). Re-encoding to HEVC (H.265) reclaims 3050% of disk space. The catch: Plex tracks each file's "date added" in a SQLite database, and that order matters for playback queues. Naive re-encode-and-replace approaches can corrupt or reset that metadata.

Solution

Use ffmpeg with hevc_vaapi (AMD GPU hardware encoder) to batch re-encode files in-place using an atomic rename swap that preserves the Plex database record — including added_at — without any Plex downtime or database editing.


How Plex Stores "Date Added"

Plex does not use file modification time (mtime) for "date added." It stores a Unix timestamp in its SQLite database:

-- Plex DB location (override via systemd unit may differ — check):
-- /var/lib/plexmediaserver/Library/Application Support/Plex Media Server/
--   Plug-in Support/Databases/com.plexapp.plugins.library.db
-- (or wherever PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR points)

SELECT mi.added_at, datetime(mi.added_at, 'unixepoch'), mp.file
FROM metadata_items mi
JOIN media_items me ON me.metadata_item_id = mi.id
JOIN media_parts mp ON mp.media_item_id = me.id
WHERE mp.file LIKE '%your-file%';

Note: If the default path returns 0 rows, check your actual data directory:

systemctl cat plexmediaserver | grep APPLICATION_SUPPORT

The added_at field is keyed to the file path in media_parts. As long as the file path doesn't change, the database record — including added_at — is untouched even after the file's content is replaced.


Why VAAPI Instead of libx265

On a host with an AMD RX 480/580 (or similar Polaris GPU), hardware HEVC encoding via VAAPI is roughly 9× faster than software libx265 at comparable quality:

Encoder Speed (1080p) Notes
libx265 -preset medium ~21 fps / 0.35× Best quality/size ratio
hevc_vaapi QP 28 ~186 fps / 3.1× Sufficient for streaming content

For 1080p streaming content (game streams, podcasts, YouTube archival), the quality difference is imperceptible. libx265 is preferable only for archival encodes where absolute quality matters.

Verify VAAPI is working

vainfo 2>&1 | grep -E "vaapi|HEVC|hevc|Driver"
ls /dev/dri/renderD128

You need VAProfileHEVCMain : VAEntrypointEncSlice in the output. If missing, install mesa-va-drivers-freeworld (RPM Fusion) for AMD hardware.


The Atomic Swap Strategy

The key insight: mv file.tmp file on the same filesystem is an atomic inode rename at the kernel level. Plex sees the same path still present — it never fires a "file removed" event, so the metadata_items record (including added_at) is preserved.

Safe sequence:

  1. Encode source → .hevc.tmp.mp4 alongside the original
  2. Verify the output with ffprobe
  3. touch -r original.mp4 temp.mp4 — copy mtime (cosmetic, not required)
  4. mv temp.mp4 original.mp4 — atomic replace

The one pitfall: if the original file is deleted before the mv, Plex orphans the DB record (removes metadata_items entry on next scan) and re-indexes the new file with a fresh added_at. The original must still exist at swap time.


The Batch Script

Script lives at ~/hevc_batch.sh on majorhome.

# Dry run — scan and report what would be encoded, no changes
bash ~/hevc_batch.sh --dry-run

# Full run (default: files >1GB, QP 28)
tmux new-session -d -s hevc_batch 'bash ~/hevc_batch.sh'

# Custom options
bash ~/hevc_batch.sh --min-size-gb 2 --qp 26

Queue and resume

The script writes a queue file at ~/hevc_queue.txt on first run (scanning all files with ffprobe — takes ~10 min for a large library). On subsequent runs it resumes from where it left off. Completed files are logged to ~/hevc_done.txt. Failed files go to ~/hevc_failed.txt.

To restart from scratch: rm ~/hevc_queue.txt ~/hevc_done.txt

Log output

# Structured log lines only (skip ffmpeg progress noise)
grep '^\[20' ~/hevc_batch.log

# Watch live progress
tail -f ~/hevc_batch.log | grep '^\[20'

Each file logs:

  • Source size and codec
  • Plex added_at before: <unix timestamp>
  • ffmpeg exit code and elapsed time
  • Output size and savings
  • DB check: added_at PRESERVED ✓ (or WARN if changed)

Space guard

The script aborts if free space on the Plex volume drops below 20GB (MIN_FREE_GB). Worst-case headroom needed is source_size + tmp_size simultaneously — on a 4GB source file that's ~8GB peak.


ffmpeg Command

ffmpeg \
  -vaapi_device /dev/dri/renderD128 \
  -i "input.mp4" \
  -vf 'format=nv12,hwupload' \
  -c:v hevc_vaapi -rc_mode CQP -qp 28 \
  -c:a copy \
  -movflags +faststart \
  -y "output.tmp.mp4"
  • -rc_mode CQP -qp 28 — constant quantizer; higher value = smaller file / lower quality. QP 24 is high quality, QP 28 is good for streaming content.
  • -vf 'format=nv12,hwupload' — required to move frames to GPU memory for VAAPI encoding.
  • -c:a copy — passes audio through untouched.
  • hevc_vaapi does not support 10-bit output on Polaris (RX 480/580). For 10-bit HDR sources, fall back to libx265 with color signaling flags.

Plex Data Directory Override

On majorhome, the Plex data directory is overridden in the systemd unit — the default path /var/lib/plexmediaserver/ is empty:

systemctl cat plexmediaserver | grep APPLICATION_SUPPORT
# Environment=PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=/plex/plexdata/Library/Application Support

The actual DB path is therefore:

/plex/plexdata/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db