# ADR-002: Direct play only — no transcoding, ever ## Status 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 2026-10-12 ## Context A media server normally transcodes: it re-encodes a file on the fly when the client cannot play it. That is how Jellyfin and Plex make an arbitrary library playable on an arbitrary device. Vellum runs on a **headless, GPU-less Xeon**. It has no display and no spare CPU for real-time encoding. A 1080p HEVC transcode on a CPU-only machine runs at a few times realtime at best; a two-hour film would take tens of minutes, per viewer, forever. The question was what to do instead. ## Decision **Never transcode.** A file either direct-plays in the client or it does not. The server reports what is *in* the file and the **client** decides whether it can play it. ## Alternatives Considered ### Transcode on demand, CPU only - **Pros:** every file plays everywhere. - **Cons:** on this hardware a single viewer would saturate the CPU for minutes per film. Two viewers would make it unusable. - **Rejected:** the hardware cannot support it. ### Remux on demand (`-c copy`), no re-encode - **Pros:** cheap — no video re-encode, just a container change. Would fix "mkv will not play". - **Cons:** the premise turned out to be **false**. Measurement showed Chrome plays `mkv + h264 + aac` perfectly. The container was never the problem. - **Rejected:** it solves a problem that does not exist. ### Pre-transcode everything to a normalised format - **Pros:** paid once, everyone direct-plays forever after. - **Cons:** doubles disk usage for a whole library, and takes days of CPU for a large collection. Also irreversible if the settings were wrong. - **Rejected:** too much cost and risk for the benefit. ## The measurement that decided it Files with known codecs, played through Vellum's own stream endpoint in Chrome 155 on Linux: | container | video | audio | result | |---|---|---|---| | mp4 | h264 | aac | plays | | **mkv** | h264 | aac | **plays** | | mov | h264 | aac | plays | | webm | vp9 / vp8 | opus / vorbis | plays | | mp4 | av1 | opus | plays | | mp4 | **hevc** | aac | **audio only**, `videoWidth` stays 0, no error | | mkv | **hevc** | flac | **audio only** | | avi | mpeg4 | mp3 | fails outright | All seven audio formats play. **Two conclusions:** 1. **Codec matters, container barely does.** mkv+h264 plays; mp4+HEVC does not. Judging a file by its extension is wrong. 2. **`canPlayType` is unreliable in both directions.** It says "no" for `mov+h264` (which plays) and "maybe" for `mkv+hevc` (whose video never decodes). The only trustworthy test is loading the file and checking `videoWidth`. **And the conclusion that shapes the architecture:** playability is a property of the **client**, not the file. Chrome cannot decode HEVC; Safari can; Jellyfin's native apps handle far more than either. So a server-side `playable: true/false` would be wrong for most clients. ## Consequences - `internal/probe` records codec facts at scan time (optional dependency — the server works without `ffprobe`, just with less information). - `internal/engine` holds a registry of playback engines. `html5` ships built in; `mpv`, `vlc` and `mdk` are registered for native clients. **Adding one is a declaration, not a re-architecture.** - `POST /api/v1/playback/resolve` matches a file against the engines the client declares. The same HEVC file is unplayable with `["html5"]` and playable with `["html5","mpv"]`. - The player treats the server as an **advisory** and the media element as the final authority, because `canPlayType` lies. - Jellyfin's `PlaybackInfo` reports `SupportsTranscoding: false`, and a client demanding a transcode gets `NoCompatibleStream` rather than a URL that would never resolve. - **The honest UX consequence:** files that cannot direct-play say so clearly, the same way an unreadable `.mobi` does — rather than a player that loads and shows nothing. For HEVC, Chrome plays audio over a blank frame with no error, which is the worst possible outcome, so the player detects exactly that.