# HVAC — Get your TBs back
[](https://github.com/JackDanger/hvac/actions/workflows/ci.yml)
[](https://crates.io/crates/hvac-transcoder)
[](https://docs.rs/hvac-transcoder)
[](LICENSE)
Point `hvac` at a directory that contains videos — even ones hidden inside `.img` and `.iso` files — and it'll compress them to `h.265` (`HEVC`) using reasonable defaults. You can overwrite these defaults with a small config file.
You need a GPU with an HEVC encoder (NVIDIA NVENC, Intel VAAPI, or
Apple VideoToolbox) and an ffmpeg built against it. The installer below
auto-installs ffmpeg on macOS, Debian/Ubuntu, Alpine, and OpenMediaVault,
and prints platform-specific guidance on Synology, QNAP, and Unraid.
For everything else there's [Docker](#docker) or the
[NAS-specific notes](docs/NAS.md).
---
## Install
**Debian / Ubuntu**
```bash
curl -fsSL https://jackdanger.github.io/HVAC/key.gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/hvac.gpg
echo "deb [signed-by=/etc/apt/keyrings/hvac.gpg] https://jackdanger.github.io/HVAC stable main" \
| sudo tee /etc/apt/sources.list.d/hvac.list
sudo apt update && sudo apt install hvac
```
Future releases arrive via `sudo apt upgrade`.
**macOS**
```bash
brew install JackDanger/tap/hvac
```
Future releases arrive via `brew upgrade hvac`.
**All platforms**
```bash
The script uses Homebrew on macOS and the apt repository on Debian/Ubuntu; other Linux distros and NAS platforms get a pre-built tarball with platform-specific guidance.
Other ways: `cargo install hvac-transcoder` · [AUR](https://aur.archlinux.org/packages/hvac) · [tarballs](https://github.com/JackDanger/hvac/releases) · [Docker](#docker) · [Synology / QNAP / Unraid](docs/NAS.md)
---
## Use
First time on a library you care about, do a dry run:
```bash
hvac --dry-run /path/to/movies # preview, change nothing
```
Once you've eyeballed the list, drop `--dry-run`:
```bash
hvac /path/to/movies # overwrite in place (default)
hvac --no-overwrite /path/to/movies # write .transcoded.mkv copies, keep originals
```
It scans the directory, skips files that already meet the target, and re-encodes
the rest. Re-running picks up where you left off — there's a sidecar
`.hvac.complete` next to each output that records source size + duration so the
next run knows whether to adopt it or re-encode. Ctrl-C is safe; in-progress
encodes leave a `.hvac_tmp_*` file that the next run sweeps.
---
## Disc images (.iso / .img)
When the input is a Blu-ray or DVD image, hvac analyses the disc structure,
picks the main feature (largest m2ts run for Blu-ray, largest VTS for DVD;
multi-title DVDs become one output per title), and pipes the streams into
ffmpeg without ever extracting to a temp directory.
A disc usually carries several audio tracks — the primary mix, a commentary,
sometimes a dub or audio description. hvac selects **exactly one** as the
output audio. The picker (see `pick_primary_audio` in `src/probe.rs`):
1. Drops tracks flagged as commentary (by `disposition.comment` or by title
keyword — "Commentary", "Director's", "Audio description", etc.).
2. Among what's left, prefers the track with the most channels (a 5.1
surround mix beats a 2-channel commentary on any modern release).
3. Tiebreaks on bitrate, then the muxer's `default` flag, then stream index.
**Year-aware flip.** When the disc image filename carries a release year
earlier than 1955 — pre-stereo cinema — the channel preference inverts: a
1-channel mono original beats a 2-channel commentary. The year is parsed
from the filename (`The Maltese Falcon (1941).iso`, `Movie.Title.1933.iso`),
falling back to non-commentary track titles when the filename is opaque
(e.g. a raw `BDMV_DISC.iso`).
**Ambiguous selections.** Some discs have a genuine coin flip — two AC3
2.0 192k tracks on a DVD with no disposition flag set, or every track
flagged as commentary. By default hvac picks one anyway and logs a
`WARN`. To skip those discs instead, opt in:
```bash
hvac --skip-ambiguous-audio /path/to/movies
```
or, persistently, in `config.yaml`:
```yaml
skip_ambiguous_audio: true
```
Either source enables it (CLI on **or** config on → skip). Skipped discs
print one line each to stderr with the reason so you can review them by
hand.
---
## Does it actually save space?
Real numbers from one library — public domain films, full bitrate range from pristine remuxes to lo-fi early transfers:
| Nosferatu (1922) Remux-1080p.mkv | 27.2 GB | 4.1 GB | **-84%** |
| The Blood of a Poet (1932) Bluray-1080p.mkv | 3.3 GB | 1.0 GB | **-69%** |
| Battleship Potemkin (1925) Remux-1080p.mkv | 21.0 GB | 3.5 GB | **-83%** |
| Sherlock Jr. (1924) Remux-1080p.mkv | 22.7 GB | 4.6 GB | **-79%** |
| Way Down East (1920) Remux-1080p.mkv | 17.5 GB | 3.2 GB | **-81%** |
| The Black Pirate (1926) Remux-1080p.mkv | 21.1 GB | 4.7 GB | **-77%** |
| Intolerance (1916) Bluray-1080p.mkv | 13.9 GB | 3.2 GB | **-77%** |
| The Gold Rush (1925) Bluray-1080p.mkv | 15.0 GB | 4.1 GB | **-72%** |
| Metropolis (1927) Remux-1080p.mkv | 798 MB | 120 MB | **-84%** |
| Our Hospitality (1923) Bluray-1080p.mkv | 11.4 GB | 4.2 GB | **-63%** |
| The Boat (1921) Bluray-1080p.mkv | 2.2 GB | 839 MB | **-62%** |
| Safety Last! (1923) WEBDL-1080p.mkv | 6.1 GB | 3.1 GB | **-49%** |
| The Phantom of the Opera (1925) Bluray-1080p.mkv | 4.8 GB | 2.5 GB | **-48%** |
| The Blacksmith (1922) Bluray-1080p.mkv | 1.5 GB | 670 MB | **-55%** |
| The Blot (1921) Bluray-1080p.mkv | 3.9 GB | 2.5 GB | **-34%** |
| The General (1926) Bluray-1080p.mkv | 4.9 GB | 3.4 GB | **-31%** |
| The Navigator (1924) Bluray-1080p.avi | 700 MB | 406 MB | **-42%** |
| The Kid (1921) Remux-1080p.mkv | 5.4 GB | 1.1 GB | **-80%** |
| Strike (1925) Bluray-1080p.mkv | 5.3 GB | 3.3 GB | **-37%** |
**Average across the full library: ~65% smaller.**
---
## GPU required
| NVIDIA (Kepler+) | `hevc_nvenc` | Linux |
| Intel (Broadwell+) | `hevc_vaapi` | Linux |
| Apple Silicon / Intel Mac | `hevc_videotoolbox` | macOS |
No GPU, no go — `hvac` exits with a clear message. CPU h265 is too slow to be worth shipping.
---
## Config
The defaults are sensible. To tune quality, presets, max resolution, etc.:
```bash
hvac --dump-config > config.yaml
$EDITOR config.yaml
hvac --config config.yaml /path/to/movies
```
---
## Docker
If you'd rather not install ffmpeg + drivers + the binary on the host —
or if the host is a NAS where those don't behave — there's a container
image with everything pre-wired.
```bash
# Intel iGPU (Broadwell+)
docker run --rm \
--device /dev/dri:/dev/dri \
--user "$(id -u):$(id -g)" \
-v /path/to/media:/media \
ghcr.io/jackdanger/hvac:latest --dry-run /media
# NVIDIA (needs nvidia-container-toolkit on the host)
docker run --rm \
--gpus all --runtime=nvidia \
--user "$(id -u):$(id -g)" \
-v /path/to/media:/media \
ghcr.io/jackdanger/hvac:latest --dry-run /media
```
The image ships configured to run as UID 1026 GID 100 — Synology / Unraid
/ OMV's default admin user. On a regular Linux host that UID probably
doesn't own your media, so the `--user "$(id -u):$(id -g)"` above
remaps the container's user to yours; drop the flag if you're on a
NAS and your admin account already matches 1026:100.
For compose, copy [`compose.example.yml`](compose.example.yml) and edit
the volume path. The image is built and published to GHCR by the
[`docker.yml`](.github/workflows/docker.yml) workflow on every push to
`main`; until the first push lands you can build it locally from this
repo's [`Dockerfile`](Dockerfile):
```bash
docker build -t hvac .
docker run --rm --device /dev/dri:/dev/dri -v /path/to/media:/media \
hvac --dry-run /media
```
NAS-specific instructions (Synology Container Manager, QNAP Container
Station, Unraid Community Applications, TrueNAS SCALE, OpenMediaVault)
live in [`docs/NAS.md`](docs/NAS.md). If your NAS has no GPU, that doc
also covers the "mount over NFS and transcode off-box" pattern.
---
## Troubleshooting
**"No GPU found for h265 encoding!"**
- macOS: nothing to do — Apple Silicon and all post-2017 Macs have
VideoToolbox built in. If you still see this, your shell is missing
`ffmpeg`; `brew install ffmpeg`.
- Linux + Intel iGPU: `ls -la /dev/dri` — if `renderD128` isn't there,
load the driver (`sudo modprobe i915` on most distros) and install
`intel-media-va-driver` + `vainfo`.
- Linux + NVIDIA: `nvidia-smi` should print your card. If it doesn't,
install the proprietary driver and reboot. The open-source `nouveau`
driver has no NVENC.
- Docker / NAS: pass the device. `--device /dev/dri:/dev/dri` for
Intel; `--gpus all --runtime=nvidia` for NVIDIA. See
[`docs/NAS.md`](docs/NAS.md).
**"Can I do CPU encoding instead?"** No, by design. x265 at the quality
the defaults target runs at ~5 fps on a fast desktop CPU. A 2-hour
movie is 6+ hours of wall time vs. 5 minutes on a $50 used Quadro. If
you're on a NAS without a GPU, see [`docs/NAS.md`](docs/NAS.md) for
the off-box pattern.
**"Will it touch my files?"** It overwrites by default — only after
the new encode has passed an ffprobe duration + codec + min-size
check, and only via an atomic rename of a `.hvac_tmp_…` sidecar over
the original. The first run on a library you care about should be
`hvac --dry-run`, then `hvac --no-overwrite`; the latter writes
`.transcoded.<ext>` copies you can compare before committing with
`--replace`.
**"It's stuck on a single file."** ffprobe has a watchdog
(`--probe-timeout`, default 30 s); the directory walk doesn't. If
your media lives on a flaky NFS / SMB mount and the scan hangs, that
hang is on the kernel's mount layer, not hvac. Raise the probe
timeout on slow NAS shares: `hvac --probe-timeout 120 /path`.
**"I want to stop it cleanly."** Ctrl-C once — workers finish their
current file, then exit. Ctrl-C twice — force quit; in-progress
`.hvac_tmp_*` files are swept on the next run. Resume is automatic.
---
## Controlling resource usage during multi-day transcodes
For long-running batch transcodes (a media library of thousands of files takes days) you may want a way to observe progress, push tuning changes, or kill the process from outside without losing partial work. hvac integrates with LaunchDarkly to support that — the binary connects to your project at startup if you pass an SDK key:
**Full disclosure:** At the time of writing, I work at LaunchDarkly. I drive my whole homelab config with it.
1. Provision the LaunchDarkly project once: `hvac --setup-launchdarkly --ld-api-key <YOUR_LD_API_KEY>`
2. Note the SDK key it prints. Pass it on each long-running invocation:
```
hvac --launchdarkly-sdk-key <SDK_KEY> /path/to/media
```
3. With a key supplied, hvac connects to LaunchDarkly's evaluation endpoint and exports per-encode OpenTelemetry spans to LaunchDarkly Observability, so you can watch live progress and timing in the LD dashboard.
The three flags that are active during a run:
| `pause-transcoding` | boolean | Workers finish their current file, then spin until you set it back to false |
| `enable-transcoding` | boolean | Kill-switch — workers stop picking up new files when set to false |
| `max-parallel-jobs` | integer | Override the parallel encoder count on the fly (0 = auto) |
The SDK key is **CLI-only** by design — it does not read from any environment variable. This is deliberate: hvac controls expensive GPU/disk resources, and a key that lives in your shell rc would silently apply to every run. Keep the key in a secure location and pass it explicitly when you want remote observability/control to be active.
---
## Development
Hook the lint checks up once per clone:
```bash
git config core.hooksPath .githooks
```
After that, every commit that touches a `.rs` file runs:
1. `cargo fmt --all -- --check` — sub-second; the commit is rejected if any file would be reformatted. Fix with `cargo fmt --all`.
2. `cargo clippy -- -D warnings` — slower (5-30s cold, <2s incremental); rejected if any lint fires. Both checks mirror exactly what CI enforces.
To bypass the slow check for a quick fix-up commit (you've already run clippy yourself or are about to squash anyway): `HVAC_SKIP_CLIPPY=1 git commit ...`. To bypass both: `git commit --no-verify`.
For more on the code layout, retry tiers, and concurrency model, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
---
## Contributing
Pull requests welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for the
PR checklist, code style notes, and release flow. Bugs go on the
[issues tracker](https://github.com/JackDanger/hvac/issues/new/choose);
security-sensitive reports go to the address in
[`SECURITY.md`](SECURITY.md) instead.
The [`CHANGELOG.md`](CHANGELOG.md) tracks user-visible changes in
[Keep a Changelog](https://keepachangelog.com/) format.
---
## License
[MIT](LICENSE) © Jack Danger