Homebridge · Docker · Linux

Hardware transcoding for Homebridge cameras

Homebridge re-encodes every camera stream it hands to HomeKit Secure Video, and on Linux it does that on the CPU by default — even when there's a perfectly good GPU in the box. This is how to move that work onto an Intel, NVIDIA or AMD GPU, and how to tell whether it actually worked.

Why it’s off by default

Not because it’s disabled. In homebridge-unifi-protect, the Video.Transcode.Hardware feature option already defaults to on. The problem is one layer down.

Homebridge ships its own FFmpeg build, ffmpeg-for-homebridge, and the Linux binaries are compiled without any GPU support beyond AMD’s AMF. You can confirm it on your own container in one command:

$ docker exec homebridge ffmpeg -hide_banner -hwaccels
Hardware acceleration methods:
amf

No qsv, no cuda, no vaapi. The plugin asks for hardware acceleration, finds nothing it can use, and quietly falls back to software. Nothing in the log says so. That silent fallback is the whole problem — and this is a documented decision rather than an oversight, since the ffmpeg-for-homebridge README points Linux users who want GPU acceleration at Jellyfin’s FFmpeg build instead.

So the fix is in three parts: give Homebridge an FFmpeg that can talk to your GPU, give the container access to the GPU device, and tell the plugin to use it.


Find out what you're working with

Five minutes, no changes yet

Run these on the Docker host, not inside the container.

# What GPUs are present?
lspci -nn | grep -iE 'vga|3d|display'

# Intel: is there a render node, and does VA-API work?
ls -l /dev/dri/by-path/
vainfo --display drm --device /dev/dri/renderD128

# NVIDIA: is the driver loaded, and what's the GPU's UUID?
nvidia-smi -L

For the Intel check you want vainfo to list VAEntrypointEncSliceLP next to an H.264 profile — that’s the hardware encoder. On Intel Arc, encode is only ever offered as EncSliceLP (low-power/VDEnc); that’s normal and not a fault.

Don't trust renderD128

If you have two GPUs, /dev/dri/renderD128 is not a stable name. Both cards register render nodes, and which one lands on 128 versus 129 depends on kernel module load order — it can swap on reboot and quietly point your container at the wrong card. Use the PCI-addressed symlink in /dev/dri/by-path/ instead. Step 3 shows how.

Get an FFmpeg that supports your GPU

Jellyfin's build, dropped in beside your config

Jellyfin publishes an FFmpeg with QSV, VA-API and NVENC all enabled. The portable tarball is the one you want — it’s a static binary with no dependencies of its own:

curl -LO https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/\
v7.1.4-3/jellyfin-ffmpeg_7.1.4-3_portable_linux64-gpl.tar.xz

mkdir -p /your/homebridge/ffmpeg
tar -xf jellyfin-ffmpeg_7.1.4-3_portable_linux64-gpl.tar.xz \
    -C /your/homebridge/ffmpeg

Check the releases page for the current version — the portable assets sit below the long list of .deb files, so scroll past them. Put the result somewhere beside your Homebridge config: nothing then lives inside the image, so it all survives image updates.

/your/homebridge/ffmpeg/
├── ffmpeg
└── ffprobe

Then mount that folder into the container at /ffmpeg. If you’re on NVIDIA, this is the only download you need — skip ahead to step 3.

intelamd The VA-API driver libraries

This is the step that trips people up, and it applies to both GPU vendors that go through VA-API. That FFmpeg loads the VA-API driver at runtime, and a driver can’t be baked into a static binary — so the container also needs libva, libigdgmm, libmfx-gen and the driver itself (iHD_drv_video.so for Intel, radeonsi_drv_video.so for AMD).

Those ship in the same release, in the .deb rather than the tarball. You don’t need to install it — just unpack it and take the lib folder. Pick the .deb matching your container’s base distribution, not your host’s; the official Homebridge image is Ubuntu 24.04, which is noble:

curl -LO https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/\
v7.1.4-3/jellyfin-ffmpeg7_7.1.4-3-noble_amd64.deb

dpkg-deb -x jellyfin-ffmpeg7_7.1.4-3-noble_amd64.deb ./jf
cp -a ./jf/usr/lib/jellyfin-ffmpeg/lib /your/homebridge/ffmpeg/lib

No dpkg on your machine? A .deb is just an ar archive: ar x file.deb && tar -xf data.tar.* gets you the same tree.

The mount path is load-bearing

Jellyfin’s libva has its driver search path compiled in as /usr/lib/jellyfin-ffmpeg/lib/dri, and it ignores LIBVA_DRIVERS_PATH — setting that variable looks correct and does nothing at all. Mount the folder at that exact path, and point LD_LIBRARY_PATH at it.

Two things worth knowing about why it’s split this way. The .deb’s own ffmpeg is dynamically linked and expects a full Jellyfin install — dropped into the Homebridge container on its own it fails with libOpenCL.so.1: cannot open shared object file. The portable tarball’s binary is static and has no such problem, but ships no libraries. Taking the binary from one and the libraries from the other gets you a combination that works, from two official downloads.

And match the .deb to the container’s distribution. Libraries built against a newer glibc than the container’s will fail to load — which is also why copying these from your host is a bad idea if your host is newer than Ubuntu 24.04.

NVIDIA — skip this step

NVENC needs no driver bundle. The NVIDIA Container Toolkit injects libcuda and libnvidia-encode into the container for you, so the portable tarball on its own is enough.

Give the container the GPU

Two different mechanisms — pick the one for your card

Intel — device passthrough

Pass the render node straight through. Address it by-path so a reboot can't repoint it at the other card, and remap it to a fixed name inside the container.

NVIDIA — container toolkit

Install nvidia-container-toolkit on the host, then reserve the GPU by UUID. Don't pass /dev/nvidia* by hand.

AMD — device passthrough

Same mechanism as Intel: pass the render node through by-path. Needs the amdgpu kernel driver, and Mesa's RadeonSI supplies the VA-API driver.

intel Compose fragment

services:
  homebridge:
    environment:
      - LD_LIBRARY_PATH=/usr/lib/jellyfin-ffmpeg/lib
      - LIBVA_MESSAGING_LEVEL=0          # silences per-run libva chatter
    devices:
      # Long syntax is required: a PCI path contains colons, which the
      # short "src:dst:perms" form can't parse. Use YOUR card's address.
      - source: /dev/dri/by-path/pci-0000:3d:00.0-render
        target: /dev/dri/renderD128
        permissions: rwm
    group_add:
      - "993"                            # the host's 'render' group; check: getent group render
    volumes:
      - /your/homebridge/ffmpeg:/ffmpeg
      - /your/homebridge/ffmpeg/lib:/usr/lib/jellyfin-ffmpeg/lib:ro

nvidia Compose fragment

services:
  homebridge:
    environment:
      - LD_LIBRARY_PATH=/usr/lib/jellyfin-ffmpeg/lib
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids:
                - GPU-12e4eb47-80fe-6344-f0c6-8c0a4421e70f   # from nvidia-smi -L
              capabilities:
                - gpu
                - compute
                - video                  # 'video' is required for NVENC
    volumes:
      - /your/homebridge/ffmpeg:/ffmpeg

Naming the GPU by UUID rather than index matters on a multi-GPU host — indices can shift, UUIDs don’t. And video in capabilities is the one people forget: without it you get CUDA compute but no encoder, and NVENC fails at runtime with the GPU apparently present.

The NVIDIA path needs no driver-library mount. The container toolkit injects libcuda and libnvidia-encode for you.

amd Compose fragment

Structurally identical to the Intel fragment — same render node, same group, same driver-bundle mount. Only the PCI address changes, and the driver Mesa loads (radeonsi rather than iHD) is selected automatically from the card:

devices:
  - source: /dev/dri/by-path/pci-0000:0c:00.0-render   # your card's address
    target: /dev/dri/renderD128
    permissions: rwm
group_add:
  - "993"
volumes:
  - /your/homebridge/ffmpeg:/ffmpeg
  - /your/homebridge/ffmpeg/lib:/usr/lib/jellyfin-ffmpeg/lib:ro

Check it the same way, but expect radeonsi in the driver line: vainfo --display drm --device /dev/dri/renderD128. Polaris (RX 400/500) and newer get full acceleration; older cards are patchier.

Point Homebridge at it

One line in config.json

In your UniFi Protect platform block (the same idea applies to other camera plugins that expose a video processor setting):

{
  "platform": "UniFi Protect",
  "videoProcessor": "/ffmpeg/ffmpeg"
}

That’s the whole change for Intel. Video.Transcode.Hardware is already on by default, so once the plugin has an FFmpeg that can reach the GPU, it uses it.

NVIDIA and AMD — not yet in a release

homebridge-plugin-utils currently implements only Intel QSV and Apple VideoToolbox. There is no NVENC or VA-API path in the shipped library, so the NVIDIA and AMD fragments above will expose the GPU and the plugin still won’t use it. Support for both is proposed upstream but unreleased. Until it lands, treat those routes as preparation — the Intel route works with released code today.

Under that proposal the accelerator is an explicit choice rather than something probed for, because a machine with Intel integrated graphics and a discrete card is common and a probe picks by search order rather than by which card is actually better:

"hardwareAccel": "nvenc"

It defaults to "qsv", so Intel setups need nothing new and anything else is opt-in.

AMD is experimental

Worth being straight about this: the VA-API path’s argument chains are verified against a real VA-API device, but not against AMD hardware with the amdgpu and Mesa RadeonSI stack specifically. VA-API is device-agnostic enough that it ought to work, but nobody has confirmed it on an actual Radeon yet. If you try it, the result is worth reporting either way.

Confirm it's actually running on the GPU

Don't take the absence of errors as success

Restart the container and look for one line per camera:

[UniFi Protect] Using FFmpeg version: 7.1.4-Jellyfin.
[UniFi Protect] Front Door: ⚡️ Hardware-accelerated decoding and ⛭︎ transcoding enabled: Intel Quick Sync Video.

If that line is missing, you’re still on software — the plugin doesn’t announce the fallback. To test the pipeline directly, without waiting for a camera to stream:

# Intel
docker exec homebridge /ffmpeg/ffmpeg -hide_banner -v error \
  -hwaccel qsv -t 1 -f lavfi -i color=black:1920x1080 \
  -c:v h264_qsv -f null - && echo "QSV OK"

# NVIDIA
docker exec homebridge /ffmpeg/ffmpeg -hide_banner -v error \
  -hwaccel cuda -t 1 -f lavfi -i color=black:1920x1080 \
  -c:v h264_nvenc -f null - && echo "NVENC OK"

# AMD (VA-API)
docker exec homebridge /ffmpeg/ffmpeg -hide_banner -v error \
  -init_hw_device vaapi=hw -filter_hw_device hw \
  -t 1 -f lavfi -i color=black:1920x1080 \
  -vf 'format=nv12,hwupload' -c:v h264_vaapi -f null - && echo "VA-API OK"

Watch utilisation while a camera is live: intel_gpu_top for Intel (the interesting row is Video, not Render/3D), nvidia-smi dmon for NVIDIA, where the enc column should come off zero, or radeontop for AMD.


When it doesn’t work

SymptomCauseFix
Failed to initialise VAAPI connection: -1libva can’t find iHD_drv_video.soMount the driver bundle at exactly /usr/lib/jellyfin-ffmpeg/lib. LIBVA_DRIVERS_PATH will not help.
GLIBC_2.xx not foundDriver libraries copied from the hostTake them from the jellyfin/jellyfin image instead.
Worked, then broke after a rebootrenderD128 now points at the other GPUSwitch the device to a /dev/dri/by-path/ source.
Permission denied on the render nodeContainer user isn’t in renderAdd group_add with the host’s render GID.
NVENC: Cannot load libnvidia-encode.so.1video missing from capabilitiesAdd video alongside gpu and compute.
hwdownload fails, -22 Invalid argumentCUDA frames need an explicit formatUse hwdownload,format=nv12 — CUDA needs the pin, QSV doesn’t.
VA-API: No support for codec h264 profileCard predates full VCE/VCN encode supportCheck vainfo lists an H.264 Enc entrypoint. Pre-Polaris Radeons are limited.
VA-API: driver line says iHD on an AMD boxWrong render node passed throughTwo GPUs present — point the by-path source at the Radeon’s PCI address.
Log shows amf as the only hwaccelStill the stock FFmpegvideoProcessor isn’t pointing at the Jellyfin binary.

What to expect

The win is CPU headroom rather than better-looking video. Hardware encoders trade some quality-per-bit against a large drop in CPU time, which is the right trade for HomeKit Secure Video — several cameras re-encoding continuously is exactly the workload that pins a CPU, and HKSV’s bitrates are modest enough that the quality difference is hard to see.

A modest Intel GPU handles a handful of camera streams comfortably. If you have both an Intel iGPU and a discrete NVIDIA card, the NVIDIA card is usually the stronger encoder — but note that transcoding is a fixed-function block on both, so it doesn’t meaningfully compete with compute work already running on the same card.