ADR-008: transcoding is optional, hardware-gated, off by default
ADR-002 decided never to transcode because the server is a headless, GPU-less Xeon, and its measurements still hold. The problem is scope: "this server cannot transcode" became "Vellum does not transcode", and only the first is forced by the hardware. Intel Quick Sync has been in most desktop CPUs since 2015, NVENC is in every recent Nvidia card, and VAAPI covers AMD and Intel on Linux. Capping the product at the hardware in one closet is the wrong constraint, and the gap is visible: a client that cannot decode HEVC gets nothing, and in Chrome that is not even a clean refusal - the video never decodes and audio plays over a blank frame. So the engine is added and its registration is gated: - registered only when usable transcode hardware AND ffmpeg are both present - off by default; direct play stays the default and the preference - consulted only when no other engine can direct-play the file, which is exactly the case that returns "no engine can play this" today - hardware-accelerated only. No CPU fallback: that is the stall ADR-002 measured, and it fails slowly, which is worse than refusing. This deployment is unchanged. On a GPU-less Xeon the engine is never registered and the path is never reached, so ADR-002 holds as written. ADR-002's status now points here rather than being deleted, per the rule that a changed decision gets a new record instead of an edited one.
This commit is contained in:
parent
795b77b709
commit
fad0d7bc96
3 changed files with 144 additions and 1 deletions
|
|
@ -1,7 +1,12 @@
|
||||||
# ADR-002: Direct play only — no transcoding, ever
|
# ADR-002: Direct play only — no transcoding, ever
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
Accepted
|
|
||||||
|
Accepted, **amended by [ADR-008](ADR-008-optional-hardware-transcoding.md)**.
|
||||||
|
The reasoning below still holds for this deployment: a GPU-less Xeon never
|
||||||
|
registers a transcode engine, so nothing here is reached. ADR-008 scopes the
|
||||||
|
rule to the hardware rather than to the product, and adds an optional
|
||||||
|
hardware-gated engine for servers that have the capability.
|
||||||
|
|
||||||
## Date
|
## Date
|
||||||
2026-10-12
|
2026-10-12
|
||||||
|
|
|
||||||
137
docs/decisions/ADR-008-optional-hardware-transcoding.md
Normal file
137
docs/decisions/ADR-008-optional-hardware-transcoding.md
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
# ADR-008: Optional, hardware-gated transcoding
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted. **Amends [ADR-002](ADR-002-direct-play-only.md) — it does not reverse
|
||||||
|
it.** Direct play remains the default, the preference and the only path this
|
||||||
|
deployment uses.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-10-12
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-002 decided that Vellum never transcodes, because the server is a headless,
|
||||||
|
GPU-less Xeon and a CPU transcode of 1080p HEVC runs far below real time. That
|
||||||
|
reasoning is sound and its measurements still hold.
|
||||||
|
|
||||||
|
The problem is what the decision was scoped to. "This server cannot transcode"
|
||||||
|
became "Vellum does not transcode", and those are different claims. Only the
|
||||||
|
first is forced by the hardware.
|
||||||
|
|
||||||
|
The constraint is one deployment, not the product:
|
||||||
|
|
||||||
|
- Intel Quick Sync has shipped in most desktop CPUs since 2015.
|
||||||
|
- NVENC is in every recent Nvidia card; VAAPI covers AMD and Intel on Linux.
|
||||||
|
- A user running Vellum on a gaming PC, a mini-PC with an iGPU, or a NAS with a
|
||||||
|
modern SoC has transcode hardware sitting idle.
|
||||||
|
|
||||||
|
Meanwhile the gap is real and visible. Direct play only means a client that
|
||||||
|
cannot decode HEVC gets **nothing** — the file is reported unplayable and there
|
||||||
|
is no fallback. Measured behaviour in Chrome is worse than a clean refusal: the
|
||||||
|
video never decodes, `videoWidth` stays 0, and audio plays over a blank frame.
|
||||||
|
A general audience reads that as "broken", not as "your phone lacks an HEVC
|
||||||
|
decoder".
|
||||||
|
|
||||||
|
A competing product's headline claim is "your graphics card converts anything
|
||||||
|
else on the fly". That is not a feature Vellum lacks the architecture for — the
|
||||||
|
engine registry was designed for exactly this. It is a feature the deployment
|
||||||
|
cannot use.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Transcoding is optional, off by default, and available only when the server has
|
||||||
|
usable hardware for it.**
|
||||||
|
|
||||||
|
1. A `transcode` engine is added to the `internal/engine` registry, registered
|
||||||
|
**only** when both are true: usable transcode hardware is detected (VAAPI,
|
||||||
|
NVENC or QSV) *and* an `ffmpeg` binary is on PATH.
|
||||||
|
2. **Off by default.** Direct play remains the default and the preference. A
|
||||||
|
client that can play the original always receives the original. Transcoding
|
||||||
|
is never chosen to normalise a file that already works.
|
||||||
|
3. **Only as a last resort.** The engine is consulted only when no other
|
||||||
|
declared engine can direct-play the file — precisely the case that returns
|
||||||
|
"no engine can play this" today.
|
||||||
|
4. **Hardware-accelerated only.** There is no CPU fallback path. A CPU transcode
|
||||||
|
is the stall ADR-002 warns about, and offering it would produce a feature that
|
||||||
|
appears to work and then does not keep up.
|
||||||
|
5. The capability is advertised: a flag in `GET /api/v1/capabilities` and in the
|
||||||
|
mDNS TXT records, so a client knows before it asks.
|
||||||
|
6. This deployment keeps `transcode` unregistered. On a GPU-less Xeon the engine
|
||||||
|
is never built, the code path is never reached, and ADR-002's reasoning holds
|
||||||
|
exactly as written.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Keep "never transcode" as a product rule
|
||||||
|
|
||||||
|
- **Pros:** no new dependency, no new failure mode, the story stays simple.
|
||||||
|
- **Cons:** caps the product at the hardware in one closet, and leaves the
|
||||||
|
worst-case client experience — audio over a blank frame — unaddressed.
|
||||||
|
- **Rejected:** the constraint is a deployment detail, not a product decision.
|
||||||
|
|
||||||
|
### CPU transcode as a fallback
|
||||||
|
|
||||||
|
- **Pros:** works everywhere, no hardware detection needed.
|
||||||
|
- **Cons:** on a CPU-only machine a 1080p HEVC transcode does not keep up. The
|
||||||
|
user gets a stream that starts, stutters and never finishes — worse than the
|
||||||
|
honest refusal they get today, because it fails slowly.
|
||||||
|
- **Rejected:** this is exactly the failure ADR-002 measured.
|
||||||
|
|
||||||
|
### Transcode by default, as other servers do
|
||||||
|
|
||||||
|
- **Pros:** simplest mental model, every file plays.
|
||||||
|
- **Cons:** re-encodes files that would have direct-played, burning power and
|
||||||
|
losing quality for no benefit. It also makes the hardware a hard requirement
|
||||||
|
rather than an optional upgrade.
|
||||||
|
- **Rejected:** direct play is strictly better whenever it is possible.
|
||||||
|
|
||||||
|
### Pre-transcode the library at scan time
|
||||||
|
|
||||||
|
- **Pros:** paid once, everyone direct-plays forever.
|
||||||
|
- **Cons:** doubles disk usage for a whole library and takes days of CPU. Still
|
||||||
|
requires the same hardware for a good result.
|
||||||
|
- **Rejected:** already rejected in ADR-002, and for the same reasons.
|
||||||
|
|
||||||
|
### Client-side transcoding
|
||||||
|
|
||||||
|
- **Pros:** no server load at all.
|
||||||
|
- **Cons:** not possible. Real-time decode-and-re-encode in a browser or a TV app
|
||||||
|
is beyond what those runtimes offer.
|
||||||
|
- **Rejected:** technically unavailable.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**What this buys.** A user with transcode hardware gets the "anything plays"
|
||||||
|
experience without a second server. A user without it loses nothing — direct play
|
||||||
|
is unchanged and remains the default. The same file is still unplayable with
|
||||||
|
`["html5"]` on a GPU-less host, exactly as ADR-002 describes.
|
||||||
|
|
||||||
|
**What it costs, honestly:**
|
||||||
|
|
||||||
|
- `ffmpeg` becomes a real runtime dependency for the transcode path. It is
|
||||||
|
already an *optional* dependency for `probe`, so the packaging change is a
|
||||||
|
promotion, not a new requirement — but it is a change.
|
||||||
|
- A transcode session needs CPU/GPU and memory accounting. The job queue's four
|
||||||
|
workers are for scans and enrichment; a transcode must not be scheduled through
|
||||||
|
them or it will starve the queue.
|
||||||
|
- HLS or DASH packaging appears where none exists today. The current stream path
|
||||||
|
is `http.ServeContent` over a whole file, which gives Range and seeking for
|
||||||
|
free. A transcoded stream is a playlist of segments and needs its own serving
|
||||||
|
path.
|
||||||
|
- Session cleanup becomes a real concern: a transcode that nobody is watching
|
||||||
|
must be torn down, or it burns the GPU forever.
|
||||||
|
- "One binary, one database file" gains a caveat — still true, but the binary now
|
||||||
|
shells out to `ffmpeg`.
|
||||||
|
- Hardware detection is per-platform and can be wrong. A detection that reports
|
||||||
|
VAAPI available when the driver cannot actually encode would produce the slow
|
||||||
|
failure this ADR exists to avoid, so detection must be verified by a probe
|
||||||
|
encode at startup rather than by the presence of a device node.
|
||||||
|
|
||||||
|
**What does not change.** The engine registry, the resolve path, the client
|
||||||
|
capability handshake and `internal/probe` all stay as they are. The registry was
|
||||||
|
built to hold engines like this; this ADR adds one and a condition for
|
||||||
|
registering it. `SupportsTranscoding` in the Jellyfin surface becomes `true` only
|
||||||
|
when the engine is registered, which is the same "report what is true" rule the
|
||||||
|
rest of the surface follows.
|
||||||
|
|
@ -16,6 +16,7 @@ Read the ones relevant to what you are changing.
|
||||||
| [005](ADR-005-one-item-access-chokepoint.md) | One item-access chokepoint, `internal/catalog` | Leave the duplication; a shared helper; a generic repository |
|
| [005](ADR-005-one-item-access-chokepoint.md) | One item-access chokepoint, `internal/catalog` | Leave the duplication; a shared helper; a generic repository |
|
||||||
| [006](ADR-006-table-backed-job-queue.md) | The job queue is a table | Redis or a broker; in-memory goroutines |
|
| [006](ADR-006-table-backed-job-queue.md) | The job queue is a table | Redis or a broker; in-memory goroutines |
|
||||||
| [007](ADR-007-fixed-width-timestamps.md) | Fixed-width timestamps via `core.Stamp` | Integer nanoseconds; six/nine digits; sorting in Go |
|
| [007](ADR-007-fixed-width-timestamps.md) | Fixed-width timestamps via `core.Stamp` | Integer nanoseconds; six/nine digits; sorting in Go |
|
||||||
|
| [008](ADR-008-optional-hardware-transcoding.md) | Transcoding is optional, hardware-gated, off by default | Keeping never-transcode as a product rule; CPU fallback; transcode by default |
|
||||||
|
|
||||||
## Writing a new one
|
## Writing a new one
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue