ai newsSo., 18. Okt.
EilmeldungGoogle Adopts Pyrefly for Faster, More Accurate Python Type Checking

How My NVR Cuts Recording Storage by 75% — Without AI, and Without Dropped Frames

A few third-party write-ups about MiBeeNvr — review sites, aggregator posts — describe its adaptive recording as "motion-aware, saves disk" and stop there. I'm the author. This post is the full mechanism: what signal decides when to record, why the switch drops zero frames, which cameras break it,

dev.toSonntag 16:18 · 18.10.10 Min. Lesezeit
Bild: dev.to

A few third-party write-ups about MiBeeNvr — review sites, aggregator posts — describe its adaptive recording as "motion-aware, saves disk" and stop there. I'm the author. This post is the full mechanism: what signal decides when to record, why the switch drops zero frames, which cameras break it, and where AI fits (and deliberately doesn't).

If you run Frigate, Blue Iris, Surveillance Station or just a camera on a Raspberry Pi, the design trade-offs below should map onto things you already know.

95% of the time, nothing happens in front of your cameras — but continuous recording writes full-rate video anyway. MiBeeNvr's adaptive recording:

You don't need a video-codec background for this section, but it's the foundation for everything else.

A camera's H.264/H.265 stream is cut into GOPs. Each GOP starts with an I-frame (a complete picture, independently decodable), followed by P-frames that only store the difference from the previous frame.

The key property: the quieter the scene, the smaller the P-frames. An empty corridor produces P-frames of a few KB. Someone walks through, and the encoder has to describe their outline, motion and shadows — the same P-frame balloons to hundreds of KB. So the sequence of P-frame sizes is already an activity curve. You can see it without decoding a single frame.

Measured on a 2K H.265 camera pointed at a mostly-static studio (documented, not estimated):

The third bar is derived: a sparse segment of 5.6 MB / 248 s ≈ 81 MB/h, versus ~190 MB of full-rate video for the same wall-clock window — about 34× less.

If the camera's bitrate is flattened — roughly the same bytes per second whether or not anyone is there — P-frame sizes stop correlating with activity. Some smart-encoder cameras behave exactly like that. That's the blind spot of compressed-domain gating; my fallbacks are pixgate and tiered recording.

MiBeeNvr's design baseline is a 1 GB low-end ARM board (Raspberry Pi 3B), with a 512 MB process budget, a single static binary and zero runtime dependencies. Every feature has to fit that budget.

A resident inference model doesn't fit. It would either eat the memory or force you onto Coral/GPU hardware — which would break the premise of "an old device that runs today". So the core recording path deliberately contains no model. That's a trade-off, not a capability gap.

Where does AI go instead? Two places, both outside the budget line:

This is also why the storage design works at all: the cheaper the activity decision, the more often you can afford to make it — and the denser you can make "nothing is happening" writes. Three layers, cheapest first: compressed-domain signals (near-zero cost), pixgate classic CV (under 2% of one core), external Vision (someone else's hardware).

The NVR never decodes the picture. It watches each P-frame's size against a rolling baseline — the typical P-frame size of that stream recently, maintained with MAD (median absolute deviation, a statistic that resists jitter). A P-frame suddenly several times the baseline (default 5×) means "activity": switch back to full frame rate immediately.

Conversely, if the scene stays quiet for 60 seconds, drop into sparse mode: one keyframe every 30 seconds, audio paused.

In config terms, that's all there is (every value below is the default):

cameras: - id: " studio" recording_mode: " adaptive" # default "continuous" = full frame rate adaptive: calm_threshold: " 60s" # how long quiet must last before downgrading (10s–30m) timelapse_interval: " 30s" # keyframe spacing in sparse mode (5s–10m) spike_factor: 5.0 # activity sensitivity (1.5–20, lower = more sensitive) gop_buffer_bytes: 33554432 # GOP pre-buffer ceiling (1–64MB) Enter fullscreen mode Exit fullscreen mode In the web UI it's Camera → Edit → Recording mode: Adaptive (motion-aware).

The biggest risk of a "don't write everything" strategy isn't saving too little — it's the first frames of an event missing from disk. Here's the insurance.

While in sparse mode, a 32 MB ring buffer in memory always holds the most recent complete GOP. The moment an activity spike appears:

The switch point contains no gap. Playback doesn't artifact, doesn't skip.

Blue Iris has a pre-trigger buffer, Synology has 5–300 s pre-recording — same family of idea: get the seconds before the trigger back onto disk. The difference is only the source: their pre-roll comes from deciding to record in advance; mine comes from what the buffer happens to be holding. No hierarchy, just different constraints.

Empty periods are stored as compressed time. During merge, dwell samples longer than 2 s are squeezed to 0.1 s frame spacing. A 122-second sparse segment becomes 0.4 seconds in the file — playback fast-forwards through empty stretches and runs real-time through active ones, automatically, inside the same file.

The database row keeps the wall-clock axis, and the playback timeline's seek goes through a timeline_map to land at the right offset inside the product.

Synology's Smart Time Lapse and Blue Iris' Periodic mode do the same family of thing — fewer bytes for longer empty time. I folded it into the everyday recording stream instead of shipping it as a separate switch.

Compressed-domain gating has a blind spot that is principled, not a bug: rain hitting a puddle, leaves in the wind, a person walking past — all are real pixel motion, all inflate P-frames. The bitrate domain cannot tell "rain" from "person".

A second class of cameras is worse: smart encoders flatten the bitrate, so P-frame sizes decouple from activity entirely. Compressed-domain gating simply stops working on those streams.

So there's a pixel-domain layer: sample, decode, and use classic computer vision to ask "is there a coherent, persistent foreground object?" This layer isn't novel — ZoneMinder's zones, Shinobi's regions and Blue Iris' motion zones are all this idea. Here's my implementation.

With video_exit: false, video spikes never exit sparse mode — rain can pour and the camera stays in time-lapse. Only pixgate, audio, camera events or the external API restore full rate. For doorways and corridors where "no human, no full frame rate" is the rule.

For cameras whose bitrate is flattened, single-layer adaptive may never downgrade. Tiered recording walks on two legs:

Sub-layer segments live in the same directory tree with a sub_ prefix, are born terminal ( merge_status='sublayer' ), never merge, and follow ordinary retention. The recordings API takes?layer=1 /?layer=0 to toggle visibility.

Blue Iris' "sub-stream continuous + main on trigger" (direct-to-disc BVR, sub-stream upsampled on playback) and Synology's Advanced Continuous Recording (low-res continuous, high-res on event) are the same family. Their trigger comes from configured rules; mine comes from automatic compressed-domain/pixgate decisions — plus sub-layer segments can feed external AI directly (next section).

All three exist because of real user reports:

Explicit floor and self-calibrated floor take the larger value — the explicit value sets a hard minimum, the self-calibration raises it further on starved streams.

Every segment gets an activity score from the compressed-domain analyzer, queryable in the recordings library: filter by "has activity / quiet / scene change", set a minimum score, or color the timeline by activity (green = quiet → red = busy).

Two anti-distortion measures since v0.13:

When the disk hits its watermark (default 85%), cleanup deletes the lowest-activity quiet segments first — the same disk holds more "something happened" footage. Frigate's retain ladder decides by type what to keep; I order deletions by content within a type. Same idea, two directions.

The gate is a pure function of "P-frame size sequence + config", so a finished recording can answer "what would this parameter set have done?" — no camera, no decoding:

# scorer replay: activity score + confidence per segment mibee-nvr eval-replay --corpus corpus.json # gate replay: default vs candidate parameters, side by side mibee-nvr eval-replay --corpus corpus.json --gate --videoexit = false mibee-nvr eval-replay --corpus corpus.json --gate --spike 8 --noisefloor-bytes 600 Enter fullscreen mode Exit fullscreen mode A corpus is a JSON array whose label field is conventionally rain / lowbitrate / static / active, aggregated into per-label means.

The NVR watches segment-completed events, checks the Vision consumer's heartbeat, and pushes the segment in the window after it lands on disk but before merge ( push_mode: notify lets the consumer pull; upload pushes bytes).

Why push at all: early Vision polled the NVR for recordings, and 78% of those requests hit 404 — the segment had already been merged or cleaned up before Vision arrived. With push, the video is in Vision's hands before the merge process touches it.

Heartbeat timeout (default 60 s) pauses pushing; on recovery, missed segments are re-pushed with bounds — a 2-hour window, max 500 segments, 200 ms pacing — so a recovery burst can't hammer the consumer or the disk.

vision.instances supports several consumers at once, each with its own address, identity and on/off switch; cameras select instances via vision_targets (empty = all). Different instances can run different models, routed by scenario:

vision: enabled: true url: " http://192.168.1.20:9091" # single-instance shorthand heartbeat_timeout_secs: 60 push_mode: " notify" # notify = consumer pulls skip_cameras: [ " cam-mjpeg-1" ] # pushing MJPEG cameras is pointless sub_layer_cameras: [ " cam-4k-yard" ] # on-demand sub-stream analysis segments (not in the recordings library) tiered_cameras: [ " cam-smart-codec" ] # layer=1 segments for semantic gating cameras: - id: " cam-gate" vision_targets: [ " jetson-yolo", " coral-tpu" ] # multi-instance routing Enter fullscreen mode Exit fullscreen mode Feeding AI low-resolution segments: decode cost cut to 1/4–1/16 Two low-res channels (shipped in v0.12.0, issues #512–#537):

4K main-stream recording is untouched. For the consumer, judging a 480p segment is an order of magnitude cheaper than judging 4K.

Two paths, both ending in the same action — pulling the main stream back to full frame rate:

curl -u admin:password -X POST \ http://192.168.1.50:9090/api/cameras/cam-gate/adaptive/trigger \ -H "Content-Type: application/json" \ -d '{"source": "vision", "hold": "30s"}' Enter fullscreen mode Exit fullscreen mode hold is how long full frame rate is maintained; each new confirmation restarts the clock.

Lite is the first footprint on this path: one camera, CPU. It changes none of the NVR's budgets — it runs on your other machine, and the NVR pays at most some push bandwidth.

F is the load-bearing one: an NVR's storage ceiling is set first by which signal it uses to decide "record now".

In prose, because the table flattens things:

H.265+ (Hikvision), Smart H.265+ (Dahua), Ultra H.265 (Uniview), Zipstream (Axis), WiseStream (Hanwha) cut baseline bitrate with background modeling, dynamic GOP and ROI encoding. Vendor claims: 20–40% below standard H.265 at equal quality, more in favorable scenes. They complement NVR-side gating; they don't replace it.

Two traps I've written about ( in Chinese ):

# one-line install: downloads the binary, creates the nvr system user, # generates a config, installs a systemd service and starts it curl -fsSL https://raw.githubusercontent.com/Mi-Bee-Studio/MiBeeNvr/main/install.sh | sudo bash # Web UI on http://localhost:9090, data in /var/lib/mibee-nvr # or Docker (recordings on an external drive: edit the volume mount + NVR_DATA_DIR) docker compose --project-directory. -f deploy/docker/docker-compose.yml up -d Enter fullscreen mode Exit fullscreen mode Then set any camera to Recording mode: Adaptive and watch the storage trend on the dashboard. eval-replay lets you tune parameters against recordings you already have.

Mechanisms described against v0.13.0 docs and source comments; comparison data from each project's public docs and community measurements as of 2026-10; storage figures marked as measured are from my own documented runs, the rest are estimates or vendor claims. Diagrams are the SVG files in the diagrams/ folder next to this markdown — upload them to your publishing platform (dev.to: drag each into the editor) or host them on your own domain and update the paths.

Quelle: dev.to (dev.to). Original öffnen