6.2 KiB
| title | domain | category | tags | status | created | updated | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| HEVC Batch Re-Encode for Plex Using VAAPI (AMD GPU) | streaming | plex |
|
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 30–50% 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:
- Encode source →
.hevc.tmp.mp4alongside the original - Verify the output with
ffprobe touch -r original.mp4 temp.mp4— copy mtime (cosmetic, not required)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_vaapidoes not support 10-bit output on Polaris (RX 480/580). For 10-bit HDR sources, fall back tolibx265with 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
Related
- plex-4k-codec-compatibility — Apple TV Direct Play compatibility, HEVC HDR notes
- snapraid-mergerfs-setup — MajorRAID storage pool setup
- SnapRAID-Majorhome — majorhome SnapRAID project