Re-encode H.264 โ HEVC at ~70% smaller, verify before replacing, never touch what's already lean โ and fan the work out across every idle Apple Silicon media engine you own.
Note
Honest status: this runs in production on the author's fleet (TrueNAS + 4 Apple Silicon Macs), clearing a ~3,000-episode library. It's a set of focused, battle-tested bash scripts, not a packaged app โ you wire it to your NAS + nodes via one config file. No secrets ship in the repo; copy farm.conf.example โ farm.conf (gitignored) and go.
| ๐ค Why | ๐ง Core Concepts | ๐๏ธ Architecture |
| ๐ How It Works | ๐ก๏ธ Safety Model | ๐๏ธ Two Ways to Run |
| โก Quick Start | ๐งฐ CLI & Config | ๐ Benchmarks |
| ๐งช Cross-Platform | ๐บ๏ธ Roadmap | ๐ค Contributing |
You've got terabytes of H.264 and a pile of Apple Silicon with dedicated media engines sitting idle (they don't even compete with your GPU/ML work). Meanwhile most "transcode everything" tools either hammer one box, re-encode stuff that's already small, or replace originals with zero verification. ๐ฌ
| ๐ฌ mediaoptimizer | ๐ naive for f in *; ffmpeg |
๐ฅ๏ธ single-box transcoder | |
|---|---|---|---|
| Skips already-HEVC / HDR / low-bitrate | โ | โ | |
| Verifies output before replacing | โ codec + duration | โ | |
| Recoverable rolling trash (FIFO) | โ | โ | |
| Resumable (shared state, dedup) | โ | โ | |
| Fan out across many machines | โ N nodes | โ | โ |
| Uses idle Apple Silicon media engines | โ VideoToolbox | โ | โ |
| Survives reboot / crash | โ
launchd KeepAlive |
โ |
mindmap
root((media<br/>optimizer))
๐ฏ Decide
probe codec/res/bitrate
skip already-hevc
skip HDR / low-res / low-bitrate
๐ง Encode
QSV ICQ ยท Intel iGPU
VideoToolbox ยท Apple Silicon
only-downscale to 1080p
๐ก๏ธ Safety
encode then verify
atomic in-place replace
rolling size-capped trash
๐ Fan out
disjoint slices per node
shared state dedup
ssh pull / push
launchd daemons
Two file homes, one shared brain. The NAS holds the library + the shared state file + a tiny persistent ffmpeg container for fast probing. Each node owns a disjoint slice and does the heavy lifting on its own media engine.
flowchart LR
CTL["๐งโ๐ป control box<br/>farm-deploy.sh + farm.conf"]
subgraph NAS["๐๏ธ NAS (file server)"]
LIB["๐ media library"]
STATE["๐ shared state.tsv"]
PROBE["๐ณ hevc-probe<br/>(ffprobe, docker exec)"]
end
subgraph FLEET["๐ Apple Silicon nodes"]
N1["node 1 ยท VideoToolbox<br/>conc=3"]
N2["node 2 ยท VideoToolbox<br/>conc=3"]
N3["node 3 ยท VideoToolbox<br/>conc=2"]
end
CTL -- "scp worker + install launchd" --> FLEET
N1 & N2 & N3 -- "probe (no pull)" --> PROBE
N1 & N2 & N3 -- "pull h264 / push hevc (ssh)" --> LIB
N1 & N2 & N3 -- "append done rows" --> STATE
STATE -. "dedup: skip what's done" .-> FLEET
๐ก Single-box mode drops the fleet entirely: one Docker container on the NAS encodes in place with the Intel iGPU (QSV). Same brain, no ssh.
Each file flows through the same pipeline โ but the farm probes remotely first so it only ever pulls files it's actually going to convert (no wasting bandwidth pulling a 2 GB file just to learn it's already HEVC).
sequenceDiagram
autonumber
participant W as ๐ node worker
participant N as ๐๏ธ NAS
W->>N: remote_probe (docker exec ffprobe) โ no pull
N-->>W: codec / res / bitrate / duration
alt already-hevc ยท HDR ยท low-res ยท low-bitrate
W->>N: record skip (no transfer) โ
else convert
W->>N: pull source over ssh โฌ๏ธ
W->>W: VideoToolbox encode โ verify (codec+duration) ๐
alt verified & โฅ MIN_GAIN%
W->>N: push HEVC โฌ๏ธ
W->>N: atomic replace (orig โ rolling trash) ๐
W->>N: append "done" to shared state ๐
else failed / no gain
W->>N: keep original, record reason ๐
end
end
Originals are precious. Nothing gets replaced unless the new file is provably good.
| Guard | What it does |
|---|---|
| ๐ Verify | Output must be HEVC and within 1% of source duration, or the replace is aborted |
| ๐ Min-gain | If the HEVC isn't at least MIN_GAIN_PCT smaller, the original is kept |
| ๐๏ธ Rolling trash | Replaced originals go to a size-capped (TRASH_CAP_GB) trash in the same dataset โ the swap is an instant atomic rename, and recent originals stay recoverable (FIFO by insertion time) |
| ๐ง Atomic | mv within one dataset โ never a half-written file at the real path |
| ๐ Single instance | flock (Linux) / atomic mkdir+PID lock (macOS) per node |
| ๐งฎ Free-space floor | Single-box pauses below MIN_FREE_GB; ๐ the farm also skips the remote replace if the NAS pool is below NAS_MIN_FREE_GB (the file is left for a later pass, never pushed onto a full pool) |
| โป๏ธ Restore | ๐ hevcctl restore <path> pulls an original back out of the rolling trash โ one-command undo of a bad conversion |
| ๐ฌ Subtitle pre-check | ๐ An ffprobe check skips the doomed first encode when image subs (PGS/DVD/DVB) can't fit the target container โ goes straight to the -sn pass instead of burning a full failed attempt |
| ๐๏ธ Shared-state compaction | ๐ The append-only NAS state.tsv is opportunistically deduped (one node wins an atomic mkdir lock per pass) so it can't grow unbounded or bloat every node's per-pass re-read |
| ๐ฅ๏ธ Single box (QSV) | ๐ Distributed (VideoToolbox) |
|---|---|
|
Managed by |
Deployed by |
๐งฌ Both share the same
hevc-convert.shcore (cross-platform:ENCODER=qsv|videotoolbox, BSD/GNU shims, portable lock). The farm worker is a thin ssh transport around that same decide/encode/verify logic.
Prereqs: each node has bash 5+ & ffmpeg (with hevc_videotoolbox) via Homebrew, passwordless ssh to the NAS, and passwordless sudo for the probe container. The NAS runs a persistent probe container (see below).
# 0. on the NAS โ start the persistent probe container (one time)
docker run -d --name hevc-probe --restart unless-stopped \
-v /srv/media:/media --entrypoint sleep \
lscr.io/linuxserver/ffmpeg:latest infinity
# 1. configure
git clone <your-fork> mediaoptimizer && cd mediaoptimizer/scripts
cp farm.conf.example farm.conf
$EDITOR farm.conf # nodes, slices, NAS host, paths
# 2. sanity-check the config, then deploy auto-restarting daemons to every node
./farm-deploy.sh check # ๐ lint: NAS path, hosts reachable, slices disjoint
./farm-deploy.sh # all nodes
./farm-deploy.sh status # pulse checkStop waiting for the hourly rescan. Point Sonarr/Radarr at hevc-enqueue.sh and new media converts within ~60s:
Settings โ Connect โ + โ Custom Script ยท Path:
hevc-enqueue.shยท Triggers: On Import + On Upgrade
# *arr runs the script inside its container, so map its path to the NAS-host path the workers pull:
# QUEUE_FILE=/tv/.hevc-queue PATH_MAP=/tv=/mnt/tank/media/videos/TV
# Test it by hand:
./hevc-enqueue.sh /mnt/tank/media/videos/TV/Show/S01E01.mkv # -> appended to .hevc-queuesequenceDiagram
autonumber
participant A as Sonarr/Radarr
participant Q as .hevc-queue (NAS)
participant W as farm worker
A->>Q: On Import โ hevc-enqueue.sh appends path
W->>Q: poll every QUEUE_POLL_SECS (60s)
Q-->>W: queued path (atomic mv, one node wins)
W->>W: claim โ pull โ encode โ verify โ atomic replace
MEDIA_DIR=/srv/media WORKDIR=/srv/hevc ./hevcctl.sh start
./hevcctl.sh status| Command | Does |
|---|---|
./farm-deploy.sh |
Deploy worker + launchd daemon to all nodes |
./farm-deploy.sh <host> |
Deploy to one node |
./farm-deploy.sh check |
๐ Lint farm.conf โ NAS path, host reachability, disjoint slices, numeric CONC, + missing-key drift vs farm.conf.example. Run before deploy. |
./farm-deploy.sh status |
Daemon state + recent log per node ยท in-flight claim count ยท hevc-probe container state |
./farm-deploy.sh drain |
๐ Graceful stop โ drop a .drain flag on every node; each worker finishes its current file then exits cleanly (no wasted half-encode). The next deploy clears the flag. |
./farm-deploy.sh failed ยท retry |
๐ Tally failed files by reason ยท clear them from shared state so the next scan re-attempts |
./farm-deploy.sh reverify |
๐ Sample-decode already-converted files (REVERIFY_SAMPLE) to catch silent corruption โ originals are gone, so it alerts |
./farm-deploy.sh kick ยท stop |
Force-restart all daemons ยท bootout all daemons |
./farm-watchdog.sh |
๐ Self-heal: re-bootstrap any node whose launchd job isn't running, ntfy alert + dead-man heartbeat. Cron every ~10 min. |
| Command | Does |
|---|---|
start / stop / restart |
Manage the single-box QSV container |
status ยท savings |
Progress tally + pool free ยท lifetime size-saved from the durable ledger |
failed ยท retry |
๐ List failures by reason ยท restart with RETRY_FAILED=1 |
restore <path> |
๐ โป๏ธ Undo a bad conversion โ pull the original back from .hevc_trash (newest match) and overwrite the converted file |
logs [N] ยท stats |
Tail the log ยท live container stats |
| Command | Does |
|---|---|
./scripts/hevc-estimate.sh <root> |
๐ Dry run โ probe + classify a library and project total reclaim before you convert (EST_RATIO) |
./scripts/hevc-digest.sh |
๐ Daily savings digest (last SINCE_HOURS) โ ntfy or stdout. Cron it. |
./scripts/vmaf-sample.sh <filesโฆ> |
๐ Measure mean VMAF of a few sample encodes so you can set VMAF_MIN from data, not a guess |
./install.sh |
๐ Symlink hevcctl/farm-deploy onto PATH + seed farm.conf (no brew tap needed) |
./scripts/test.sh |
๐ Zero-dep regression gate: bash -n every script + lib/enqueue/estimate/digest selfchecks + e2e |
./scripts/test-e2e.sh |
๐ Real encodeโverify smoke test (generates a clip, libx265โ shared verify()); self-skips if ffmpeg absent |
๐ค CI: every push runs
test.sh+shellcheck --severity=warning+ the e2e encode on GitHub Actions (and Forgejo Actions, given a runner)..shellcheckrcdocuments why info-level ssh-expansion findings are advisory.
| Env | Effect |
|---|---|
PLEX_PAUSE=1 + PLEX_TOKEN |
Farm waits while Plex has a live transcode (MAX_PLEX_WAIT_MIN cap) |
ARR_URL + ARR_KEY (ARR_KIND=sonarr|radarr) |
After a pass replaces a file, tell *arr to re-read it (debounced 1/pass) |
SPACE_GUARD=1 (default on) |
Skip a file if the worker's local disk can't hold ~2.2ร its size |
EXCLUDE |
Newline globs the farm never touches (keep a grain master untouched) |
| Key | Meaning |
|---|---|
HOSTS |
Array of node ssh hosts |
SLICE[host] |
Disjoint newline-separated library subdirs per node |
CONC[host] |
Concurrent encodes per node (Ultra โ 3, Max/laptop โ 2) |
NAS ยท REMOTE_ROOT ยท STATE_REMOTE |
File server host, library root, shared state path |
PROBE_CTR ยท MEDIA_HOST ยท MEDIA_CANON |
Probe container + hostโcontainer path remap |
NODE_USER ยท NODE_DIR ยท NODE_BASH ยท LABEL |
Per-node daemon identity & install paths |
๐๏ธ Tuning knobs (env, both modes)
| Var | Default | Meaning |
|---|---|---|
VT_QUALITY |
60 |
VideoToolbox -q:v (1โ100) |
QUALITY |
22 |
QSV ICQ global_quality |
MAX_WรMAX_H |
1920ร1080 |
Only-downscale ceiling |
MIN_SRC_KBPS |
3000 |
Skip sources already leaner |
MIN_GAIN_PCT |
8 |
Output must be this % smaller |
TRASH_CAP_GB |
80 |
Rolling trash cap per dataset |
CONCURRENCY |
1 |
Parallel encodes per worker |
DRY_RUN ยท LIMIT ยท ONESHOT |
0 |
Preview ยท cap files ยท one pass (great for testing) |
Measured on the author's fleet: 2ร M3 Ultra, 1ร M1 Ultra, 1ร M3 Max, feeding from a TrueNAS box. Your mileage varies with NAS bandwidth & node count.
| Metric | Result |
|---|---|
| ๐ Size reduction | ~70% average (H.264 โ HEVC, VMAF stays high) |
| ๐ VideoToolbox speed | ~36ร realtime on a 1080p segment (single stream) |
| ๐ Fleet throughput | ~60โ100 converts/hour at 11 concurrent across 4 nodes |
| โฑ๏ธ ~1,600-file backlog | days on one NAS iGPU โ under a day on the farm |
โ๏ธ Reality check: 3ร concurrency โ 1.5โ2ร throughput, not 3ร. The ceiling is the shared media engine per machine + NAS pull bandwidth, not idle compute โ so past ~3-per you just add contention.
| ๐ฅ๏ธ Linux / Intel | ๐ macOS / Apple Silicon | |
|---|---|---|
| Encoder | hevc_qsv (ICQ) |
hevc_videotoolbox (-q:v) |
| Lock | flock |
atomic mkdir + PID |
stat / df |
GNU | BSD shims |
| Role | single-box, in-place | farm node, ssh pull/push |
| Deploy | Docker container | launchd daemon |
flowchart LR
A["โ
QSV single-box"] --> B["โ
VideoToolbox farm"]
B --> C["โ
concurrency + config"]
C --> G["โ
event-driven (*arr)"]
G --> H["โ
VMAF gate ยท AV1 ยท tiers"]
H --> I["โ
Plex-pause ยท reverify ยท digest"]
I --> J["โ
restore ยท drain ยท sub-precheck"]
J --> D["๐จ auto-balance slices"]
D --> F["โฌ web dashboard"]
| Status | Item |
|---|---|
| โ | Single-box QSV converter (Docker, in-place, verified) |
| โ | Distributed VideoToolbox farm (ssh pull/push, atomic replace) |
| โ | Remote-probe optimization (no pull-to-skip) |
| โ | Per-node concurrency + externalized farm.conf |
| โ | launchd auto-restart daemons (survives reboot/crash) |
| โ | HDR / Dolby Vision auto-skip (never flattens a master) |
| โ | Event-driven convert โ *arr On Import โ hevc-enqueue.sh โ ~60s latency |
| โ | Perceptual quality gate โ opt-in VMAF floor (VMAF_MIN) before replacing originals |
| โ | AV1 opt-in (VT_CODEC=av1) with per-box capability probe + HEVC fallback |
| โ | Self-healing farm-watchdog.sh (re-bootstrap dead nodes + ntfy + dead-man heartbeat) |
| โ | Plex-pause for farm workers + *arr refresh-after-replace + pre-pull space guard + path excludes |
| โ | Re-verify sweep (farm-deploy reverify) โ spot-decode converted files for silent corruption |
| โ | Savings digest (hevc-digest.sh) + dry-run estimator (hevc-estimate.sh) + VMAF baseline sampler |
| โ | Per-resolution quality tiers ยท farm-deploy check/retry ยท test.sh ยท install.sh |
| โ | One-command undo (hevcctl restore) ยท graceful farm-deploy drain ยท subtitle pre-check ยท per-file failure stderr ยท conf-drift lint ยท claims/probe in status |
| โ | Shared-state compaction (locked, per-pass) + NAS free-space guard before the farm replace |
| โ | CI + real e2e test โ GitHub/Forgejo Actions run test.sh + shellcheck + a live encodeโverify on every push |
| ๐จ | Auto-balance slices by measured node throughput |
| โฌ | Web dashboard / live progress UI |
| โฌ | Optional NFS/SMB transport where the OS cooperates |
PRs welcome! Keep it bash-portable (works under the Linux/macOS shims), and never let a doc lie about the code. Before any change to behavior, run a DRY_RUN=1 LIMIT=1 pass against a test slice.
๐ License: Apache 2.0 โ permissive, patent-grant included. Use it, fork it, ship it.