Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.4 KiB
Jellyfin Hardware Transcoding (jupiter) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Give jupiter's Jellyfin service access to the Intel iGPU's VAAPI render node so Quick Sync hardware transcoding can be enabled, instead of every transcode falling back to CPU.
Architecture: One NixOS module change (modules/environments/jellyfin/default.nix) grants the jellyfin systemd service supplementary access to the video/render groups and installs libva-utils for verification. This is declarative and build-verifiable from the Mac. Enabling Quick Sync inside Jellyfin's own dashboard, and the on-machine verification, is a manual step run by the user on jupiter after deploy — the NixOS module has no option for it and this environment's convention is that the assistant never SSHes into jupiter directly (see docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md).
Tech Stack: NixOS (flake-parts), nixpkgs services.jellyfin module, VAAPI/intel-media-driver, libva-utils.
Global Constraints
- No SSH from the assistant into jupiter — all on-machine commands are given to the user to run and paste back.
- Follow the existing profile pattern in
modules/environments/jellyfin/default.nix(config = lib.mkIf cfg.enable { ... }); don't introduce a new toggle option — hardcode the hardware-acceleration wiring on, per the approved spec. - Verify locally via
nix eval/nix buildbefore asking the user to deploy.
Task 1: Grant Jellyfin access to the iGPU and verify the build
Files:
- Modify:
modules/environments/jellyfin/default.nix
Interfaces:
-
Produces:
systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [ "video" "render" ];— verified vianix evalin Step 2. -
Step 1: Add the device-access config and
libva-utilspackage
Read the current file first (modules/environments/jellyfin/default.nix), then edit the config = lib.mkIf cfg.enable { ... } block so it reads:
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
name = "Jellyfin";
description = "Media server";
href = "http://${hostName}:${toString port}";
icon = "jellyfin.png";
}
];
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
Note the two existing systemd.services.jellyfin keys (after) and the new serviceConfig.SupplementaryGroups now live in the same attrset — don't create a second systemd.services.jellyfin = { ... } block, it would overwrite the first.
- Step 2: Verify the rendered config with
nix eval
Run (from the repo root on the Mac):
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
Expected output: [ "video" "render" ]
- Step 3: Verify the machine still builds
Run:
nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' \
--extra-experimental-features 'nix-command flakes' --no-link
Expected: build succeeds with no errors (may take a while; watch for any evaluation error mentioning jellyfin or libva-utils).
- Step 4: Format and commit
nixfmt-rfc-style modules/environments/jellyfin/default.nix
git add modules/environments/jellyfin/default.nix
git commit -m "feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding"
Task 2: Deploy on jupiter and enable Quick Sync (user-executed)
Files: none (on-machine deploy + Jellyfin dashboard UI)
Interfaces:
- Consumes: the
SupplementaryGroupschange from Task 1, already merged into the flake.
These steps run on jupiter, by the user — paste the output back so we can confirm each one before moving to the next.
- Step 1: Deploy
sudo nixos-rebuild switch --flake '.#jupiter'
Expected: switch succeeds, no errors mentioning jellyfin.
- Step 2: Confirm the service picked up the new groups
systemctl show jellyfin -p SupplementaryGroups
systemctl status jellyfin --no-pager
Expected: SupplementaryGroups=video render (order may vary) and the service is active (running).
- Step 3: Confirm VAAPI driver loads
vainfo
Expected: output starts with something like vainfo: VA-API version: 1.x and Driver version: Intel iHD driver, followed by a list of supported VAProfiles/VAEntrypoints (e.g. VAProfileH264Main : VAEntrypointVLD, VAEntrypointEncSlice).
If this instead prints a permissions or "no VA display" error, paste it back — that means the group grant isn't reaching the process and Task 1 needs a follow-up fix (e.g. the jellyfin service may be more sandboxed than expected, requiring an explicit DeviceAllow=char-drm rw in serviceConfig as well).
- Step 4: Enable Quick Sync in the Jellyfin dashboard
In the Jellyfin web UI:
- Dashboard → Playback.
- Hardware acceleration: Intel QuickSync (QSV).
- VA-API device:
/dev/dri/renderD128. - Enable hardware decoding for the codecs your library uses (H264 at minimum).
- If the library has HDR content, enable tone-mapping.
- Save.
- Step 5: Functional check
Play a file that requires transcoding (or force a lower quality in the client's playback settings to trigger one), then:
journalctl -u jellyfin -n 50 --no-pager
Look for a line referencing qsv or vaapi in the transcode command. Separately, watch CPU usage (htop) during playback — it should stay low on the core doing the transcode, rather than pegging at 100%, since the iGPU is now doing the encode/decode work.
Self-Review Notes
- Spec coverage: NixOS change (Task 1) ✓, manual dashboard step (Task 2 Step 4) ✓, verification via
vainfo/build (Task 1 Step 2-3, Task 2 Step 3) ✓, functional check (Task 2 Step 5) ✓. Toggle option explicitly excluded per approved spec — not present, correctly. - No placeholders — every step has literal commands/code.
SupplementaryGroupskey/value matches exactly between Task 1 (produced) and Task 2 (consumed/checked).