BirdDog PLAY firmware package generator

Builds an installable .fw carrying an SSH key, Tailscale, an NDI KVM endpoint, a USB media player, a UVC converter, a streaming gateway — and any modules of your own. Everything is assembled in this browser — nothing is uploaded, and no existing firmware file is needed.

Access

Installed to /root/.ssh/authorized_keys. SSH on the PLAY listens on port 9031.

Payload

Tailscale
Current stable arm64 build into /userdata/tailscale, with a systemd unit. No auth key is baked in — it would sit in cleartext in the archive. Instead this adds a Tailscale panel to the System page of the PLAY's own web UI, so you sign the device in from a browser and never need SSH for it. Changing anything there needs a birdUI login; reading status does not. You can still tailscale up over SSH on port 9031 if you prefer. Stock firmware has no /dev/net/tun and ships no kernel modules at all, so you get userspace-networking. Inbound still works — SSH, the web UI and the :8080 API are all reachable over the tailnet, measured at ~195 Mbps against a 920 Mbps wired baseline. Enough for NDI|HX and SRT, not for full-bandwidth NDI.
Source: the binaries are Tailscale's own, tailscale/tailscale (BSD-3); the birdUI panel is stoatworks-labs/bdts (MIT).
NDI KVM endpoint
Installs bdkvm to /userdata/bd-kvm under its own self-supervising unit, rather than behind BirdDog's 1.0.34-only hooks (which bdup.sh deletes on every vendor update). Needs a USB keyboard or mouse attached and an NDI source selected to do anything.
Source: agent/ in this repo — Go, free NDI SDK, dlopens the device's own libndi.
USB media player beta
Adds a USB entry to the source dropdown and plays video, stills and PDFs off a USB stick, hardware-decoded straight to HDMI — in order or shuffled, looping until you stop it. Control it from that page, or from http://<play>:8091/.
Selecting USB stops BirdDog's decoder to take the display, and switching back to NDI restores it. This is the only option here that edits a firmware file: it adds one <option> to videoset.html, keeps a pristine backup beside it, and bdplay -unpatch-ui restores it byte-for-byte.
Why beta: two known rough edges, both measured rather than guessed. Repeatedly switching between USB and NDI destabilises BirdDog's own decoder — its birddog-runner aborts during NDI teardown, and on the test unit that went from 0 occurrences to 30 over an afternoon of switching. It restarts each time, and the player clears a failed state and checks the decoder came back, but expect the odd dropout if you flick between sources constantly; treat it as a mode you set. And playback has been proven from internal storage and a loopback exFAT volume, but not yet with a physical stick hotplugged.
Source: stoatworks-labs/bd-play-usb-player (MIT), plus PDFium and fuse-exfat as credited below.
UVC converter beta
Turns the PLAY into a converter the other way round: a USB camera on its USB-A port, out as NDI, NDI|HX, SRT or HDMI — any combination, from one capture. Settings live in a UVC Converter tab in the web UI, or on http://<play>:8091/'s sibling, :8090.
A UVC 1.5 camera is bound automatically. This kernel's uvcvideo matches only protocol 0 and ignores 1.5 devices silently — no device node, nothing in the log — so without that they simply never appear.
Why beta: it runs on a test unit and every claim here is measured, but two things are worth knowing. NDI|HX uses a compressed-packet layout reconstructed from the Advanced SDK's documentation rather than a header, so a future libndi could change it; the symptom would be a picture fault, not a crash. And at heights that are not a multiple of 16 — 1080 among them — the bottom 8 rows are cropped, because H.264 pads to a macroblock boundary and this decoder ignores the crop the standard puts in the stream, showing the padding as a green band.
Selecting HDMI directly stops BirdDog's decoder to take the display; routing HDMI through the decoder instead keeps the OSD, web UI and tally alive, and needs NDI|HX.
Source: stoatworks-labs/bdcam (MIT) — Go, against the device's own GStreamer and libndi.
Streaming gateway beta
RTMP, RTSP, HLS, WebRTC and UDP endpoints on the PLAY, in both directions. In: push RTMP from OBS or an encoder to rtmp://<play>:1935/live, pull an RTSP camera or an HLS playlist, publish WHIP from a browser, receive MPEG-TS or RTP over UDP — and put any of it on the HDMI output. Out: serve the UVC converter's camera over RTSP, RTMP, HLS, WebRTC and SRT, and push it to YouTube, Twitch or any RTMP(S), RTSP, SRT or WHIP destination. Settings live in a Streaming tab in the web UI.
The box runs MediaMTX as the hub; the picture reaches the screen through the PLAY's own decoder, which is pointed at the chosen path as an SRT source on loopback — the three files the stock AV Setup page writes, plus a restart. Nothing takes the display, so the OSD, tally and web UI stay as they are, and if the hub dies the PLAY behaves like a PLAY.
Why beta: this one has not yet run on a PLAY. Everything it relies on is read out of the firmware or measured by the projects beside it, and its configuration is validated against the real MediaMTX in CI — but the edge the whole inbound side rests on, the PLAY's decoder reading MediaMTX's SRT over loopback, is untested until the test unit is back. The decode ceilings are the chip's: H.264 to 1080p60 or 2160p30, HEVC to 2160p60; WebRTC publishers must send H.264. YouTube refuses video without audio, and the UVC converter has none yet.
Ports 1935, 8554, 8888, 8889 and 8890/udp open with no credentials — the LAN or the tailnet is the boundary, exactly as for the stock API on :8080. The tab itself is gated on your birdUI login, because it holds stream keys. Adds about 29 MB to the package; 62 MB on the device.
Source: stoatworks-labs/bd-play-stream-gateway (MIT) — Go; the hub is bluenviron/mediamtx (MIT), fetched here at build time and checked against its published SHA-256.
Reboot when finished
Off by default. Without it the installer restarts BirdDogRunner itself, so video comes back without a reboot.

Build

Names the file and is logged on the device as the installed build.

Leave empty to fetch the current stable arm64 release and verify it against the published SHA-256. Supply tailscale_<version>_arm64.tgz yourself to pin a version or build offline.

Only used with the streaming gateway. Leave empty to fetch the pinned release and verify it against the checksums.sha256 GitHub publishes beside it. Supply mediamtx_v<version>_linux_arm64.tar.gz yourself to build offline — the gateway's configuration is written for the pinned version, so another may not load it.

Add your own payloads. A module is a .tgz with a module.conf and an install script; it goes into the package at modules/<name>/ and the installer runs install as root after the payloads above. Each file is read and checked here the moment you choose it — structure only, never what install does. Read what you package. How to build one, or start from bd-play-module-template.

Installing it

  1. Open the PLAY's web UI and upload the .fw on the firmware page, exactly as you would a vendor release.
  2. Read the first-boot report at http://<play-ip>/static/bd-probe.txt — it works even if the SSH key did not take, and reports whether /dev/net/tun exists, free space, USB input topology, and one line per custom module with its exit code. The full install log, including everything your modules printed, is at /tmp/bd-custom-install.log on the device.
  3. ssh -p 9031 root@<play-ip>, then /userdata/tailscale/tailscale --socket=/run/bd-tailscaled.sock up.
  4. Delete the probe report when you are done: rm /srv/birddog-web-ui/static/bd-probe.txt.

Before you put it on a tailnet

The PLAY's REST API on :8080 has no authentication of any kind and sets Access-Control-Allow-Origin: *. Joining a tailnet does not change that — anyone who can reach the device on the tailnet can reconfigure it without credentials. Scope an ACL for this node; the device has no access control of its own to fall back on.

Undoing it

/userdata/bd-tailscale-ui/bdts --unpatch-ui && systemctl restart BirdDogWebUI
systemctl disable --now bd-tailscaled bd-kvm bd-tailscale-ui
rm -rf /userdata/tailscale /userdata/bd-kvm /userdata/bd-tailscale-ui /etc/systemd/system/bd-{tailscaled,kvm,tailscale-ui}.service

Take the first line before the others: it puts the System page back exactly as it shipped. A pristine copy is also kept beside it as settings.html.bdts-stock, and bdts --restore-ui restores from that.

The streaming gateway: clear Show on HDMI in its tab first, so the decoder is handed back, then /userdata/bd-gw/bdgw --unpatch-ui && systemctl restart BirdDogWebUI, systemctl disable --now bd-mtx bd-gw, and rm -rf /userdata/bd-gw /etc/systemd/system/bd-{mtx,gw}.service.

A custom module built from the template leaves its own uninstall beside its payload: bash /userdata/bd-<name>/uninstall. One without it is whatever its author documented — /userdata/bd-modules.log says what was installed and when.

Then reflash stock firmware if you want a clean unit. Recovery-mode flashing of the factory image is a proven path, but it restores factory state — not this unit's provisioned serial, hostname or /userdata.