diff --git a/docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md b/docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md new file mode 100644 index 0000000..af4e585 --- /dev/null +++ b/docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md @@ -0,0 +1,108 @@ +# Intel Quick Sync hardware transcoding for Jellyfin on jupiter + +## Problem + +Jellyfin streams stutter on jupiter whenever a client needs a transcode +(unsupported codec/container, bitrate cap, or a client that can't +direct-play). Transcoding currently runs entirely on CPU. + +jupiter's Intel iGPU is already usable at the OS level: + +- `hardware.graphics.enable = true` with `intel-media-driver` (the `iHD` + VAAPI driver) is configured in + `machines/jupiter/hardware-configuration.nix:20-27`. +- The commented-out `i915.force_probe = "9a49"` kernel param there + corresponds to a Quick-Sync-capable Intel UHD iGPU, confirming the + hardware supports it. + +But `modules/environments/jellyfin/default.nix` never grants the +`jellyfin` systemd service access to `/dev/dri`, so Jellyfin has no path +to the GPU and silently falls back to software transcoding. + +## Goal + +Give the Jellyfin service access to the iGPU's VAAPI render node, so +Quick Sync can be enabled in Jellyfin's own dashboard and transcodes are +offloaded from the CPU. + +## Non-goals + +- Remote/external access or reverse-proxy tuning. +- General CPU/RAM headroom review of jupiter. +- A toggle option (`my.profiles.jellyfin.hardwareAcceleration.enable`) — + jupiter only has the one iGPU, so this is hardcoded on rather than + made configurable. + +## Design + +### NixOS change (declarative) + +In `modules/environments/jellyfin/default.nix`, inside the existing +`config = lib.mkIf cfg.enable { ... }` block, grant the systemd service +supplementary access to the `video` and `render` groups (the groups that +own `/dev/dri/card*` and `/dev/dri/renderD*`): + +```nix +systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [ + "video" + "render" +]; +``` + +This is additive to the existing `systemd.services.jellyfin.after = [ +"network-online.target" ];` block already in the file — both apply to +the same service. + +Also add `libva-utils` to `environment.systemPackages` (or scoped to +this module) so `vainfo` is available on jupiter to verify the driver +loads correctly. + +### Manual step (not declarative) + +Jellyfin stores its transcoding/hardware-acceleration choice in its own +internal `encoding.xml`, which the NixOS module does not expose as an +option. After deploying the Nix change, one-time manual configuration in +the Jellyfin dashboard is required: + +1. **Dashboard → Playback**. +2. Hardware acceleration: **Intel QuickSync (QSV)**. +3. VA-API device: `/dev/dri/renderD128`. +4. Enable hardware decoding for the codecs your library actually uses + (H264 at minimum; HEVC/VP9 depending on iGPU generation). +5. If any HDR content exists in the library, enable tone-mapping — this + is one of the more CPU-expensive operations Quick Sync can offload. + +## Verification + +Build-time (from the Mac, no SSH needed): + +``` +nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \ + --extra-experimental-features 'nix-command flakes' +``` + +Expect `[ "video" "render" ]`. + +On jupiter after `sudo nixos-rebuild switch --flake '.#jupiter'`: + +``` +systemctl status jellyfin +journalctl -u jellyfin -n 50 --no-pager +vainfo +``` + +`vainfo` should list the `iHD` driver and print supported VAEntrypoints +(VLD decode / encode profiles for H264/HEVC). + +Functional check: play a file on a client that forces transcoding (or +force it manually via Jellyfin's playback quality setting), then in +Jellyfin's dashboard **Activity/Now Playing** panel confirm the +transcode reason and check that CPU usage on jupiter (`htop`) stays low +during playback rather than pegging a core — Quick Sync offload should +show up as low CPU, some GPU (`intel_gpu_top`) activity instead. + +## Open items + +- Exact supported codec list depends on the iGPU generation (device ID + `9a49`) — confirm via `vainfo` output once run, and enable only the + hardware decode paths it actually reports.