Home Assistant add-on · Free

Real video on the TV, not a sequence of stills

Kestrel HLS repackages your cameras' RTSP streams as HLS — the one live format a Roku can actually play. Pair it with Kestrel for Roku once, and the channel sends its own camera list across every time it connects and gets back URLs it can play. Nothing is typed twice.

Home Assistant add-on · MIT · amd64, aarch64 and armv7 · Local network only · Not yet confirmed playing on a Roku

Read this first

What this buys, and what it costs

This has been run against real hardware, and it is not finished

A real Reolink RLN36 and a real Roku television have both been in front of it, and that is how several faults were found that no test would have caught — an RTSP option ffmpeg rejects rather than ignores, a codec hint read as "no sound" that made every camera play silently, and an encoder level written the way a person writes it, which made every re-encoded stream declare itself level 0.4 and be refused outright.

All of those are fixed. End-to-end playback is still not confirmed, which is why the add-on's rows in the Roku channel are marked untested and will not open until Untested features is switched on in its settings. Re-encoding in particular has a great deal of surface — televisions that decode H.265 and televisions that do not, the fragmented-MP4 path, an armv7 machine asked to re-encode 4K — and none of it should be treated as proven. Snapshot mode is the supported way to watch on a TV today; this is the one to try if you want the other thing.

Roku cannot play what an NVR produces

Reolink, Frigate, QNAP and UniFi publish RTSP, RTMP and HTTP-FLV. Roku's video player speaks HLS, DASH, Smooth Streaming and progressive MP4. There is no overlap, and no way around it on the device — which is why Kestrel for Roku shows a stream of snapshots by default, and why its HLS path has until now said "bring your own go2rtc, MediaMTX or Frigate".

This is the first-party answer to that. It does the conversion and nothing else: it is a proxy for the Roku channel, not a Home Assistant integration.

You get video. You pay in latency

Real video, with sound, at full frame rate — and five to ten seconds behind live. A player buffers about three segments before it starts, which is inherent to HLS rather than something an add-on can tune away.

So the two modes are not ranked. For checking the driveway or lining up a PTZ shot, snapshots are fresher. For watching what is actually happening, this is the one worth using. What decides that number, and how to bring it down.

How it works

Pair it once and stop thinking about it

The Roku already knows your NVRs and their channels. Rather than describing every camera a second time in a second place, it sends the addresses it builds and gets playable URLs back.

Kestrel for Roku ──POST /api/v1/cameras──▶ Kestrel HLS ──RTSP──▶ your NVR
       │            (its own camera list,        │
       │             built from the device       │  ffmpeg, copying rather
       │             config it already holds)    │  than re-encoding
       │                                         ▼
       └──────GET /s/…/index.m3u8 ───────▶  HLS, on demand

Cameras register themselves

On every connect, not only at pairing — so a camera renamed on the NVR, a password changed, or a device added is carried across without a second setup. A registration speaks only for the devices that answered, so an NVR that was briefly unreachable does not have its cameras deleted here.

Copy, not re-encode

H.264 with AAC goes through untouched, which costs a few percent of a processor core. Only what a Roku genuinely cannot play is re-encoded, and only so many at once — past the limit a camera is refused with a message rather than started anyway and left to make every other stream stutter.

On demand

A stream starts when somebody asks for it and stops about a minute after they stop watching. The first request is held open while ffmpeg starts, because a Roku that receives an error abandons HLS without retrying. A camera you watch often can be kept warm instead.

Local, like the rest of it

No cloud and no account. Camera passwords stay in the add-on's own volume, and are redacted out of every log line and every API response. Segments are written to a tmpfs rather than to the machine's card, so a continuous ring of two-second files is not grinding away at storage.

Setting it up

Four steps, and two of them are on the television

  1. Install and start the add-on Add its repository in Home Assistant under Settings → Add-ons → Add-on store → ⋮ → Repositories, install Kestrel HLS, and start it. It appears in the sidebar.
  2. Press "Pair a Roku" A code appears in the panel. It pairs one television and is then spent — press the button again for a second one.
  3. Enter the code on the TV Settings → Live video → Pair with the add-on. That row is marked untested, so Untested features has to be on for it to open.
  4. That's it The Roku sends its cameras across and they appear in the panel. In live view, press OK and choose Switch to HD video. If a stream fails, the channel falls back to snapshots and says why.

One setting in Home Assistant, everything else in the panel

The add-on's options tab has the log level and nothing else. Cameras, transcoding and the rest live in the add-on's own panel, so a camera is described in one place rather than in two that can disagree. Cameras can also be added by hand there, for anything that is not a Kestrel camera at all.

The panel

What you can change, and when it matters

Per camera

SettingDefaultNotes
TranscodingAutomaticCopies the stream through untouched when it can and re-encodes only what a Roku cannot play. Never and Always override it — except that a camera watched by a television that cannot decode H.265 is re-encoded regardless, since honouring "never" there would mean honouring a request for something that cannot work
Keep warmOffStarts the camera when the add-on starts and never stops it, so it plays the instant you ask. Worth setting on one or two favourites — a warm stream is a permanent RTSP connection to the camera
RestartRe-checks what the camera is actually sending. This is what to press after changing something on the NVR, and after a camera that fell back to its sub stream can offer the main one again
What is it serving?Produces the stream, reads it back, and reports the codec, profile, level, size, pixel format, frame rate and sound track going out. Those are the fields a television has documented limits on, so "unsupported video format" can be compared against something rather than guessed at

Everything else

SettingDefaultNotes
Cameras re-encoded at once2Copying is nearly free; re-encoding costs most of a core each. Past this, a camera is refused with a message
Segment length2 secondsLargely decides how far behind the picture runs. Below two seconds some Rokus stall, and on a copied stream the camera's own keyframe interval sets the floor whatever this says
Segments offered at once6How much of the recent past stays fetchable. Fewer gives a player less to fall back on when the network hiccups
Fall back to a camera's other streamOnThe chosen stream is always tried first. This decides what happens when it turns out to be unreadable — mis-framed H.265 that no decoder will accept, say — and the camera's row says when it has happened
Silent sound track for silent camerasOnVideo with no sound track at all is handled far less reliably by players than video with a silent one. Costs almost nothing
Stream addressTaken from how the Roku connectedNormally left empty — the address a Roku used to register is by definition one it can reach. Set it only if something sits in front of the add-on
Let anything on the network fetch the streamsOffStream URLs normally carry an unguessable per-Roku prefix. Turning this on serves them to any client that knows the camera id
Log levelinfoThe one setting that lives on the Home Assistant options tab
Latency

Why the picture is behind

A player buffers about three segments before it starts, so the delay is roughly three segment lengths — six seconds at the default of two — plus whatever the camera and the network add. Each camera's row reports the number it is actually achieving.

The add-on frequently cannot make segments as short as it is asked to

Copying a stream through without re-encoding it means a segment can only end on a keyframe. If the camera sends one every four seconds, the segments are four seconds, and the picture is about twelve seconds behind however low the segment length is set. The panel says so on the camera's row when it happens.

  1. Lower the camera's I-frame interval on the NVR This is the one that actually moves the number. On Reolink it is in the camera's encoding settings, often expressed as a multiple of the frame rate; one or two seconds is the target.
  2. Shorten the segment length here Once the camera can support it — before that it changes nothing.
  3. Re-encode instead of copying A keyframe is forced on every segment boundary when re-encoding, so the camera's interval stops mattering. It costs most of a processor core per camera, which is a poor trade for a few seconds unless you were re-encoding anyway.

None of this beats snapshot mode for freshness. HLS buys smooth motion and sound, not immediacy.

The H.265 problem

The reason "copy by default" re-encodes more than it sounds

Newer cameras ship H.265 on the stream a live view wants

Newer Reolink models put H.265 on the main stream, which is the one full screen asks for — and H.265 in a form Roku accepts cannot be produced by copying. Re-encoding it to H.264 brings the picture down to 1080p and caps the bitrate, which keeps it inside what a television will play, but costs most of a core per camera where copying costs almost nothing.

Two fixes, in order of how much they help: set the camera's main stream to H.264 on the NVR — one setting, no visible difference on a television — or use the sub stream, which is nearly always H.264 already.

Each television says what it can decode

H.265 decoding is a property of the particular box rather than of Roku: the 4K models have it and the rest do not, so a 1080p set — which is most televisions with Roku built in — refuses an H.265 stream however it is packaged.

So it is asked rather than assumed. The channel reports what the set can decode on every registration, and a camera watched by anything that cannot manage H.265 is re-encoded to H.264. One stream serves every television registered for it, so the least capable one decides. A television that never said is taken to be H.264-only, which is true of every Roku ever made.

Ports and access

Two surfaces, deliberately

PortWhat it carries
Ingress (the sidebar)The panel and its admin API. Home Assistant has already authenticated whoever reaches it, so there is no separate login, and nothing on your network can reach it directly
8100Pairing, registration and the video itself. This has to stay open — a Roku cannot reach an Ingress URL, because Ingress is a Supervisor-authenticated proxy and a Roku has no session with Home Assistant
  • The admin API is absent from the published port's router, not merely refused there. Telling the two surfaces apart by a header — the way an add-on whose port is never published reasonably can — would not be safe, since anything on the LAN can send whichever header the check looks for.
  • Stream URLs carry an unguessable per-Roku prefix, so publishing 8100 does not publish your cameras. The prefix is derived rather than stored, so unpairing a Roku invalidates every URL it held with nothing else to keep in step.
  • Registration carries NVR passwords, which is why it is authenticated with a token rather than left open. Those passwords are redacted from logs and from API responses, and live in the add-on's own volume.
  • Keep it on your LAN. Same rule as the rest of Kestrel: do not port-forward any of this to the internet.

What it does not do

  • It creates no Home Assistant entities and publishes no discovery record. It is a proxy for the Roku channel, not an integration. Whether it should offer a camera entity per stream is an open question, not something that was tried and abandoned.
  • It cannot help with ZoneMinder. ZoneMinder serves MJPEG, which has no keyframes and cannot be repackaged as HLS. Those cameras are reported back to the Roku as unsupported, so the option never appears for them.
  • UniFi Protect streams should be expected to stop after about an hour. Protect's RTSPS needs a session that expires, and the add-on holds the one it was given at registration. Reconnecting from the Roku re-registers and gets a fresh one. This has never been run against a real Protect controller.
  • The screensaver never uses any of it. Roku forbids video in a screensaver, and the screensaver package contains nothing that could build a stream address.
  • Only Reolink's RTSP addresses are proven. The channel builds the URLs and sends them; that formula has run against one RLN36. Frigate, QNAP QVR and UniFi Protect are written from documentation on both sides.

What has not been run

Stated here for the same reason the rest of this site states it: overselling an unproven path is how a product earns a review it cannot answer.

  • Playback end to end has not been confirmed. Real streams have been produced and a real television has been asked to play them; that work is not finished.
  • Nothing has been re-encoded in anger. Whether an armv7 machine can re-encode a single 4K stream at all is unknown.
  • The H.265-copied-into-fragmented-MP4 path is the most speculative thing here. It exists because MPEG-TS cannot carry HEVC to a Roku, and no Roku has confirmed it can play the result.
  • Cold-start timing — the entire justification for the retry on the Roku side — is measured but not settled.
Install

Getting Kestrel HLS

Home Assistant add-on

Free · MIT · amd64, aarch64, armv7

Added as an add-on repository in Home Assistant, then installed from the add-on store like any other. Alpine, ffmpeg and about a thousand lines of Python; no account, no registry, nothing phoning home.

https://github.com/kestrel-app/kestrel-hls

Paste that under Settings → Add-ons → Add-on store → ⋮ → Repositories, then install Kestrel HLS. One setting in Home Assistant — the log level — and everything else in the add-on's own panel.

Or a container, without Home Assistant

Free · MIT · amd64, aarch64, armv7

The same application, configured by environment rather than by an options file. Home Assistant is a convenient place to run this and not a requirement of it.

docker run -d --name kestrel-hls \
  -p 8100:8100 -p 8099:8099 \
  -e KESTREL_HLS_ADMIN_PASSWORD=choose-something \
  -v kestrel-hls-data:/data \
  --tmpfs /tmp/kestrel-hls:size=512m \
  ghcr.io/kestrel-app/kestrel-hls:latest

8100 has to be open — a Roku cannot reach the panel, and that is where it pairs, registers its cameras and fetches video. 8099 is the panel, and it asks for a password: it shows and can change the address and credentials of every camera in the house, so the container refuses to start without one. Inside Home Assistant neither applies, because the Supervisor authenticates the panel and its port is never published.

Kestrel for Roku

One-time purchase · Roku Channel Store

The add-on is only useful with the channel — it is the thing that registers the cameras and plays the streams. The channel works fully without the add-on, on snapshots.

About the Roku channel

What you need

  • Home Assistant with the Supervisor — this is an add-on, so Home Assistant OS or Supervised, not Core in a container
  • A machine with some processor to spare if anything will be re-encoded; almost none if everything can be copied
  • Kestrel for Roku on a Roku on the same network, with Untested features switched on
  • Port 8100 reachable from the Roku
  • Cameras publishing H.264 where you can manage it — see the H.265 problem

It costs nothing

Free and open source under the MIT licence, like the desktop client. The Roku channel is the only paid part of Kestrel, and it does not become more capable for having bought anything here. Support covers what to send if something does not work.