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
/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.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.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.
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.
libndi.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.
host:port for one on another subnet, and press SHOW ON HDMI.
The receiver speaks the OMT 1.0 wire protocol itself and decodes its VMX video with the codec's reference library, patched to write the one pixel format the PLAY's display path accepts. Like the USB media player it takes the display: the PLAY's decoder is stopped while a source is shown, so NDI, the OSD and tally are gone until you press STOP, at which point it's started again. A source that was on air comes back after a reboot.
Why beta: it has not yet run on a PLAY. It has received a real OMT sender at 1080p60 off the device, and its GStreamer pipelines run under the PLAY's own GStreamer version, but whether four Cortex-A53s decode 1080p60 VMX in time is the question only the hardware can answer. 720p is the safer bet until then. Interlaced sources aren't supported, and the sound isn't lined up with the picture (it runs a little late).
The API is on port 8094 and every change is gated on your birdUI login. Adds about 7.5 MB.
Unlike the USB media player and the OMT receiver, it doesn't take the display. A small shim preloaded into the PLAY's own decoder changes the mode list it reads and where it puts each frame, so NDI, the OSD and tally all carry on. A new format is tried: the decoder restarts on it and, unless you press KEEP IT within 30 seconds, the last one you kept comes back on its own, so a format the screen can't show doesn't leave you in the dark. The area of interest takes at once, with no restart.
Why beta: it has not yet run on a PLAY. The shim has run inside a stand-in for the PLAY's decoder under the PLAY's own glibc, but whether the real one lets it in is the first thing the hardware will answer. The PLAY's HDMI chip makes twenty pixel clocks and no others, so custom sizes are fitted to those.
The API is on port 8095 and every change is gated on your birdUI login. The installer restarts the PLAY's decoder so the shim loads. Adds about 6.5 MB.
The screens are drawn on the PLAY from the room's state, which Stagetimer's public API sends live; there's no browser involved, so the room's logo and custom fonts and colours aren't reproduced. The API key is kept on the PLAY, readable by root only, and never shown back. Like the USB media player it takes the display: the PLAY's decoder is stopped while the timer is shown, so NDI, the OSD and tally are gone until you press STOP, at which point it's started again. A timer that was on air comes back after a reboot. The PLAY needs to reach
api.stagetimer.io. Not affiliated with Stagetimer.
Why beta: it has not yet run on a PLAY. It has met Stagetimer's real API only far enough to be refused with a made-up room, and its frames have gone through the PLAY's own GStreamer version inside the PLAY's factory system image, but the handover, the colours and the drawing speed on the hardware are still open.
The API is on port 8096 and every change is gated on your birdUI login. Adds about 9 MB.
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 the module template.
- File
- Size
- SHA-256
Installing it
- Open the PLAY's web UI and upload the
.fwon the firmware page, exactly as you would a vendor release. - 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/tunexists, 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.logon the device. ssh -p 9031 root@<play-ip>, then/userdata/tailscale/tailscale --socket=/run/bd-tailscaled.sock up.- 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.
The OMT receiver: press STOP on its tab first, so the PLAY's
decoder is started again, then
/userdata/bd-omt/bdomt --unpatch-ui && systemctl restart BirdDogWebUI,
systemctl disable --now bd-omt, and
rm -rf /userdata/bd-omt /etc/systemd/system/bd-omt.service.
HDMI formats and area of interest:
/userdata/bd-out/bdout --unpatch-ui && systemctl restart BirdDogWebUI,
rm /etc/systemd/system/BirdDogRunner.service.d/bd-out.conf && systemctl daemon-reload,
systemctl disable --now bd-out, systemctl try-restart BirdDogRunner
(so the decoder starts again without the shim), and
rm -rf /userdata/bd-out /etc/systemd/system/bd-out.service.
Stagetimer: press STOP on its tab first, so the PLAY's decoder
is started again, then
/userdata/bd-stage/bdstage --unpatch-ui && systemctl restart BirdDogWebUI,
systemctl disable --now bd-stage, and
rm -rf /userdata/bd-stage /etc/systemd/system/bd-stage.service, which takes the
room's API key with it.
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.