Linux desktop

Every NVR you own, in one window

A live camera wall across every device you add, recorded playback, PTZ control, snapshots and on-demand recording — covering the parts of a vendor desktop client that matter day to day, on a platform those clients skip. One AppImage, no root, no system ffmpeg.

x86-64 Linux · 10.3 MB · MIT / Apache-2.0

Systems

What it talks to, and how much of it works

Every capability is reported rather than assumed. Kestrel asks each system what it can do at discovery, and a camera that cannot move shows no PTZ pane at all instead of buttons that would only error.

System Live Playback PTZ & lights Detections Status
Reolink
NVR or standalone camera
YesYesYesMotion + AI Verified on an RLN36
Frigate
NVR with object detection
YesIn-progress events Never run
ZoneMinder
1.36 or newer
Yes Never run
QNAP QVR
QVR Pro or QVR Elite
Yes Never run
UniFi Protect
local account required
YesMotion flag Never run

A dash means Kestrel does not implement it for that system, not that the system lacks it. Playback, PTZ, presets and floodlight are Reolink-only today; on the others the Playback tab says the system does not serve recordings rather than showing an empty calendar.

Four of these have never been run

Reolink is verified continuously: it connects, enumerates 36 channels and streams live video through the vendor dispatcher on a real RLN36. Frigate, ZoneMinder, QNAP QVR and UniFi Protect are implemented from published API documentation, and none was reachable from the network this is developed on. They compile and are unit-tested for the parts that are pure logic — URL shapes, response parsing, label mapping — and that is all that is known.

The project keeps a written record of what is most likely wrong in each, rather than discovering it from a bug report:

  • Frigate — live video assumes the bundled go2rtc republishes each camera over RTSP on port 8554 under its own name. An install that has moved go2rtc, or one whose cameras are not restreamed, gets nothing. Authentication is assumed off, which is the default; with it enabled every request returns 401.
  • ZoneMinder — the /zm path prefix is assumed, so an install at the web root needs it removed. Video comes from nph-zms as MJPEG, whose location moves between packagings, and which has no keyframes — so warm streams buy nothing there, though they cost nothing either.
  • QNAP QVR — the camera list and snapshot paths differ between QVR Pro and QVR Elite in ways the documentation is vague about. The RTSP path is the least certain thing in the whole implementation.
  • UniFi Protect — needs a local account; a cloud one requires two-factor, which cannot be completed here. Streams come over RTSPS by an alias Protect only publishes once the stream is enabled per camera, and a camera without one says so rather than failing to connect. The console's self-signed certificate is no longer a blocker — see below — though that path has not been run against Protect itself.

One thing has changed elsewhere. UniFi Protect has since been run against a real controller — by the Roku channel, which is a separate implementation of the same API. It logged in, read the bootstrap, listed the cameras and drew them. That is genuine evidence about Protect's API and none at all about this client, which has still never met a controller; the two are written from the same documentation but share no code. It does make UniFi the most likely of the four to work here, which is worth knowing and is not the same as a claim.

If you run one of these, you will be the first — and we would very much like to hear what happened, at [email protected]. What makes such a report useful is here.

HTTPS to a device with its own certificate

Camera hardware ships a self-signed certificate, and often a strange one. The RLN36 serves HTTPS with an X.509 v1 certificate whose subject is CN=CERTIFICATE — which the TLS library refuses to parse rather than merely distrust. Ticking the HTTPS box therefore used to produce a connection that could never succeed, on Reolink as much as on UniFi Protect.

Trust this device's own certificate handles those devices. It is set per device and off unless you ask for it. Measured against the RLN36's HTTPS port: refused with the setting off, HTTP 200 with it on.

What it does not do: the handshake signature is not checked either, because verifying it means extracting the public key, which means parsing the certificate — the very thing that fails on these devices. So a trusted device gets encryption without authentication. That is better than plain HTTP carrying your password in the clear, and it is not the same as verified TLS. The setting says so itself rather than leaving it to be discovered.

Features

Everything you reach for daily

Configuration stays in your device's own web UI. Kestrel is the window you leave open.

Live wall

A paged grid of 1, 4, 6, 9 or 16 cameras across every device and NVR channel at once — several NVRs from different makers included. Sub streams in the grid, main stream when a camera is expanded, so a full wall stays cheap.

A wall is tiled square-ish and cameras are not, so three columns and two rows of a widescreen display give a 16:9 camera a cell narrower than its picture. Pictures are stretched to fill the cell by default, which wastes none of it and drops none of the frame. The other two answers are settings because both are ones people want: fit keeps the shape and accepts the bars, fill keeps the shape and crops the edges away.

Expand and fullscreen

Double-click a camera to fill the viewing pane; F11 then takes the whole screen, hiding the sidebar and controls. Esc unwinds one level at a time.

Digital zoom

Scroll to zoom into any tile and drag to pan, on top of whatever optical zoom the camera itself offers.

Virtual cameras

A camera aimed along a driveway sees the gate, the porch and the road in one picture, and a wall can only show you all three or none of them. Right-click a tile and Make a virtual camera: a box appears on the picture, which you drag to place and size by its corners. Double-click to keep it. What you keep goes on the wall beside the camera it came from, under its own name, as a camera in its own right — it pages, hides, expands, goes fullscreen, badges detections, and follow motion can bring it up. One camera can carry as many as you want.

The tile shows the whole camera while you are choosing, whatever it was showing before, because you cannot pick part of a picture you cannot see — and a tile you had already scrolled into opens with the box around exactly that, so keeping what is on screen is one gesture.

It costs no extra connection and no extra decode. A camera and every view of it read the same stream and draw different rectangles out of it, so putting a camera on the wall three times is one RTSP session and one decoder, not three. That stream is promoted to the camera's main stream while a virtual camera is on screen, because magnifying a sub stream has nothing to magnify: four times into 640×360 is 160×90 filling a cell. Sixteen main-stream decodes is not a thing to do to somebody's wall without asking, so it is a setting.

Hide the cameras you don't want

An NVR input pointed at nothing useful takes a tile like any other. Hide any camera from its tile's right-click menu or from the sidebar and it stops being shown and stops streaming. It is filtered in the same place offline channels are, so it is absent from the grid, the sidebar, paging and follow motion alike.

Getting one back needs it to be findable, so right-clicking the device it belongs to offers Reveal N hidden cameras, named with a count rather than left as a toggle nobody would think to try. Revealed cameras come back among the rest, marked hidden on the wall and struck through in the sidebar so you can tell which is which, and right-clicking one shows it again for good; showing the last of them puts the mode away with it. Hiding is stored with the device rather than as a preference — it describes the hardware, not a way of looking at it — so it survives editing the device.

PTZ and presets

Hold-to-move pan and tilt, zoom, focus and adjustable speed, with the presets stored on the camera re-read each time you select it. Home returns to the guard position; Calibrate runs the pan/tilt sweep, after asking. Reolink only today — elsewhere the control pane does not appear at all.

Playback

A calendar of days that have footage, a clip list per day, streamed playback with scrubbing and variable speed, and download to disk. Reolink only today — the other systems report that they do not serve recordings, and the tab says so.

Snapshots and recording

Full-resolution stills pulled from the device, and on-demand recording of the live stream straight to MP4 without re-encoding.

A snapshot of a virtual camera is cut from that full-resolution still rather than grabbed off the stream, so a 2.5× view of a 4K camera saves at around 1600 across — sharper than the picture it was framed on. A recording of one is the parent's whole picture, and the toast that starts it says so. Recording copies packets rather than re-encoding them, and narrowing a picture means encoding a new one; the ffmpeg inside Kestrel is a decoder-only build, which is what lets it be shipped the way it is. It is also the better failure, since a recording can be cropped afterwards and can never be widened.

Events

Motion and AI detection — person, vehicle, pet, face — shown as a live feed, with the tile and the sidebar entry highlighted and a desktop notification raised.

Follow motion

An optional mode that points the view at whatever is detecting: one camera fills the pane, several appear as a grid, and each is held for a dwell period so the view does not flick away the moment someone stops moving.

Any camera can be left out of it, from the same right-click menus that hide one. That is not hiding, and the difference is the point: a drive that catches every car on the road is worth having on the wall and not worth being pulled to. The camera stays exactly where it is and its detections still reach the feed, the notifications and the badges — it simply stops steering the view. Excluding one drops it from the selection immediately rather than making it sit out the dwell, which is the moment somebody reaches for this. While following is running, a camera left out of it says so on its own name strip, and the menu offers Follow every camera again with a count — because a mode switched off one camera at a time looks exactly like a quiet night, and there was no way to see which cameras to visit.

Hidden cameras are left out of it too, and are refused before they are considered rather than filtered out afterwards. That distinction is the whole of it: detections are reported for every online channel whether or not it is on the wall, so a hidden camera watching a road could take every slot the view holds and keep the camera you wanted off the list for as long as it kept triggering.

Dual-lens as one camera

TrackMix and similar appear as a single camera rather than two. Scrolling up, or the PTZ zoom-in button, switches to the telephoto lens; zooming back out returns to the wide one.

Weather beside the cameras

Off by default. A strip of conditions above the grid and a Weather tab beside Live and Playback: the strip is the glance while you are watching cameras, the tab is the read — current conditions, the readings the strip has no room for, and the forecast period by period with its full wording. Watches and warnings take the forecast's place rather than sharing a line with it.

It reads either a WeeWX server on your own network or the National Weather Service, from a ZIP code and nothing else. Both fill the same model, so nothing on screen depends on which one you use, and a station reporting in metric displays metric as it comes. Degrees, speeds and distances are one setting — US customary by default — asked whatever the source, because your own station settles its own readings and says nothing about how far the radar reaches. The ZIP code is resolved against a table carried inside the binary — no geocoding service to sign up for, and none to stop working. The radar is addressed the same way whichever source you pick, so the ZIP code is asked for either way: your own station reads the weather, but it has no idea where the rain is.

Radar

The National Weather Service enhanced radar inside the Weather tab: the seamless national mosaic rather than one station's cone, over a street, terrain or dark map, with watch and warning areas beneath the weather and place names above it. Up to twenty minutes of sweeps at two-minute steps, played as a loop that holds on the current one so the picture left on screen is the picture of now.

It can also take a cell on the camera wall, on terms you choose: a spare cell the cameras left over — and nothing when they tile exactly, so no camera ever changes size for it — or a cell of its own, where four cameras and the radar lay out as six cells rather than four and nothing is dropped to make room.

It behaves like a map, because it is one: drag to move, scroll to zoom, and the tiles are kept, so going back somewhere you have already been costs nothing. A pin marks where your ZIP code landed and stays on that ground wherever you go, and Reset view returns to it. Double-click the radar on the camera wall to open the full one, the way double-clicking a camera fills the pane with it. A scrubber steps through the twenty minutes with play and pause; the dBZ scale is drawn from the same colour stops the mosaic uses, so it cannot disagree with the picture; the reflectivity, warnings and place names each switch off on their own; and clicking a warning names it the moment you click, with the wording the office issued it in arriving a beat behind.

The two kinds of alert are drawn differently because they are different things. The short-fuse, storm-based warnings — tornado, severe thunderstorm, flash flood — are one layer; watches, advisories and statements are another, and on a summer afternoon that second one is a seventh of the country in solid colour. Both are outlined solid and washed out inside, so you can see where a warning is and read the county and the rain band straight through it, and both sit under the reflectivity: nothing the warning service draws can paint out the weather the radar is there to show.

Those areas are shapes, not a picture of shapes. Kestrel asks the service for the geometry it would have drawn and draws it here, and that is a different thing in three ways you can see. The outlines are lines rather than pixels, so they stay a hairline at every zoom instead of thickening into a band as you go in. A click is answered from the shape under the pointer instead of by asking the service what lies near where you clicked — so it is instant, it is exact at the edge, and a click on open ground quietly does nothing rather than going away to find that out. And where two alerts cover the same county, the more serious one is the colour you see: one shape, one colour, the one the key names.

It is also less to fetch. One request brings back everything in view, simplified on the way out to exactly what your screen can draw and no finer. The whole of the lower 48 at full fidelity is 29 MB; the same ground drawn to a 4K screen is 414 KB, a regional view 124 KB, and the radar in a cell on the camera wall 8.5 KB. And a shape already in hand simply moves with the map, so panning and zooming cost nothing at all — where the old picture had to be fetched again, in pieces, every time the view changed.

The colours have a key, in the bottom right, and it is a key to what is on the map rather than a legend of everything that could be. The service publishes 111 named colours; the key reads the ones actually in force in the view off the shapes already in hand and lists only those — warnings above watches above advisories, six of them and then a count. So a magenta county is an extreme heat warning without your having to click it, and a colour you have never seen before names itself the first time it appears.

Only fetched while you are looking at it. A clear sky and a layer that failed to load are the same empty rectangle, so a quiet radar says nothing showing rather than looking broken. Only a very large window over a very wide view trades anything away, and what it trades is the length of the loop rather than the picture: fewer sweeps of the real thing, never twenty minutes of a smeared one. An ordinary screen gives up nothing at any zoom.

The loop waits for itself. It holds on the current sweep until every sweep it is going to play has arrived and been traced, so it never steps into one that is still full of holes — and it decides that has happened by whether tiles are still landing rather than by a stopwatch, because how long a loop takes to fetch depends entirely on how much sky is in view. If the weather service stops answering, whatever is already on screen keeps playing instead of disappearing, and Kestrel asks less often until it is being answered again.

Rain drawn as numbers, not as a picture of numbers

The weather service sends the mosaic as an image, and an image cannot be magnified. Zoom into one and each colour band bleeds into the next; the usual answer is to blur it on purpose so the kilometre-wide cells it is stored in stop showing as squares, and what you get is soft at every zoom. That is what the radar did, and it is what almost every radar does.

It turns out the picture can be read back into the numbers it was drawn from — exactly. The service colours the mosaic through a continuous ramp, and that ramp is reversible: checked against the same sweep fetched as raw data, every colour it draws means one reflectivity and one only, across 153 colours and forty thousand cells, with no scatter at all. So Kestrel recovers the reflectivity, works out the shape at the size your screen will draw it, and applies the colours last.

Getting the order right is the whole of it. Colour first and magnifying blends one band into another, which is why radar pictures go soft. Colour last and a band edge lands exactly where the reading crosses, one pixel wide — while the boundary itself is free to curve rather than follow the square grid the data is stored on. Sharp edges and smooth shapes, which are usually a trade.

It is also markedly less to fetch. The old arrangement asked for four times the pixels the tile would be drawn with and averaged them back down; this asks for what the mosaic actually holds, or what the tile is drawn with, whichever is the smaller. That makes a tile five to eighteen times smaller on the wire depending on how far out you are — 4 KB against 71 KB where the mosaic is at its finest.

And once you have the numbers, the last step is not to draw a picture at all. Kestrel traces the rain into shapes — the outline of each band, as geometry. A picture is only ever as sharp as the size it was drawn at, so keeping up with a zoom means drawing it bigger and the cost grows as the square. Geometry has no resolution at all: zooming is a transform, so an edge is exactly as sharp as your screen can draw however far in you go, and a tile costs the same whether you are looking at a county or a street.

It also makes the loop move. Twenty minutes of radar is ten sweeps two minutes apart, and a storm travelling at 50 km/h covers nearly two kilometres between one and the next — so a radar loop does not really show weather crossing the map, it shows it teleporting ten times in four seconds. Because a sweep is a shape rather than a picture, Kestrel can work out which way the weather went — by sliding one sweep over the next until they line up — and then simply put it there, a little further along on every frame. Storms travel.

Travelling is half of it. The picture itself still changed all at once — every echo in view appearing and vanishing on the same frame, two and a half times a second, which reads as the display faulting rather than as weather. So most of each step is a crossing: the sweep arriving comes up as the one leaving goes, both drawn over the same ground so nothing shifts underneath, eased at each end so it begins and finishes gently rather than switching on. The change lands over about eight frames instead of one, and there is still a moment of single crisp sweep in every cycle. The wrap back to the start of the loop stays a cut — that one really is twenty minutes at once, and it should look like it.

Making that honest meant changing what a band is. A band used to be everything inside its own contour, so the bands nested and a storm was stacked shapes — lightest first, each heavier one painting over the middle of the one beneath. That comes out right only while every one of them is opaque: the moment a sweep is part transparent, the light band shows through the core it contains and the core reads as several colours mixed rather than as the heaviest. Each band is the ring between its own contour and the next one up now, so no ground is painted twice and a storm is the same colour at any opacity. Two sweeps at half each would still leave a quarter of the map showing through where both agree there is solid rain, so they are crossed at the root of that instead — measured on a storm over southern Oklahoma, it takes the dip from 16% of the echo's brightness down to under 6%.

The dBZ scale in the corner is now drawn from the same table as the map rather than a second one kept in step by hand. And because the colour table is learned from the service rather than agreed with it, it is checked: if the service ever restyles the mosaic, Kestrel notices that the colours no longer mean anything and draws the picture exactly as it arrives. A restyle would cost the sharpening, not the radar.

The forecast in the cells nothing else wanted

The arrangement the Roku channel has always had, brought across. A wall is tiled square-ish, so unless the camera count divides into it the last row ends short: five cameras make a 3×2 with one cell spare, seven make a 3×3 with two. Those cells now take the front of the forecast, one period per cell, in the order the service sends them — so the spare cell on a five-camera wall is tonight, and a seven-camera wall gets tonight and tomorrow. Nine cameras in a 3×3 leave nothing, and nothing is what appears: no camera is displaced to make room.

For a wall that is as much a weather display as a camera one, it can also be forced on with a count. Those cells are taken before the wall picks its shape, so four cameras become a 3×2 rather than a 2×2 — every camera a little smaller, none dropped. A cell that exists only because the forecast asked for it is not a cell the cameras left over, so the radar's spare cell setting will not take it; its whole promise is that nothing shrinks a camera for the radar.

Each tile carries the camera tiles' own name strip in the same place, at the same height, so a wall of cameras and forecasts reads as one wall — then the glyph with the temperature beside it, the summary wrapped to as many lines as the cell has room for, and the chance of rain. Everything is sized off the cell, which can be a quarter of the screen or a sixteenth of it, and anything that would run past the bottom is dropped rather than clipped. Click one to open the Weather tab on that period, with the full wording the cell has no room for.

The screen stays awake

A wall is something you watch without touching, which is exactly what a screen blanker treats as an idle machine. While fullscreen, Kestrel asks the desktop not to blank or sleep, and stops asking the moment you leave. On by default, and About Kestrel reports whether your desktop actually granted it.

The window

An app shell, not a document window

Header

The wordmark, a segmented Live / Playback switch, then grid layout, page controls and the snapshot, record, expand and fullscreen buttons. Everything else lives behind the overflow button.

Sidebar

Devices and their channels with a status dot each. Click a channel to jump to it; click a device to fold its channel list away and again to open it, double-click one to put the whole wall back. Folded devices are remembered between sessions.

The whole list folds away from the button in the top bar or with Ctrl+B, which gives the wall the width back. Whether it starts open is a setting; whether it is open now is not, the same way follow motion works.

Right-click a device for the things that belong to the box rather than to one camera: Edit device, Reconnect this device on its own rather than reconnecting everything, and Reveal N hidden cameras — which lives here because it has nowhere else it could: a hidden camera has no tile to right-click and no row in the sidebar, so the device that owns it is the last place left to ask. Revealing is scoped to that device, so finding one input you hid on a 36-channel NVR does not put every hidden camera you own back on the wall.

Right rail

Camera control above, event feed below. It belongs to the live camera, so it appears on Live and gives its width back on Playback, where it would have nothing to say. Fixed width, and it does not fold away — the camera list on the left is the one that does.

The top bar can go too

Optional, and off unless you ask: the bar across the top takes itself away when the mouse goes still and comes back on any movement. It runs off the same idle timer the camera names use rather than one of its own, so a wall settling down clears itself in one movement rather than two. Ctrl+B still reaches the camera list while it is away.

Toasts, not a status bar

Transient messages appear over the video and then leave, rather than occupying a strip of window forever.

Shortcuts

KeyAction
Ctrl + 1 / 2Live / Playback
Ctrl + 3Weather, once it is switched on
Ctrl + SSnapshot the selected camera
Ctrl + RToggle recording on the selected camera
/ Page through the grid, or step camera to camera when one is expanded
F11Fullscreen
EscLeave fullscreen, then leave the expanded view

Double-click a tile to expand it to fill the pane; double-click again to go back. Right-click a tile for per-camera actions.

Responsiveness

Why cameras appear instantly

Opening an RTSP stream is expensive. Measured against an RLN36, connecting takes about three seconds and the first decodable picture arrives around seven — roughly half RTSP setup, half waiting for the camera's next keyframe. Tuning the client only trims the first half, so the real work is never reconnecting.

Showing a cameraTime to first frame
Cold connect6.10 s
Adopting a warm stream0.07 s

Off-screen cameras stay warm

Cameras you cannot currently see remain connected and demuxing, without decoding, holding the most recent keyframe. Showing one then costs a fourteenth of a second instead of six, because the session already exists and there is a keyframe to decode from immediately.

Tiles survive rebuilds

A camera that stays on screen through a layout change, page turn or follow-motion update keeps the connection it already has. Tiles that leave the screen are parked, still streaming, for twenty seconds.

Expanding upgrades in place

The sub stream keeps playing — live, not a frozen frame — while the main stream connects behind it, and the swap happens only once the main stream has decoded a picture. Leaving early simply cancels it.

Damaged video is not drawn

H.264 and H.265 are error-resilient by design: handed a stream with a slice missing they do not fail, they conceal — patch the hole from neighbouring blocks and report success. That concealment is the solid magenta and green rectangles you see on a busy NVR. Kestrel reads what the decoder says about each picture rather than trusting that it returned one, and a picture the decoder had to patch up is dropped and the decoder reset until a keyframe rebuilds it. That costs nothing already lost: every picture after a damaged reference is damaged too, until the next keyframe.

Decoding is capped at two threads per stream rather than one per core, because sixteen decoders each claiming a machine's worth is what makes every reader late — and a late reader on an RTSP socket is a source that throttles and starts dropping, which arrives as exactly that damage. If a camera has had nothing clean for four seconds the tile says Recovering the picture over the held frame, because a frozen picture and a still scene look identical from across a room.

A camera on the wall twice is one connection

Connections are keyed by camera rather than by tile, so a camera and every virtual camera cropped out of it share one session and one decoder — each tile uploads its own picture to the graphics card and draws its own rectangle, which is work the card was going to do anyway. The tiles on screen are also what decides when a connection is let go: a camera keeps streaming while any view of it is on the wall, and parks when the last one leaves.

Cheap at rest

All fifteen online channels of an RLN36 held warm at once measured 2.0 Mb/s in total and about nine per cent of one CPU core — against roughly 2.7 per cent per camera when actually decoding.

The remaining keyframe wait on a genuinely cold camera is set by the camera, not by Kestrel: lowering its I-frame interval in the Reolink settings shortens it directly.

Where things go

On your disk, in the obvious places

WhatWhere
Devices and preferences ~/.config/kestrel/config.json, mode 0600
Passwords System keyring via Secret Service, falling back to the config file if no keyring is available
Snapshots, recordings, downloads ~/Videos/Kestrel/, changeable in preferences

The About dialog reports which password store is actually in use, so the fallback is never silent. Nothing is written anywhere else, and nothing is sent anywhere — see the privacy policy.

Download

Get Kestrel for Linux

AppImage

Kestrel-x86_64.AppImage · latest release

One self-contained file: the binary plus ffmpeg's shared libraries. Nothing to install, no root, and no system ffmpeg, GStreamer or VLC dependency.

Download AppImage

Tarball

kestrel-x86_64.tar.gz · latest release

The same two components unpacked, for anyone who would rather not run an AppImage. The binary carries an rpath of $ORIGIN/lib, so it finds its libraries beside it.

Download tarball

Verify what you downloaded against SHA256SUMS: sha256sum -c SHA256SUMS

Running it

chmod +x Kestrel-x86_64.AppImage
./Kestrel-x86_64.AppImage

Release builds are linked against an older glibc than the machine that produced them, so the same file runs on distributions older than the build host.

Adding a device

Press + in the Cameras sidebar, pick the system, then enter the address, username and password. If you are unsure what is at that address, Identify this system asks it directly — each one gives itself away without credentials, and picking a system also moves the port to where it listens without overwriting one you typed. Against the real NVR the probe answers "Reolink" in 0.18 seconds.

Test connection confirms the details before saving, reporting the model, firmware and channel list it found. Point it at an NVR and every channel appears as its own tile; add another device and its cameras join the same grid.

Licensing

Kestrel for Linux is licensed under the MIT and Apache 2.0 licences, at your option. It bundles ffmpeg 7.1, built LGPL-only and shipped as shared libraries so the licence's replacement requirement is met by the shared-library mechanism itself. No GPL encoders are built in — Kestrel decodes; it never calls them. The full terms of use are in the Terms of Service.