MiBeeNvr v0.7.0: Timelapse v2 + Dual-Lens Xiaomi + H.265 Muxing + Release Hardening
After v0.6.0 put the timelapse pipeline in place, community feedback quickly pointed out several hard problems: the JPEG sequence consumed too much storage, H.265 cameras couldn’t generate playable timelapse segments, dual-lens Xiaomi devices could only capture the main lens, and H.265 HLS would occasionally panic outright. None of these are edge cases — the dual-lens CW500 and Outdoor Cam 4 ship in large volumes in China, and H.265 is the de facto standard for mid-to-high-end cameras. The v0.7.0 mainline was built around exactly this feedback.
This version rewrites the timelapse pipeline into v2: a pure-Go H.264/H.265 NAL→MP4 muxer replaces the previous FFmpeg-dependent merge path, keyframe files are self-contained with SPS/PPS, and the merged output is directly playable H.264/H.265 video — no more intermediate JPEG sequences. Dual-lens Xiaomi support lands via channel selection, so devices like the CW500 and Outdoor Cam 4 can target a specific lens; the H.265 HLS panic is thoroughly fixed by a StreamHub refactor and automatic muxer recovery. The transcoding module also gets a layered refactor in this release — hardware probing, encoder abstraction, and the job pipeline are split into independent layers, so adding new encoders or hardware backends no longer ripples through the whole pipeline. The frontend Dashboard gets a major cleanup too: the AI module is removed, camera management is simplified, and timelapse and regular recordings merge into a unified list.
Before release, all these changes were thoroughly validated against real camera environments — including the CW500 dual-lens, H.265 cameras in HLS/timelapse recording, and Docker deployments around storage paths and GPU passthrough. The three self-developed camera projects were updated in sync — one was rebranded (MiBeeHomeCam → MiBee Cam, with ONVIF auto-discovery added), one moved repos and broadened its positioning (rpi-cam → mibee-eye-raspi, now covering all libcamera-compatible boards), and one fixed a leftover bug from the last release; these are covered in a dedicated section at the end. Details on the earlier round (synced with v0.6.0) are in camera-test-machines. See the full changelog at GitHub Release Notes.
Timelapse v2 Pipeline
The core change in v2 is that the merge path no longer goes through FFmpeg. Keyframes enter an in-house muxer directly as H.264/H.265 NAL units and are packaged into self-contained MP4 files:
flowchart TB
SRC["RTSP / Xiaomi source<br/>h264 / h265"] --> KE["Keyframe Extractor<br/>SPS/PPS cached"]
KE --> KF["H.264 / H.265<br/>keyframe sequence"]
KF --> RM["Rolling Merge<br/>pure-Go muxer"]
RM --> OUT["Self-contained MP4<br/>directly playable"]
classDef source fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef rec fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef store fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
classDef out fill:#F3E5F5,stroke:#9C27B0,color:#6A1B9A
class SRC source
class KE,RM rec
class KF store
class OUT outThe v0.6.0 pipeline merged on top of JPEG sequences, which necessarily re-encoded during merge — neither CPU cost nor file size could come down. The v2 keyframe extractor caches SPS/PPS at the very first frame, so every subsequent keyframe’s NAL unit is persisted together with this parameter set. At merge time, NAL units only need to be packaged into the MP4 container in order — pure mux, zero encoding.
Dual-Mode Keyframe Extraction
The v2 extractor supports two operating modes, configured per camera:
| |
The interval mode suits MJPEG sources or high-frequency RTSP streams that need a lower frame rate; the IDR mode suits H.264/H.265 streams — the camera’s own GOP determines the keyframe interval, and reusing IDR frames skips decoding entirely. The extractor only does NAL unit filtering and SPS/PPS caching, at near-zero overhead.
Merge Durations for Rolling Merge
Merge is no longer a fixed “once per day”. It offers 8h / 12h / 24h / natural-day / 7d / 30d options. The rolling merge manager splits the time window by the merge duration and triggers a merge task when a window closes; users can also trigger a merge manually from the UI via the retry-merge API.
Timelapse recordings now appear in the unified recordings list alongside regular recordings — the frontend no longer needs a separate timelapse entry point. Each camera’s timelapse config (interval, frame source, merge mode) can be set independently in the camera config editor.
Pure-Go Muxer vs FFmpeg Merge
| Dimension | v2 Pure-Go muxer | v0.6 FFmpeg merge |
|---|---|---|
| Dependency | None | Requires FFmpeg |
| Merge method | NAL units packaged directly into MP4 | JPEG re-encoded to H.264 |
| Encoding cost | Zero (mux only) | High (encoding) |
| Input frame source | H.264 / H.265 keyframes | JPEG sequence |
| Use case | Long-term archive, live preview | Deprecated |
The v2 muxer supports both H.264 and H.265 — a key improvement for H.265 cameras, since the v0.6 JPEG path couldn’t produce H.265 timelapse segments at all.
Transcoding Architecture Refactor
The v0.6.0 transcoding pipeline was hastily assembled in three waves (Wave 1/2/3) — queue, polling, and backfill were each independent, and hardware probing and encoder selection were still tangled in the task-execution path. v0.7.0 does a layered refactor of the whole transcoding module, with the goal of making it possible to add new encoders and hardware backends without touching the job pipeline.
flowchart TB
Q["Job queue<br/>SQLite-persisted"] --> WP["Worker pool<br/>concurrent consumption"]
WP --> PR["Hardware probe layer<br/>V4L2M2M / VAAPI / NVENC"]
PR -->|"device available"| HW["Hardware encoding<br/>vaapi / h264_v4l2m2m / h264_nvenc"]
PR -->|"device unavailable"| SW["Software fallback<br/>libx264 / libx265"]
HW --> EX["Encoder abstraction layer<br/>unified command building"]
SW --> EX
EX --> FF["FFmpeg subprocess<br/>progress callback + timeout control"]
classDef q fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef w fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef p fill:#F3E5F5,stroke:#9C27B0,color:#6A1B9A
classDef hw fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
classDef ex fill:#FFFDE7,stroke:#F9A825,color:#BF6F00
classDef ff fill:#FFEBEE,stroke:#C62828,color:#C62828
class Q q
class WP w
class PR p
class HW,SW hw
class EX ex
class FF ffLayered Refactor
The refactor splits transcoding into three layers, each with a clear responsibility:
- Job pipeline layer: a SQLite-persisted job queue + a Worker pool. The Worker pool consumes jobs at the configured concurrency, and each job’s lifecycle (pending → running → done/failed) is managed by the queue, fully decoupled from the concrete encoder.
- Hardware probe layer: runs once at transcoding-engine initialization, with the result cached. The VAAPI probe checks whether
/dev/dri/renderD*exists; the V4L2M2M probe enumerates encoder nodes under/dev/video*; the NVENC probe walks FFmpeg’s-encodersoutput. Hardware backends that don’t probe successfully are simply disabled, with no re-probing at runtime. - Encoder abstraction layer: collapses hardware and software encoders behind a single command-construction interface. Adding a new encoder only means implementing that interface — the job pipeline and Worker pool don’t change at all.
Before the layering, encoder selection and hardware probing were scattered across multiple branches in the task execution path, so adding one encoder meant touching four or five places. After the refactor, adding libx265 software encoding or a new hardware backend only touches the encoder abstraction layer.
Capability Strengthening
On top of the architecture, this release fills in a few previously missing capabilities:
- libx265 software encoding: v0.6.0’s software fallback was libx264 only, so H.265 cameras without a hardware encoder could only be transcoded down to H.264. With libx265 in place, the pure-software H.265→H.265 transcoding path is open — ARM boards (Raspberry Pi, Banana Pi) can keep the codec unchanged even without a hardware encoder.
- Shared probing/encoders between transcoding and timelapse: the v2 timelapse pipeline’s H.265 muxer reuses the transcoding module’s SPS/PPS parsing and hardware-probe results. When a timelapse recording needs re-encoding (e.g., JPEG sequence → H.264 for long-term archive), it goes straight through the transcoding encoder abstraction layer instead of maintaining its own FFmpeg invocation.
- Software encoding for MJPEG input: previously, MJPEG input sources had software encoding incorrectly disabled on ARM; the fix lets MJPEG input also go through libx264/libx265.
Reliability
The job pipeline’s reliability is also strengthened: failed-job retries no longer lose context (the Worker restores queue state from SQLite after a restart), and FFmpeg subprocess timeouts and progress callbacks are monitored independently, so a single stuck transcoding job can’t drag down the whole Worker pool. Combined with the VAAPI device verification and FFmpeg status test isolation mentioned earlier, transcoding stability on Docker, VMs, and ARM boards improves noticeably.
Dual-Lens Xiaomi Support
Dual-lens Xiaomi devices (CW500, Outdoor Cam 4, etc.) share one device ID across two lenses, and upstream projects like go2rtc run into channel limits when handling them. v0.7.0 adds channel selection to the Xiaomi cloud integration, so users can specify the lens when adding a camera.
sequenceDiagram
participant U as User
participant NVR as MiBeeNvr
participant XC as Xiaomi Cloud
participant CAM as Dual-Lens Device
U->>NVR: Add camera + select lens
NVR->>XC: Resolve device stream URL (did + channel)
XC-->>NVR: Return MISS URL for the selected channel
NVR->>CAM: miss connect (specified channel)
CAM-->>NVR: RTP stream for that lens
Note over NVR: Separate recorder instance<br/>independent recording & transcodingChannel selection is applied at the MISS URL resolution stage — Xiaomi Cloud returns a device-level address, and MiBeeNvr carries the channel parameter when it builds the recorder, so the stream it gets is for the specified lens. Each lens maps to an independent recorder instance, with recording, transcoding, and the timelapse pipeline each independent — identical to a single-lens camera at the data-flow level.
There’s also a fix to RTSP URL credential handling in test-connection: previously, passwords with special characters would mangle the URL, so the connection test kept failing even though actual recording worked fine — that “test fails but runs” state is exactly what trips people up.
H.265 HLS Muxing Improvements
The H.265 HLS panic reported in #20 was a priority fix for this release. The crash happened when the muxer wasn’t initialized yet and the HLS write path landed on an uninitialized muxer pointer:
flowchart LR
IN["RTP frame"] --> SH{StreamHub<br/>routing}
SH -->|initialized| M1["Muxer writes normally"]
SH -->|not initialized| RC["Auto recovery<br/>re-init the Muxer"]
RC --> M2["Continue writing after recovery"]
classDef in fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef hub fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
classDef rec fill:#FFEBEE,stroke:#C62828,color:#C62828
class IN in
class SH hub
class M1,M2 ok
class RC recThe root cause was that StreamHub didn’t manage the muxer’s lifecycle uniformly across multiple dispatch paths — when the first frame of an H.265 stream arrived, the muxer might not be ready yet, but the write path had no protection for that. The refactored StreamHub centrally manages muxer state: before writing, it checks whether the muxer is ready; if not, it triggers the auto-recovery flow, re-initializes the muxer, and then continues writing, instead of panicking outright.
The Xiaomi camera reconnect loop reported in #18 is the same class of problem surfacing at the connection layer. The UDP miss connect for Xiaomi CS2 devices times out under network jitter, the previous backoff strategy was a fixed 1 minute, and the PONG response was mishandled — once a PONG was lost, the connection state machine would get stuck, making the device look offline and repeatedly triggering reconnects. The fix covers PONG response robustness, randomized jitter in backoff timing, and actively dropping residual CS2 buffer data on reconnect, so stale data doesn’t contaminate the new connection.
Release Hardening
This version did a fair amount of hardening work on stability, CI, and packaging, largely driven by the Docker deployment feedback in #15. The transcoding module’s VAAPI device verification and FFmpeg status test isolation are covered in the Transcoding Architecture Refactor section above; here are the rest.
WebSocket decoder backpressure: when playing multiple streams concurrently, the WebSocket decoder from Worker to ConnectionManager had no backpressure control, so under backpressure it would drop frames or stall outright. The fix routes the backpressure signal all the way from Worker to ConnectionManager, noticeably improving multi-stream playback stability.
Docker storage path: #15 reported containers restarting with mkdir /var/lib/mibee-nvr: permission denied — the root cause was a mismatch between the default storage path in the entrypoint and the actual data volume path. The fix aligns the paths, so restarts no longer lose permissions.
Dashboard UX Overhaul
Between v0.6.0 and v0.7.0, the frontend Dashboard got a fairly significant cleanup. Two things drove it: first, the recordings produced by the v2 timelapse pipeline needed to show up alongside regular recordings, and the old split between a “Recordings” entry and a separate “Timelapse” entry no longer made sense; second, the browser-side AI detection module had never been finished, and leaving it on the UI was actively misleading users.
flowchart LR
subgraph v0.6 ["v0.6.0"]
A1["Camera manager<br/>with Enabled toggle"]
A2["Recordings list"]
A3["Timelapse entry (separate)"]
A4["AI detection (half-done)"]
end
subgraph v0.7 ["v0.7.0"]
B1["Camera manager<br/>simplified, Enabled removed"]
B2["Unified recordings list<br/>regular + timelapse"]
B3["Transcoding history page"]
B4["AI module removed"]
end
A1 --> B1
A2 --> B2
A3 --> B2
A4 --> B4
classDef old fill:#FFEBEE,stroke:#C62828,color:#C62828
classDef new fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
class A1,A2,A3,A4 old
class B1,B2,B3,B4 new- AI module removed: the browser-side object detection only did recognition, not tracking — limited practical use and a resource drain. v0.7.0 removes it from the frontend entirely. The backend’s
internal/ai/ONNX Runtime inference framework is kept as the foundation for a future standalone feature (real-time object detection), but the half-finished entry is no longer exposed in the Dashboard. - Camera manager refactor: the
Enabledfield is removed. Previously every camera had an enable/disable toggle, but that state was tangled with the recording, streaming, and transcoding state machines — a disabled camera would occasionally still trigger recording via the event bus, behaving inconsistently. After the refactor, a camera’s start/stop uniformly goes through the recorder lifecycle, and that trap-prone toggle is gone from the config. - Unified recordings list: timelapse and regular recordings are merged into a single list, filtered by type on the frontend. Timelapse segments used to need a separate page; once the v2 pipeline produces self-contained H.264/H.265 MP4s, there’s no playback-experience difference between a timelapse segment and a regular recording, so there’s no reason to keep two entry points.
- Transcoding history page: transcoding job status (pending/running/done/failed), the retry button, and paginated history cleanup are collected into a dedicated page, instead of being scattered across recording details.
Community Feedback & Closed Issues
This release closes 4 community issues, all of which surfaced in real-world use:
- #14 Multi-lens device support & video quality — The Xiaomi CW500 dual-lens and Outdoor Cam 4 dual-lens could only capture the main lens, and recording quality was blurry. v0.7.0 brings in the second lens via channel selection, and the transcoding pipeline supports H.264/H.265 interconversion so quality can be tuned via transcoding parameters.
- #15 v5.0 Docker test report — Docker deployment storage-path permissions, the settings page save entry, H.265 playback compatibility across HLS/HTTP-FLV/LL-HLS, and the live-view timestamp drift. Each was addressed: the Docker path was aligned, the settings entry simplified, and the HLS timestamp playback loop was traced back to the muxer.
- #18 Xiaomi camera reconnect loop — Xiaomi CS2 devices’ UDP miss connect frequently timed out, reconnect backoff was a fixed 1 minute, and PONG response mishandling stalled the state machine. Fixed PONG robustness, added jitter to backoff, and clean up the CS2 buffer on reconnect.
- #20 H.265 error — H.265 HLS panicked outright when the muxer wasn’t initialized. The StreamHub refactor centrally manages muxer lifecycle, checks ready state before writing, and triggers auto-recovery when not ready.
Bug Fixes
- Transcoding VAAPI device verification — The hardware probe checks
/dev/dri/renderD*before selecting the VAAPI encoder, falling back to libx264/libx265 when the GPU is unavailable. - FFmpeg status test isolation — Tests no longer fail when the host has system FFmpeg installed; CI unblocked.
- WebSocket decoder backpressure — The backpressure signal is wired from Worker to ConnectionManager for stable multi-stream playback.
- Rolling merge works for all recorder types — H.264, H.265, and MJPEG keyframes all merge correctly.
- SPS/PPS cached in keyframe extractor — Keyframe files are self-contained, with no external parameter-set dependency.
- Camera H.265 encoding detection — The runtime recorder type drives the keyframe extractor, rather than a static config check.
- Recording frames listing API — The listing includes H.264/H.265 timelapse frames, so the frontend renders them correctly.
- i18n missing translation keys — Filled in strings for timelapse protocol and transcoding history actions.
Upgrade
Config is backward compatible — just swap the binary:
| |
After upgrading, check the new config options in config.example.yaml (timelapse v2 merge durations, dual-lens channel selection, transcoding hardware probing, etc.) and add them to your config as needed. Dual-lens Xiaomi users need to re-select the lens in the camera config.
Camera Projects Updated in Sync
As with v0.6.0, v0.7.0’s testing also relied on three self-developed camera projects to provide a real environment. This round, they themselves had a fair amount of change — one was rebranded, one moved repos and broadened its positioning, and one fixed a leftover bug from the last release.
MiBeeHomeCam → MiBee Cam (v0.3.0)
The firmware based on the Seeed Xiao ESP32-S3 Sense got a product rename: MiBeeHomeCam → MiBee Cam, with all references in firmware, docs, and configs updated in sync. The rename aligns the naming across the MiBee family — the old HomeCam suffix was easily conflated with generic home cameras.
The focus of v0.3.0 is ONVIF integration, so this MCU camera can be auto-discovered by mainstream NVRs:
- ONVIF WS-Discovery auto-discovery: the firmware implements the WS-Discovery UDP multicast response, so ONVIF-capable NVRs like Synology Surveillance Station and Milestone XProtect no longer need manual IP entry — the device just shows up on the LAN.
- ONVIF SOAP services: Probe, DeviceInfo, GetProfiles, StreamURI, etc., returning an HTTP MJPEG URI.
- Independent MJPEG port: the MJPEG stream moves off HTTP port 80 onto a dedicated TCP port 81, served by a separate task — previously MJPEG streaming and the Web UI shared one HTTP server, and a busy stream task would freeze the config page.
- Frame Broadcaster: definitively solves the frame-buffer (FB) contention that kept coming up during v0.6.0 testing — the camera FB is copied to PSRAM and returned immediately, with recording and streaming each holding their own copy, never blocking each other. This is the production landing of the PSRAM double-buffering approach detailed in camera-test-machines.
Also added: a Prometheus /metrics endpoint (15+ metrics: heap, PSRAM, temperature, recording stats), graceful degradation when camera init fails (boots without a camera, Web UI shows offline), and a minimum timelapse interval lowered to 1 second. All RTSP-related code is removed — ONVIF + HTTP MJPEG covers the scenarios RTSP used to.
rpi-cam → mibee-eye-raspi (repo move + positioning expansion)
The Go ONVIF camera service for Raspberry Pi, rpi-cam, had a fairly significant adjustment: the repo moved from raspberrypi-camera to mibee-eye-raspi, and the positioning expanded from “Raspberry Pi camera” to “all boards that support the Raspberry Pi camera module.”
flowchart LR
REPO["mibee-eye-raspi<br/>Go ONVIF camera service"] --> ARM["ARM64 boards"]
ARM --> RPI["Raspberry Pi 3B/4/5"]
ARM --> BP["Banana Pi BPI-M5<br/>(RK3588)"]
ARM --> OP["Orange Pi 5<br/>(RK3588)"]
ARM --> OTH["Other libcamera-<br/>compatible boards"]
classDef repo fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef arm fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef board fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
class REPO repo
class ARM arm
class RPI,BP,OP,OTH boardThe key point is that these boards all run the libcamera stack — rpi-cam’s digital PTZ (based on libcamera’s ScalerCrop), imaging controls, and H.264 streaming were built on libcamera in the first place, so behavior on libcamera-capable ARM64 boards like the Banana Pi M5 (RK3588) and Orange Pi 5 is identical. RK3588-class boards can even use hardware H.265 encoding, with notably more performance headroom than a Raspberry Pi 3B.
The rename and repo move make this fact visible — it’s no longer “Raspberry Pi only,” but an ONVIF reference implementation usable on “any ARM board that can run libcamera.” Functionally it’s unchanged from v0.2.0 (HLS live streaming, Web Admin UI, SPS/PPS-cached snapshots); the change is mostly about positioning and distribution.
MiBeeCam (v0.2.2)
The compact camera based on the Luatos ESP32-S3 A10 module, this release mainly fixes the debugging-leftover bug called out in camera-test-machines — the hardcoded test WiFi credentials in wifi_start_sta(), where the parameters were suppressed by (void) and ignored entirely. v0.2.2 removes the hardcoded credentials so the function parameters work normally. ESP-IDF version references are updated to v5.5.4, and WiFi STA troubleshooting docs are added.