Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

26 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽฌ mediaoptimizer

Shrink your whole media library to HEVC โ€” on one box, or across a whole fleet of Macs ๐Ÿš€

shell platform encoders deps status CI license

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.


๐Ÿ—บ๏ธ Table of Contents

๐Ÿค” Why ๐Ÿง  Core Concepts ๐Ÿ—๏ธ Architecture
๐Ÿ”„ How It Works ๐Ÿ›ก๏ธ Safety Model ๐ŸŽ›๏ธ Two Ways to Run
โšก Quick Start ๐Ÿงฐ CLI & Config ๐Ÿ“Š Benchmarks
๐Ÿงช Cross-Platform ๐Ÿ—บ๏ธ Roadmap ๐Ÿค Contributing

๐Ÿค” Why

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 โœ… โŒ โš ๏ธ sometimes
Verifies output before replacing โœ… codec + duration โŒ โš ๏ธ rarely
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 โŒ โš ๏ธ

๐Ÿง  Core Concepts

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
Loading

๐Ÿ—๏ธ Architecture

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
Loading

๐Ÿ’ก 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.


๐Ÿ”„ How It Works

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
Loading

๐Ÿ›ก๏ธ Safety Model

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

๐ŸŽ›๏ธ Two Ways to Run

๐Ÿ–ฅ๏ธ Single box (QSV)๐ŸŒ Distributed (VideoToolbox)

hevc-convert.sh in a Docker ffmpeg container, encoding in place with an Intel iGPU. Gentle: niced, sleeps between files, pauses while Plex transcodes.

Managed by hevcctl.sh.

farm-worker.sh on each Apple Silicon Mac โ€” pulls over ssh, encodes with VideoToolbox, pushes back, replaces on the NAS. N nodes ร— concurrency.

Deployed by farm-deploy.sh + farm.conf.

๐Ÿงฌ Both share the same hevc-convert.sh core (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.


โšก Quick Start

๐ŸŒ The farm (Apple Silicon nodes + a NAS)

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 check

๐Ÿ”” Event-driven โ€” convert on import (*arr)

Stop 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-queue
sequenceDiagram
    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
Loading

๐Ÿ–ฅ๏ธ Single box (Intel QSV, in Docker)

MEDIA_DIR=/srv/media WORKDIR=/srv/hevc ./hevcctl.sh start
./hevcctl.sh status

๐Ÿงฐ CLI & Config

farm-deploy.sh

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.

hevcctl.sh

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

๐Ÿ› ๏ธ Standalone helpers

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). .shellcheckrc documents why info-level ssh-expansion findings are advisory.

๐ŸŽš๏ธ Worker behavior knobs (optional, all default to no-op)

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)

farm.conf (sourced; the only place your real values live โ€” gitignored)

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)

๐Ÿ“Š Benchmarks

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.


๐Ÿงช Cross-Platform

๐Ÿ–ฅ๏ธ 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

๐Ÿ—บ๏ธ Roadmap

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"]
Loading
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

๐Ÿค Contributing

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.


๐Ÿฟ Point it at your library, walk away, come back to half the disk usage.

Built for hoarders with too many Macs and not enough SSD. ๐Ÿ’พโœจ

About

Shrink your media library to HEVC โ€” single-box QSV, or a distributed VideoToolbox farm across Apple Silicon Macs ๐ŸŽฌ

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages