An open-source NVR running on a Raspberry Pi, competing with OBS for live streaming work? This post covers how we took the "surveillance camera → live stream" pipeline from functional to polished — and how many protocol-layer traps we hit along the way.
Above: Live stream viewed on the mobile app. Relay handled natively by MiBeeNVR's Go implementation — no FFmpeg dependency.
TL;DR
MiBeeNVR v0.8.1 supports pushing any connected camera feed directly to live streaming platforms via RTMP. Pure Go implementation — no FFmpeg installation, no external processes. Configure your stream URL and go live.
⚠️ The author has only verified streaming on the Douyu platform so far. Other platforms are theoretically compatible (all implement the FMS standard), but remain untested. If you run into issues, please report them on GitHub Issues.
Runs on Raspberry Pi 3B and above. Supports both H.264 and H.265 cameras.
What Is MiBeeNVR?
MiBeeNVR is an open-source Network Video Recorder (NVR) written in Go, optimized for embedded devices like the Raspberry Pi and Banana Pi. Prior to v0.8.1, it already shipped with:
- Multi-camera management and recording (RTSP / ONVIF / Xiaomi dual-camera)
- H.264/H.265 hardware transcoding
- Time-lapse recording and synthesis
- LL-HLS / WebRTC low-latency playback
- One-command Docker deployment
One thing was missing, though: live streaming.
The community kept asking: "Can we push camera feeds directly to a live stream?" Pet owners wanted pet live streams. Security folks wanted public surveillance channels. Hardware tinkerers just wanted to play. v0.8.1 fills that gap.
Live Streaming: Looks Simple, Hurts Plenty
Step 1: Make It Work First
The most straightforward approach is to call FFmpeg — it's the Swiss Army knife of the streaming industry, and OBS uses it under the hood. Configure an RTMP target in MiBeeNVR's "relay output," spin up an FFmpeg subprocess on the backend, and the feed does reach the live stream.
But here's the problem: FFmpeg is an external process. It needs separate installation, separate lifecycle management, and extra memory. For a project aiming to "run lightweight on a Raspberry Pi," dragging along a multi-megabyte external process for streaming isn't elegant.
What we wanted: a pure Go RTMP streaming implementation, embedded in the NVR process, with zero external dependencies.
Step 2: Six Layers of Hell in Pure Go Streaming
The RTMP protocol looks simple — handshake, connect, push data. But when you actually interface with live streaming platforms (whose backends are all Adobe FMS-compatible implementations), you'll find that every single layer can block you.
We didn't encounter one problem — we encountered six stacked problems:
flowchart TD
A["L1 Handshake digest<br/>HMAC-SHA256 signing"] --> B["L2 Chunk size<br/>mismatched block size"]
B --> C["L3 Chunk header<br/>Type 0/1/2/3 format"]
C --> D["L4 Background read<br/>buffer overflow"]
D --> E["L5 Metadata fields<br/>missing width/height/fps"]
E --> F["L6 StreamID byte order<br/>big-endian vs little-endian ⭐"]
F --> G["✅ Stream success"]
style A fill:#FF9800,color:#fff
style B fill:#FF9800,color:#fff
style C fill:#FF9800,color:#fff
style D fill:#FF9800,color:#fff
style E fill:#FF9800,color:#fff
style F fill:#f44336,color:#fff
style G fill:#4CAF50,color:#fff
After fixing the first four layers — handshake passing, connection established, publish command accepted — the first video frame would be sent, and 74 milliseconds later, the connection would be reset by the server.
That 74ms is critical: it rules out a network timeout (those take ~5 seconds). The server was rejecting the data at the frame level.
Final Boss: A Tale of Byte Order
The root cause was buried so deep that it took us a full day to find.
The RTMP protocol has a field called MessageStreamID that identifies which stream a message belongs to. Per the RTMP spec, this field should be little-endian. So we dutifully wrote it as little-endian in our code, exactly as the spec says.
But every major implementation — FFmpeg, OBS, gortmplib — uses big-endian.
And so do the live streaming platforms.
Our code used the third-party library's standard path (big-endian) for most messages, but our own custom write path used little-endian for others. On the same connection — sometimes big-endian, sometimes little-endian. The server received this schizophrenic packet stream and immediately sent an RST.
We found the bug through an in-process byte-level dump — logging every byte sent, parsing each chunk header one by one — until we spotted the endianness inconsistency in the streamID field. The fix was 4 lines of code. Finding it took an entire day.
flowchart LR
A["Go standard Writer<br/>streamID = big-endian"] --> C["Live platform<br/>expects big-endian ✅"]
B["Our Writer<br/>streamID = little-endian"] --> D["Live platform<br/>receives little-endian ❌ RST"]
style A fill:#4CAF50,color:#fff
style B fill:#f44336,color:#fff
style C fill:#4CAF50,color:#fff
style D fill:#f44336,color:#fff
TL;DR: The spec says go left. The entire world goes right. You follow the spec — and the entire world rejects you.
The Result
With all six layers fixed:
- ✅ Pure Go relay streaming works — no FFmpeg dependency
- ✅ TCP connection stays stable, continuous streaming without interruption
- ✅ Raspberry Pi camera feeds push to live streams in real-time, ~2-3 second latency
- ✅ H.264 cameras supported; H.265 streaming coming soon
⚠️ Do not enable the
use_ffmpegoption. FFmpeg-based streaming has known issues: playback may stutter mid-stream due to frame rate mismatch, and it won't auto-recover. The pure Go relay path is thoroughly validated and is the recommended streaming method.💡 The pure Go relay is the focus of this release. If you encounter Go streaming issues on other platforms, please prioritize filing an Issue about the Go path to help the project keep improving — rather than falling back to FFmpeg.
How to Use It
Option 1: MiBeeNVR UI Configuration
In the camera settings, find "Relay Output" and enter your live streaming platform's RTMP URL:
rtmp://<server>/live/<stream-key>
Get the stream key from your live streaming platform's broadcast settings.
Option 2: Docker Deployment
docker run -d \
--name mibee-nvr \
-p 8080:8080 \
-v mibee-nvr-data:/var/lib/mibee-nvr \
ghcr.io/mi-bee-studio/mibee-nvr:latest
Open http://<device-ip>:8080 to access the admin interface, add your cameras, then configure relay output.
Testing Status & Caveats
Tested platform: Douyu Live (companion app mode, LAN streaming) ✅
Theoretically compatible: All FMS-standard live streaming backends. However, implementation details vary across platforms — untested ones may present issues. If you hit problems, please report them on GitHub Issues and the author will follow up.
⚠️ Important Notice
Continuous long-term streaming to domestic live platforms is not recommended. Each platform has its own streaming policies and restrictions. Prolonged unattended camera streaming may trigger:
- Throttling (reduced bitrate/resolution)
- Stream key expiration leading to disconnection
- Account warnings or even bans
Make sure to understand your platform's policies and follow their rules. MiBeeNVR provides a technical capability — how you use it, on which platform, and what you stream is your own responsibility.
Lessons Learned
This live streaming project left us with three key takeaways:
1. Comparative Experimentation Is the Gold Standard for Debugging
When the Go code failed to stream, we couldn't tell if it was a code issue or an environment issue. Only after manually streaming via FFmpeg CLI on the same device and succeeding did we confirm the problem was in the Go protocol layer. Without that control experiment, all the guessing in the world is meaningless.
2. Binary Protocol Bugs Require Looking at Bytes
Byte-order issues are invisible when reading code — because the code "looks correct" (it follows the spec). Only by dumping the actual bytes sent over the wire and parsing them one by one can you spot the endianness mismatch. When debugging binary protocols: add a dump first, analyze second.
3. Spec ≠ Reality
The RTMP spec says MessageStreamID is little-endian, but the entire world uses big-endian. This kind of "spec vs. practice divergence" is extremely common in legacy protocols. When your spec-compliant code hits walls everywhere, go look at how others actually implemented it — de facto standards are more persuasive than paper specifications.
About MiBeeNVR
MiBeeNVR is an open-source project by Mi-Bee-Studio, written in Go and optimized for embedded devices like the Raspberry Pi.
GitHub: https://github.com/Mi-Bee-Studio/MiBeeNvr
Key Features:
- Multi-camera management (RTSP / ONVIF / Xiaomi dual-camera)
- H.264/H.265 hardware transcoding (V4L2 / VAAPI / NVENC auto-detection)
- Time-lapse recording and synthesis (pure Go muxer, zero encoding overhead)
- LL-HLS / WebRTC low-latency playback
- RTMP live streaming (new in v0.8.1, pure Go)
- One-command Docker deployment
Supported Platforms: Raspberry Pi 3B+ and above / x86_64 / ARM64 / Docker
MiBeeNVR v0.8.1 has been released. See the full changelog at GitHub Release Notes.
If you're building interesting projects with Raspberry Pi, come chat with us on GitHub.


Top comments (0)