MiBee Eye has had two implementations so far, both aimed at Raspberry Pi-class boards. This is the new one: laptop, desktop and mini-PC support, capturing the camera and microphone attached to the machine and presenting them as a device your existing NVR can pick up.
The laptop in the drawer
Most of us have one: a laptop that's too old to be a daily driver and too good to throw away. It has a webcam, a microphone, a NIC and a disk. What it doesn't have is a reason to be switched on.
At the same time, buying a camera means accepting the usual deal — someone else's cloud gets the video, a subscription unlocks the useful parts, and the firmware stops when the product line does.
MiBee Eye Notebook is the middle layer for the spare machine. It runs on a desktop, laptop or mini-PC, captures the camera and microphone physically attached to that machine, and exposes them as a standard device an NVR can discover. Day to day you drive it from a browser.
What it is, and what it deliberately isn't
The scope is narrow on purpose, and the boundaries are written down as definitional rather than as engineering decisions someone might revisit.
It is a local capture agent. It runs as a system service. It captures USB webcams, built-in laptop cameras, and USB or built-in microphones from the machine it's installed on.
It is not a network camera scanner. It never discovers, scans or pulls RTSP/ONVIF streams from remote cameras — if the camera isn't plugged into this machine, it's out of scope. It isn't an NVR: it doesn't manage or record third-party cameras. It isn't a cloud service, and it's designed to run fully air-gapped. It isn't multi-tenant: one admin account, no per-user ACLs.
Inside the machine:
flowchart TD
CAM["Camera<br/>USB or built-in"] --> CAP["Capture<br/>V4L2 read"]
CAP --> ENC["H.264 encode<br/>openh264"]
MIC["Microphone<br/>USB or built-in"] --> AUD["Audio encode<br/>G.711 or AAC"]
ENC --> HUB["Encoded frame hub"]
HUB --> LOCAL["Local output"]
LOCAL --> PRE["Browser preview"]
LOCAL --> REC["MP4 recordings"]
HUB --> PULL["Pull from outside"]
PULL --> RTSP["RTSP server"]
PULL --> OV["ONVIF device"]
HUB --> PUSH["Push to outside"]
PUSH --> RTMP["RTMP push"]
PUSH --> GB["GB28181 device"]
AUD --> REC
Default ports: web UI 8443 (TLS), RTSP 8554, RTMP 1935, SIP 5060.
It's part of the MiBee Eye family alongside mibee-eye-go and mibee-eye-rs, which target Raspberry Pi-class boards. Same protocols, same SPEC v1 web API, same NVRs. Pick by the machine you have: a board with a CSI camera or a USB camera goes to one of those; a PC or laptop goes here.
Your microphone is a first-class input
This is the part the board-based implementations don't do, and it's what makes repurposing a laptop interesting.
The microphone path forks into two encoders for two purposes:
flowchart TD
MIC["Microphone"] --> PCM["PCM capture"]
PCM --> G711["G.711 encode"]
PCM --> AAC["AAC encode<br/>optional feature"]
G711 --> GB["GB28181 stream"]
AAC --> REC["MP4 recordings"]
AAC --> RTMP["RTMP push"]
PLAT["Platform talkback"] --> DEC["G.711 decode"]
DEC --> SPK["Local speaker"]
G.711 μ-law is the default: pure Rust, used for the GB28181 live stream and for decoding platform talkback. AAC covers MP4 recordings and RTMP push, but it sits behind a compile-time feature that's off by default, because it pulls in an external encoder library.
Talkback runs the other direction too. When a platform opens an audio-only INVITE, the device decodes G.711 and plays it out of the local speaker. That's on by default. If it's disabled in config, or the machine has no usable output device, those INVITEs get a 488 — not a 200 OK with the audio quietly dropped. That preference for an honest refusal over a fake success shows up in a few places in this codebase.
Recordings are rolling MP4 segments: 900 seconds by default, a 10,240 MB capacity cap, oldest-first pruning, and only files the application wrote are ever deleted. Segment rotation happens on keyframe boundaries, so you don't get half-written files.
Everything outbound is off until you turn it on
The default posture is worth calling out because it's unusual. RTSP server, RTMP push, ONVIF device, GB28181 device — all four ship disabled. You enable what you need from the web UI, per protocol, and the state lives in SQLite so it survives a restart.
The rest of the security posture:
- TLS is mandatory on the web UI. rustls only, no HTTP fallback, not even on localhost. A self-signed dev certificate is generated on first run.
- Admin credentials are bcrypt-hashed. Session cookies last 24 hours with
HttpOnly; Secure; SameSite=Strict. - Per-IP rate limiting on auth endpoints, 20 requests per 60 seconds by default, reset on successful login, plus a per-user lockout with exponential backoff after five failures.
- Double-submit CSRF on state-changing requests, and a strict CSP header.
- The only anonymous endpoints are
/healthand/metrics. Everything else needs a session. - Two-factor auth and mutual TLS are explicitly out of scope for v1.
That last point is the real limitation: if you expose this to the internet, a strong password over TLS is the whole authentication story. It's built as a LAN device, and it should be deployed like one.
The only interface is the browser
The web UI isn't a convenience wrapper around a config file — it's the primary surface, and the CLI is the escape hatch.
Live preview picks its transport from what the device and browser support: MSE first (fragmented MP4 over chunked HTTP), falling back to MJPEG in an <img>, then snapshot polling.
flowchart TD
OPEN["Open live view"] --> P1["MSE over HTTP"]
P1 --> P2["MJPEG fallback"]
P2 --> P3["Snapshot polling"]
The MSE engine has a stall timer, exponential backoff on reconnect, active SourceBuffer pruning to stay near the live edge, and a playhead watchdog that reconnects if currentTime stops advancing for about eight seconds — because a stuck decoder keeps bytes flowing while the picture is frozen, and a byte-level stall timer never notices.
Frames for the preview and the snapshot endpoint come from a JPEG tap maintained inside the capture loop, not from a transcoder. For MJPG cameras the raw JPEG bytes are forwarded at no cost; for YUYV-only cameras a JPEG is re-encoded every Nth frame. Snapshot and preview both read that tap.
No ffmpeg, no subprocesses
Earlier versions shelled out to ffmpeg for five different jobs. All five are now in-process Rust:
| Former ffmpeg job | Replacement |
|---|---|
| H.264 encode | openh264 (Cisco, BSD-2, built from source) |
| MJPEG decode | jpeg-decoder |
| YUYV to JPEG preview | jpeg-encoder |
| Audio encode | G.711 μ-law, or optional FDK-AAC |
| MP4 segment muxing |
muxide, a pure-Rust fMP4 muxer |
Two consequences worth stating plainly: ffmpeg is no longer a runtime dependency (the container image, the systemd unit and the bare-metal install steps all dropped it), and the encoding pipeline is crash-recoverable without process supervision. The project's own changelog puts the saving at roughly 64 MB of subprocess overhead per camera. That's the project's figure, not a third-party benchmark, and I haven't reproduced it.
Small things that matter on a machine you also use
- Protocol hot-toggle. Turning ONVIF, GB28181 or RTMP on and off from the web UI doesn't restart the service.
- Hot-plug. A udev listener watches for camera add/remove events, so plugging in a webcam makes it appear, and unplugging it marks the camera offline and shuts its recording output down cleanly.
- SSE event stream. Camera online/offline events reach the browser without a refresh.
- Observability. 14+ Prometheus metrics, OpenTelemetry tracing wired through 132 annotated spans, structured JSON logs, and an optional Loki-compatible log shipper that fails open — a missing backend is a warning, not a crash.
Platform reality
Linux x86_64 and aarch64 are the only targets that compile today. Windows and macOS are planned and don't build yet. The stated blockers are concrete: libc::getifaddrs is POSIX-only, parts of the capture code have #[cfg(unix)] without a Windows branch, and device paths are hardcoded as /dev/videoN. macOS would need AVFoundation-aware device enumeration on top of that.
That matters because the obvious user for this product is someone with a spare Windows laptop, and today they can't run it.
Other things to know before you build a plan on it:
- No releases yet. No tags, no GitHub releases, the changelog is entirely under Unreleased at version 0.1.0. If you want it, you compile it.
- The README lags the code in two places. Its per-crate line-count table is stale, and the browser-preview row still describes ffmpeg transcoding, which was removed — MSE is the current primary path.
- The resource figures are targets, not measurements. Idle CPU under 5%, memory under 200 MB, first frame under 500 ms, at most 16 concurrent streams. There's no published measurement to check them against, which is different from the board-based siblings, whose READMEs do carry measured numbers with their conditions attached.
- One authentication factor. A strong password. No 2FA, no mTLS, no per-user ACLs.
- No cloud, no mobile app, no push notifications. By design, but it's a gap if that's what you wanted.
- It's brand new. First pushed on 2026-09-16, one star. I won't make claims about maintenance cadence or response times.
- The toolchain requirement is Rust 1.88 with edition 2024, which is on the new side.
The protocol layer is the same two Rust libraries
The ONVIF device service is onvif-device-rs; the GB28181 device stack, including GB 35114 A-level, is gb28181-rs. Both are MIT, both are maintained as standalone libraries, and both can be used without this application if you're building an NVR, a platform, or a device simulator. The signal-plane code is hand-written in this repo; the protocol semantics live in those two crates.
Getting started
# System dependencies (Linux)
sudo apt install libv4l-dev libasound2-dev libclang-dev
sudo usermod -aG video $USER # reclaim webcam access; requires a fresh login
git clone https://github.com/xiqing85/mibee-eye-notebook
cd mibee-eye-notebook
cargo build --release
cp config.toml config.local.toml # gitignored; keep local overrides here
cargo run --release -- --config config.local.toml
Then open https://<host>:8443. First run walks you through creating the admin account and generates the self-signed certificate, so the browser warning is expected.
Three config gotchas: [web] advertised_host left empty auto-detects the LAN IP with a UDP probe, which picks wrong on multi-NIC or NAT'd hosts and produces stream URLs clients can't reach. [capture] video_device defaults to /dev/video0, so check the device number on a multi-camera machine. And if you lose the password, --reset-password is the way back in.
Project links
mibee-eye-notebook: https://github.com/xiqing85/mibee-eye-notebook
The Raspberry Pi-class implementations in the same family:
mibee-eye-go: https://github.com/xiqing85/mibee-eye-go
mibee-eye-rs: https://github.com/xiqing85/mibee-eye-rs
The protocol libraries underneath:
onvif-rs: https://github.com/mickeyzzc/onvif-rs (published on crates.io as onvif-device-rs)
gb28181-rs: https://github.com/mickeyzzc/gb28181-rs
Licence: Apache-2.0 for the capture agent. The two protocol libraries are MIT.
Top comments (0)