Skip to content
KestrelKestrel
Esc
↑↓navigate↵open⌘Jpreview
On this page

Running it

Set up the server and web client against your recorder.

You need mise, which installs Bun, go2rtc and the other tools at the versions the repository pins.

mise install
mise run build

Your sources

The server’s sources, such as a recorder, and the names you give cameras are kept in sources.json in its config folder: ~/.config/kestrel (or $XDG_CONFIG_HOME/kestrel) on Linux and macOS, %APPDATA%\kestrel on Windows, or the folder KESTREL_CONFIG names. The server writes it when you save from the Settings page, readable only by you, and you can edit it by hand too; restart the server after a hand edit.

The Settings page, in the app bar, lists each source as a panel that opens to the settings its kind takes; opening one closes the one open before. Add source picks a kind, Remove takes one out, and a stream source lists its cameras with their addresses. Under each source, name its cameras: the name shows across the app in place of the source’s own name for it, such as “Garden”, or “Ch 3” for a recorder’s numbered channel. Camera order, below the sources, sets the order cameras show in across the app: drag one by its handle, or focus the handle and move it with the arrow keys, Home and End. Cameras left out of the order, such as a new source’s, follow in the order their sources give them. Save restarts only the sources whose settings changed. If the settings changed since the page loaded them, such as from another tab, the save is refused: reload the page and make the change again.

{
  "sources": [
    {
      "id": "swann",
      "kind": "raysharp",
      "name": "Swann recorder",
      "settings": { "host": "192.0.2.10", "username": "admin", "password": "…" }
    },
    {
      "id": "garden",
      "kind": "stream",
      "name": "Garden cameras",
      "settings": {},
      "cameras": [
        {
          "id": "porch",
          "name": "Porch",
          "main": "rtsp://user:…@192.0.2.20:554/stream1",
          "sub": "rtsp://user:…@192.0.2.20:554/stream2"
        },
        {
          "id": "shed",
          "name": "Shed",
          "still": "http://192.0.2.21/snapshot.jpg"
        }
      ]
    }
  ],
  "cameras": [{ "camera": "swann/1", "label": "Front door" }],
  "order": ["garden/porch", "swann/1"]
}

A raysharp source is a Raysharp-made recorder, such as a Swann DVR, with live view, recordings, events and its disks; each channel is a camera. A stream source is cameras you list yourself, each with an id, a name and a main stream address, any that go2rtc plays (RTSP, RTMP, HLS, MJPEG and more), with an optional sub stream for small tiles. A camera with only a still address shows that picture on the Live page, a new one every few seconds. Streams have live view only: no recordings or events.

A reolink source is a Reolink camera, or a Reolink Home Hub or recorder with a camera on each channel, from its host, username and password. Start the host with https:// where its HTTP port is off. It has live view from each camera’s RTSP main and sub streams, so turn RTSP on in the camera’s network settings, and stills, motion and person, vehicle and animal detections, read every couple of seconds, and its disks or SD card. A hub or recorder’s recordings are copied into the recordings folder like a recorder’s: each file is replayed over port 9000, Reolink’s own protocol, and converted from H.265 to H.264 with ffmpeg. A camera’s own SD card recordings are listed beside the server’s, marked as on the source only and faded on the timeline, but not copied or played: it cuts a download short, and older firmware won’t replay a file. Record the camera on the server for footage you can play.

A bambu source is a Bambu Lab P1 or A1 printer’s camera, from its host and the LAN accessCode on its network settings screen. These printers have no video stream on the network, only a picture every second or two on port 6000, so the camera shows on the Live page as a still, a new one every few seconds. Turn on its LAN mode, or LAN-only liveview, for the feed to answer.

The printer’s recordings come from its storage over FTPS on port 990, with the same access code: what its camera records during prints, as the camera’s continuous footage, and the timelapse of each print, as a second camera, such as bambu/timelapse. They’re MJPEG, so each is converted to H.264 with ffmpeg as it’s copied or played, one at a time. Set KESTREL_FFMPEG for an ffmpeg that isn’t on the path; it needs libx264.

A source’s id names its cameras, such as swann/3, so links and labels keep working as long as it stays the same. It holds logins, so keep it to yourself. The API never sends a password back: the page shows which are set, and a save keeps each one you didn’t change.

The recorder’s details from the environment

Before there’s a sources file, the server runs one Raysharp recorder, swann, from the environment. A Raysharp source whose settings leave its address or login out falls back to the same variables, so the login can stay out of the file. Copy .env.example to .env.local, which git ignores, and fill it in:

Variable What it is
KESTREL_RAYSHARP_HOST The recorder’s address on your network
KESTREL_RAYSHARP_PORT The control port, 9000 unless it was changed
KESTREL_RAYSHARP_RTSP_PORT The RTSP port, from the recorder’s network settings
KESTREL_RAYSHARP_USERNAME A user on the recorder
KESTREL_RAYSHARP_PASSWORD That user’s password
KESTREL_RAYSHARP_RTSP_USERNAME The RTSP user, when it isn’t the one above
KESTREL_RAYSHARP_RTSP_PASSWORD The RTSP password, when it isn’t the one above

Live video logs in to the RTSP port with the RTSP user and password when they’re set. Some recorders keep a separate RTSP password: if live players show wrong user/pass, set it there.

mise run serve:server reads the file through 1Password’s op run, so the login can be op://vault/item/field references rather than the values themselves. A field whose name has spaces can be referenced by its ID.

So 1Password doesn’t ask each time the server starts, run mise run auth:save once. It fills in the references and saves the result to ~/.config/kestrel/recorder.env, which only you can read, and the server uses that file while it’s there. After changing the login, run it again; mise run auth:forget removes the file.

Starting the server

mise run serve:server

It runs in the background through Pitchfork, at https://server.kestrel.localhost, and serves the web client and its API. It starts each source as it starts, and a recorder logs in again, waiting longer each time, whenever the connection drops. A source that can’t start, such as one with no address, shows as not connected with the reason. mise run serve:server:logs follows its log.

The server listens on 127.0.0.1:7380. go2rtc only listens on 127.0.0.1:1984, and the server passes it through under /go2rtc/; it starts go2rtc again whenever the cameras’ streams change. go2rtc’s RTSP server stays on 127.0.0.1:1985, since its ffmpeg sources hand their output back through it. Set KESTREL_HOST to listen on another address, such as your machine’s address on your network to use it from a phone, and KESTREL_PORT, KESTREL_GO2RTC_PORT and KESTREL_GO2RTC_RTSP_PORT for other ports. The server doesn’t ask for a login, so only listen where everyone who can reach it should see the cameras and change its sources.

Desktop app

The desktop app runs the server itself and opens the web client in a window, so Kestrel keeps running without Pitchfork or a terminal. It listens on 127.0.0.1:7380 like serve:server, with go2rtc built in, and takes its sources from sources.json. For a recorder that leaves its address and login out there, it reads the file mise run auth:save keeps; it doesn’t read .env.local, since that needs 1Password.

Each release has it for Linux and Windows (x64): unpack the Setup file and run the installer in it, or install it with mise and start it with kestrel:

mise use -g github:timmo001/kestrel

Neither is signed, so Windows asks before running it, but each release’s files have a GitHub attestation, which mise checks, or check one yourself with gh attestation verify <file> -R timmo001/kestrel. Recording needs ffmpeg on the path, or KESTREL_FFMPEG; the app doesn’t bring its own. The Windows build hasn’t been tried on a real machine yet, and Start at login is Linux only. With mise, Start at login points at that version, so tick it again after upgrading.

Closing the window leaves it running in the tray. The tray menu opens the window again, shows whether the server is running, how many sources are connected and how many cameras it records, opens these docs and quits. The docs are built into the app, so they open in your browser without a connection, from the tray or the page’s More menu. If the server stops, the app starts it again, waiting longer each time up to 30 seconds. Opening the app again while it runs brings its window to the front.

Tick Start at login in the tray to start it in the tray when you log in. It adds ~/.config/autostart/dev.timmo.kestrel.desktop; untick it to remove that again.

The app is the always-on server, so stop the background one with mise run serve:server:stop while the app runs: both use ports 7380 and 1984 and the same recordings folder.

Task What it does
mise run desktop:start Builds the app for development and opens it
mise run desktop:build Builds the release, in packages/desktop/artifacts
mise run desktop:install Builds the release and installs it for you, in ~/.local/share/dev.timmo.kestrel

Recordings

Recordings are local first: the web client and CLI play from the recordings folder on the server wherever it has a copy. The server writes its own recordings there, and copies each source’s in beside them, with why each was made, such as motion, in the file’s name: <day>/<time>.motion.mp4. What a source has that isn’t copied yet, such as footage older than the hours kept, is listed too, marked as on the source, and plays from it. A source that can’t hand its recordings over, such as a Reolink camera’s SD card, has them listed as on the source only, faded on the timeline, with no Play or Download.

Copying from sources

Every 10 minutes the server looks at what each source with recordings of its own, such as a recorder, has from within the hours kept, and copies what the folder doesn’t have yet. It copies one file at a time, at full quality: clips first, then continuous footage, newest first. A file is written beside its final name and moved into place once whole. Past the disk share, a file is only copied when an older one would be cleared out to make room for it.

Recording cameras

The server can record cameras itself, for those whose source keeps no recordings of its own, such as a Reolink camera or a stream. It needs ffmpeg installed. Each picked camera’s main stream is read from go2rtc, so the camera serves one connection however many watch or record it, and saved unchanged, video only, as 5-minute MP4 files in recordings/<source>/<camera>/<day>/ in the state folder, or a folder you choose, such as a share on a NAS.

Keeping recordings

Each hour the server clears out old recordings, its own and those copied from sources alike:

Setting Default What it keeps
continuousHours 168 (7 days) Footage recorded all the time, in files with no reason or normal
clipHours 720 (30 days) Clips recorded for a reason, such as motion or PIR
diskShare 10 The most of its disk the recordings folder may use, as a percentage

Past the share, whatever is furthest through its own hours goes first: a clip a week into its 30 days goes before continuous footage an hour into its 7, and footage 4 days into its 7 goes before that clip. 5% of the disk is always left free. Copying from sources follows the same order, so it never copies an old clip only to clear out newer footage for it. A file written to in the last two minutes is left alone. Only folders with the server’s marker are touched, so a shared folder is safe.

Pick the cameras, folder and how long they’re kept in the Recording panel on the Settings page. They’re saved in sources.json under recording, beside the rest of the settings, so every client sees the same ones, and the server picks up a change without restarting.

After changing the server, restart it with mise run serve:server:restart. If it doesn’t come up without a saved login, op run may be waiting for 1Password to be unlocked.

Settings

Variable Default What it sets
KESTREL_HOST 127.0.0.1 The address the server listens on
KESTREL_PORT 7380 The server’s port (PORT too, for Pitchfork)
KESTREL_GO2RTC_PORT 1984 go2rtc’s port
KESTREL_GO2RTC_RTSP_PORT 1985 go2rtc’s RTSP port, on 127.0.0.1 only
KESTREL_GO2RTC go2rtc The go2rtc binary; the desktop app uses its own
KESTREL_FFMPEG ffmpeg The ffmpeg binary, for recording cameras
KESTREL_WEB packages/web/dist The built web client; the desktop app uses its own
KESTREL_CONFIG ~/.config/kestrel Where sources.json is kept
KESTREL_STATE ~/.local/state/kestrel Where push subscriptions and recordings go

Last updated on