Platform index / NDI video converter
BirdDog NDI converters (Studio / Flex / 4K / PLAY)
A range sold as one family that is really three platforms. Every converter in it is FPGA-based — Zynq-7000 on the 1080p line, Zynq UltraScale+ with a hard video codec unit on the 4K line — except the decode-only PLAY, which has no programmable logic at all and is an ordinary Rockchip applications processor running Debian.
The load-bearing finding. The NDI codec is not software. On every encoder in the range it is a block in FPGA fabric with a thin kernel driver bolted to it (birddog_speedhq.ko), which is why encoder and decoder, and 1 G and 10 G networking, ship as separate bitstreams rather than as settings. PLAY is the exception that proves it: with no fabric to put a codec in, it decodes full-bandwidth NDI in software on four Cortex-A53s.
Verdict. Hardware everywhere it earns its keep, and a plain SoC where it does not. The encoders are FPGA machines: a Zynq-7000 on the 1080p line and a Zynq UltraScale+ with a hard codec unit on the 4K line, with the NDI codec itself living in fabric behind a thin kernel driver — which is why what a 4K unit is (encoder or decoder, 1 G or 10 G) is chosen by loading a different bitstream rather than by setting a flag. PLAY inverts all of it: no programmable logic, a Rockchip RK3328 on an unmodified Firefly reference device tree, 2 GB of RAM and four Cortex-A53s at 1.392 GHz, hardware-decoding NDI|HX through the video unit while decoding full-bandwidth SpeedHQ in software on the same four cores. The asymmetry is not an accident — the measured encoder on that SoC tops out around 31 fps at 720p with no HEVC and no B-frames, which is precisely why BirdDog never built an encoder on one. Around all three platforms sits the same delivery chain: an encrypted container whose scope is the whole product line and every generation of it, an integrity check over a shell script, a model-name gate, and no signature verification at any point.
Method
Two bases, deliberately kept apart. The range-wide silicon inventory is static analysis of twenty firmware packages published on BirdDog's own unauthenticated download pages, 2020–2025, plus the RKFW recovery image for PLAY. Everything labelled measured was then read off one running PLAY on our bench (unit .42, firmware 1.0.30) — the first entry in this index with any hardware behind it. No unit was opened, no FPGA model was ever powered, and every claim about Studio, Mini, Flex and the 4K family remains file-only.
Artifacts examined
BirdDog_× 20 — each a gzip'd- .zip .fwholding a GNU tar ofbdpff(the encrypted payload), a 207-bytebirddog-update.shstub and an unstripped aarch64 or armhfupdateELF- Xilinx
.bitheaders throughout the FPGA models — the single most reliable silicon evidence in the corpus, naming the exact part, the design name and the Vivado version that built it - ZynqMP device trees from the 4K family, whose
modelstring names the configuration outright (ZCU5EV 4K Single Encoder Model V202 PS 1G No PHY ID vcu ini) PLAY_1.0.30.img— the RKFW/RKAF recovery-mode image: loader, u-boot, trust, kernel, DTB, rootfs and partition table, i.e. the whole device rather than an overlay/bin/PPApp— the PLAY application, itsDT_NEEDEDlist, its imported NDI symbols and its own log strings- One PLAY, firmware 1.0.30, on the bench — the source of every
measuredrow below
No manufacturer firmware, binaries or documentation are redistributed here. Artifacts are referenced by name and version only. See the method and legal statement for the basis on which this analysis was performed and the boundaries it observes.
Hardware architecture
| Item | Finding | Confidence |
|---|---|---|
| Two FPGA platforms, one SoC | Xilinx XC7Z020-CLG400 (Zynq-7000, 2× Cortex-A9, 32-bit armv7l userland) on Studio NDI R3/R4, Mini and all three Flex 4K models; Xilinx XCZU5EV-FBVB900-1-E (Zynq UltraScale+ MPSoC, 4× Cortex-A53) on 4K HDMI, 4K SDI, 4K QUAD and OG4/GEAR. Read out of the .bit headers on every model and every firmware generation, and corroborated by the ZynqMP device tree | Verified |
| The `EV` suffix is the point | XCZU5EV carries the hard H.264/H.265 VCU, and the 4K device trees use it: vcu@80300000 with vcu_core_enc, vcu_core_dec, vcu_mcu_enc and vcu_mcu_dec nodes. One vendor image is even named …rxfw330d04apr25vcu.img.gz. So on the 4K units, NDI|HX rides the hard codec and full-bandwidth SpeedHQ rides the fabric | Verified |
| Modes are FPGA images, not software settings | The 4K units ship a tree of bitstreams and load one at runtime: etc/ENC/birddog-nicmode-1g/system.bit, etc/ENC/birddog-nicmode-10g/…, etc/DEC/…, plus DUAL_ENC, DUAL_DEC, M1X1 and M2X2. Encoder versus decoder, and 1 G versus 10 G networking, are different bitstreams. On 4.5-LTS the QUAD's input/output split was baked in the same way — quad_1i3o, quad_2i2o, quad_3i1o as three separate builds — while by 6.1.349 the HDMI and SDI units share one unified SDI_HDMI_ENC_DEC design | Verified |
| The Zynq-7000 platform is a third-party SoM | Kernel is 4.4.0-snickerdoodle+ — the BSP derives from the krtkl snickerdoodle Zynq-7000 module, not a BirdDog carrier BSP. The bitstream is reloaded from userspace at every boot (cat /etc/tiny_4k.bit > /dev/xdevcfg, then poll f8007000.devcfg/prog_done — the Zynq DevC controller), and every design in this group was built with Vivado 2016.4 | Verified |
| PLAY — no FPGA at all | Rockchip RK3328: four Cortex-A53 in a single cluster, Mali-450MP2, HDMI 2.0a. No .bit file, no /dev/xdevcfg, no fpga_manager anywhere in the image. The recovery image says so in vendor metadata — RKAF header model = RK3328, parameter.txt MACHINE: 3328 — after the sysfs GPU address had already pinned it | Verified |
| PLAY's board is a reference design | The device tree in the recovery image is stock Firefly ROC-RK3328-CC: model = "Firefly ROC-RK3328-CC", compatible = "firefly,roc-rk3328-cc", "rockchip,rk3328". BirdDog ship the reference board's DTB unaltered, so PLAY is that publicly documented design or close enough that its device tree boots unmodified | Verified |
| PLAY memory and clock | 2 GB RAM, four A53s capped at 1.392 GHz, governor performance. No firmware image can answer this — there is no memory node in the DTB, because the Rockchip loader detects DRAM and patches it in at boot — so it had to come off a running unit | Measured |
| PLAY video blocks | rkvdec@ff360000 and vpu_service/vdpu@ff350000 for decode, vepu@ff340000 for encode, rockchip,rk3328-vop@ff370000 and rockchip,rk3328-dw-hdmi@ff3c0000 for display, with power domains pd_vpu, pd_hevc and pd_video. PPApp's own mode table runs 2160p23.97 to 2160p60 | Verified |
| What the PLAY's decoder actually does | Hardware decode is effectively free: a 1080p H.264 clip plays in real time at ~11% of one core, the VOP confirmed scanning out NV12 on win1-0 at 1920×1080p60, and the decoder reaches 218 fps at 720p. This is the block the whole product rests on and it is barely working | Measured |
| What the PLAY's encoder cannot do | The vepu is a 2017 Hantro-class block: H.264 only, no HEVC, no B-frames. Its GStreamer element's sink caps stop at 1920×1088 at 60/1, it exposes no tunable properties at all in this build (it derives a bitrate from the caps — 3.456 Mbps at 720p30), and it measured ~31 fps at 720p under load and ~19.6 fps at 1080p in a decode-then-encode pipeline. 4K encode is not available at any effort. Every BirdDog encoder product is FPGA for exactly this reason | Measured |
| PLAY networking and USB | Gigabit: gmac2io in RGMII with an external PHY, the internal 100 Mbit gmac2phy path disabled — and the unit sustained 115 MB/s, ~920 Mbps, line rate on the wire. The USB-A socket is a host port that enumerates keyboards, mice and a UVC camera; the device tree enables the USB 2.0 OTG controller, EHCI/OHCI host and a USB 3.0 dwc3 alongside each other, and which of them reaches the socket was not established | Measured |
| PLAY audio | HDMI audio is card 1 (rockchiphdmi, hw:1,0). Card 0 is the SoC's own i2s and goes nowhere on this board — a distinction no amount of reading the device tree makes obvious | Measured |
Software architecture
| Item | Finding | Confidence |
|---|---|---|
| Three operating systems | Zynq-7000 models run 32-bit armv7l Linux 4.4.0-snickerdoodle+; the 4K family runs 4.14.0-xilinx-v2018.1 on 4.5-LTS through 5.4.0-xilinx-v2020.2 on 5.5; PLAY runs Debian 10 aarch64 on a Rockchip BSP kernel 4.4.194 (Linaro GCC 6.3, built July 2023 — a Firefly/Rockchip build, not BirdDog's) | Verified |
| PLAY's kernel has no modules — none at all | lsmod is empty, there is no /lib/modules/4.4.194/modules.dep.bin, and modprobe cannot do anything. Everything the product needs is built in: uvcvideo (1.1.1), usb-storage, vfat, ntfs. Things that are not built in cannot be added without replacing the kernel — CONFIG_TUN is absent, and so is exFAT. FUSE, by contrast, is complete (/dev/fuse at 10,229, both fuse and fuseblk registered), which quietly moves a whole class of problems into userspace | Measured |
| Where the NDI codec lives | birddog_speedhq.ko — the driver for the SpeedHQ block in fabric — ships with every encoder model (Flex IN, Flex BP, 4K HDMI, 4K SDI, 4K QUAD, OG4/GEAR) and is absent from the decode-only ones (Flex OUT, PLAY). It sits alongside birddog_audio.ko, birddog_audio_playback.ko and birddog_aes3audio_playback.ko. The codec is a hardware block with a thin driver, not a library | Verified |
| PLAY runs two receivers in parallel | PPApp creates both and picks: NDIRecvVideoHX() hardware-decodes NDI|HX (H.264/HEVC) through Rockchip MPP, while NDIRecvVideoFull() decodes full-bandwidth SpeedHQ in software inside libndi.so.5, on the four A53s. A shipping commercial product decodes full-bandwidth NDI on a 2017 quad-A53 with no hardware assistance whatsoever | Verified |
| The NDI library surface on the device | Both shipped copies (libndi.so.5.5.2 and libndi.so.6.0.1) export entry points that do not exist in the free NDI SDK — NDIlib_recv_create_v4, NDIlib_recv_set_video_allocator and NDIlib_routing_create_v2 on the receive side, and send_compressed_video, NDIlib_send_get_target_bit_rate and NDIlib_send_is_keyframe_required on the send side. Checked against a complete free-SDK header set held locally, where all of them are absent. PPApp imports the receive ones, which is what lets the video processing unit see a compressed bitstream at all. Both copies also carry the SDK's 30-minute development-use string, which appears in every build of that library and therefore proves nothing | Verified |
| The rootfs carries a media stack the product never uses | GStreamer 1.14.4 is on the stock PLAY rootfs with the Rockchip rockchipmpp plugin (mppvideodec, mpph264enc, mppjpegdec, mppjpegenc) and kmssink — none of which PPApp touches. PPApp's DT_NEEDED is librockchip_mpp.so.1, libdrm.so.2 and libndi.so.5 and little else; it binds MPP output buffers straight into DRM framebuffers with no scaler and no framework in between | Measured |
| One display owner | PPApp is DRM master on card0 and there is no sharing — two processes cannot both modeset. Anything else that wants the HDMI output has to stop it first, which is confirmed on hardware: kmssink works only once the vendor runner is stopped, and while it is stopped the output is simply dark | Measured |
| PLAY and Pod are one image | The installer accepts exactly two hardware identifiers for this package — BirdDog PLAY and BirdDog Pod — and S99birddog-setup splits them at first boot by probing for a wlan0 interface: Wi-Fi present ⇒ PLAY, absent ⇒ Pod. Pod additionally enables the UVC and UAC1 USB gadgets so it enumerates as a webcam; PLAY explicitly disables ADB. Pod ends up on Debian 11 and PLAY on Debian 10, from the same package, via a node_modules_bullseye swap | Verified |
| Control surfaces | Three, of which only one has a login. A Go web UI on port 80; an Express REST API on port 8080 — plain readable JavaScript in /bin/BirdDogSrvr/ with a per-feature module layout, roughly 80 endpoints, and a spreadsheet in the image (etc/API_Management.xlsx) documenting them; and SSH on port 9031, moved there by S99birddog-setup rewriting sshd_config. libsrt is statically linked into PPApp, so source selection is NDI, SRT or CloudConnect | Verified |
Update path & security model
| Item | Finding | Confidence |
|---|---|---|
| Firmware container | Every published .fw is a gzip'd GNU tar holding an AES-256 encrypted payload (bdpff) beside the small shell stub and the update binary that decrypts it. The interesting structural fact is the scope: one password serves the entire product line — PLAY, Flex, 4K HDMI/SDI/QUAD, OG4/GEAR — and every firmware generation sampled, 2023 through 2025. Four distinct update binaries across twenty payloads, one key | Verified |
| Pre-2022 firmware is not encrypted at all | Mini 20.11.x, Studio NDI R3/R4 20.11.x and Flex 4.5.x-LTS skip the encrypted layer entirely — the inner tar is the birddog-platform-files/ rootfs overlay, in the clear. The encryption is a later arrival, not a family-wide policy | Verified |
| Integrity, not authenticity | The update binary checks an md5 of the 207-byte shell stub against a hard-coded digest, and the installer gates on /etc/birddog-hardware-version (a plain string such as BirdDog PLAY) against a whitelist. Those are the only checks in the chain: there is no signature verification of any kind anywhere in the update path. The encryption layer is obfuscation, and the model gate is a compatibility check, not an authenticity one | Verified |
| Payload contents | Inside: an installer script, a birddog-platform-files/ tree copied verbatim onto / with cp -rf, optional .deb packages, and on the Zynq models the BOOT.BIN carrying first-stage loader, bitstream and u-boot. The kernel and bootloader are not in the update package on PLAY at all | Verified |
| Network services | The REST API on port 8080 has no authentication of any kind — no auth middleware anywhere in /bin/BirdDogSrvr/*.js — and sets Access-Control-Allow-Origin: *. The web UI on port 80 has a login; the API behind it does not. SSH listens on 9031. The web UI's static/ directory is served without authentication too. Confirmed answering on the bench unit with no credentials | Verified |
These rows describe how a platform validates a firmware image, because that is a structural fact about its architecture. They are not a vulnerability disclosure and no exploit, bypass or circumvention technique is published here. Licensing and entitlement mechanisms are out of scope throughout — seescope boundaries.
What is programmable
- 1The REST API
Roughly eighty endpoints of plain, readable Express JavaScript with the surface documented in a spreadsheet inside the firmware itself, mounted per feature and reachable without credentials. Anything that wants to drive one of these boxes from OSC, AES70 or a control surface talks to this and needs no C, no firmware and no reverse engineering. It is also, for the same reasons, the platform's largest exposure.
- 2PLAY's media stack, within the display constraint
GStreamer 1.14.4, the Rockchip MPP plugins and
kmssinkare already on the rootfs, and Rockchip MPP is open source with no proprietary firmware dependency — so the decode and encode paths are reachable without anything from BirdDog. The binding constraint is not access, it is ownership:PPAppis DRM master, so the box shows the vendor application's output or somebody else's, never both. - 3Nothing inside the fabric
On every model except PLAY the NDI codec is a block in a bitstream shipped inside the vendor package, reached through a vendor kernel driver. The bitstreams are unencrypted and name their part, which makes them legible and not useful — the design is the product, and there is no third-party path into it.
Traps
Mistakes this analysis actually made, or came close to making. They are recorded because each one produces a plausible-looking wrong answer rather than an obvious failure.
- A GPU's physical base address, sitting in a sysfs path, is the sharpest Rockchip part discriminator available from firmware alone.
/sys/devices/platform/ff300000.gpu/…meansgpu@ff300000, which isrockchip,rk3328-maliin mainline and nothing else: RK356x isfde60000, RK3399ff9a0000, PX30ff400000. A register map is a hard fact and a marketing name is not, so an address that leaked into a path in a startup script identified the silicon before any vendor metadata was found. The limit is worth knowing too — RK3318 is indistinguishable, because it is the same die and the same register map in a different cost bin. The technique identifies a design, never a part number. - Grepping a rootfs for vendor names produces confident nonsense.
integrationcontainstegra;cambandwidthcontainsamba; a sweep for a Rockchip 2D engine hit repeatedly on web-UI assets in a binary that turned out not to link it at all. The Xilinx.bitheader, by contrast, names the exact part, the design and the tool version in plain text at a fixed offset. Where an artifact self-identifies, a substring search is not a cheaper version of reading it — it is a different and worse method. - "No GStreamer" was true of the application and false of the machine. The conclusion came from the vendor binary's
DT_NEEDEDlist, which is genuinely short — MPP, DRM, libndi. The rootfs it sits on ships GStreamer 1.14.4 with the full Rockchip MPP plugin set andkmssink, all unused by the product and all perfectly usable by anything else. An application-linkage finding bounds what that program does; it says nothing about what is installed. Check the filesystem before concluding something is absent. - The PLAY locks its HDMI output to the first stream it sees and silently downscales everything after it. A 2160p stream sent to a unit already locked at 1080p60 was scaled 2:1 with no error and no indication — fine gratings vanished, and the resulting measurement read as a legibility floor at 4K that does not exist. The device's own status endpoint went on reporting the original resolution throughout. Changing resolution needs the source torn down and the decoder restarted, not just a new stream. The wrong answer here was plausible, quantitative and reproducible, which is exactly what makes it dangerous.
- That same status endpoint is only trustworthy once it is locked. While failing it reports a zero resolution, a zero frame rate and a chroma format of
4:2:2regardless of the stream — a value the 4:2:0 test stream could never produce. A non-zero resolution is the only field in it that means anything.
Open questions
- RK3328 or RK3318. RK3318 is a cost-reduced bin of the same design with the same register map, and vendor images — including the device tree — are shared between them. Nothing in the firmware, and nothing readable from a running unit, distinguishes the two. Only the marking on the chip settles it, and the unit was not opened.
- What full-bandwidth NDI receive actually sustains on PLAY. The send side was measured — SpeedHQ encode costs about 1.18 cores at 720p30, which scales to roughly 2.6 cores at 1080p30 — but the equivalent receive figure was never logged on stock firmware, and 1080p60 versus 2160p is a fourfold difference in what the product can honestly claim.
- Which USB controller the PLAY's USB-A socket is wired to. The device tree enables the USB 2.0 OTG controller, the EHCI/OHCI host and a USB 3.0 dwc3 side by side; the socket enumerates devices, so one of them reaches it. This decides whether uncompressed capture above 720p is possible at all, and it is a screwdriver question, not a firmware one.
- Whether Studio and Mini genuinely lack
birddog_speedhq.koon the device, or whether their 2020 update packs simply do not refresh a module that is already there. Absence from an overlay is not absence from a rootfs — the same mistake the GStreamer trap below records. - What the non-Xilinx
.bitfiles under Studio R3'setc/birddog-rev2/are. They are in a different container, they are not Xilinx bitstreams, and they were not identified. - PLAY Pro is entirely unknown. BirdDog publish no firmware for it, so there is nothing to analyse — not the SoC, not whether it has fabric, not whether it shares the PLAY image.
Status of this entry. A written teardown exists and its claims carry evidence citations. Rows labelled Measured were read off a running unit; everything else on this page is derived from files and has not been checked against hardware.