<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ZhengZhiCong</title>
    <description>The latest articles on DEV Community by ZhengZhiCong (@mickey_zzc).</description>
    <link>https://dev.to/mickey_zzc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3972064%2Fd8a4b5b3-f282-4858-b6f6-9f246118a9b0.png</url>
      <title>DEV Community: ZhengZhiCong</title>
      <link>https://dev.to/mickey_zzc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mickey_zzc"/>
    <language>en</language>
    <item>
      <title>MiBee Steward v0.7.3: schema break, a Rust agent, and four bugs only the field could find</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:28:44 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/mibee-steward-v073-schema-break-a-rust-agent-and-four-bugs-only-the-field-could-find-185p</link>
      <guid>https://dev.to/mickey_zzc/mibee-steward-v073-schema-break-a-rust-agent-and-four-bugs-only-the-field-could-find-185p</guid>
      <description>&lt;h2&gt;
  
  
  The agent went quiet for 19 minutes and systemd stayed green
&lt;/h2&gt;

&lt;p&gt;On 2026-10-12 I rebuilt the database of my own main center. Nineteen minutes later I noticed one of my agents had stopped reporting. systemd was green the whole time, the timer was firing, the journal looked clean. The batches were being dropped on the floor.&lt;/p&gt;

&lt;p&gt;I will come back to why (#554). I lead with it because this release is mostly about failures you only meet on real hardware: a config block that silently failed to write, an eBPF observer that reported success while observing nothing, a router storing its LAN address in CIDR form.&lt;/p&gt;

&lt;p&gt;MiBee Steward is what I run to answer "what is actually on this network". Single binary, Go with Chi, SQLite through modernc so there is no CGO, SvelteKit UI embedded with &lt;code&gt;go:embed&lt;/code&gt;. Point it at a subnet and it comes back with vendor, device type, ports, certificates, and the neighbour relationships it can prove.&lt;/p&gt;

&lt;p&gt;License up front, since it decides whether you can use this at all: AGPL-3.0 with a commercial dual license. Fork it into a closed product or sell it as a hosted service and you need the commercial one. The fingerprint corpus under &lt;code&gt;configs/fingerprints/&lt;/code&gt; is separate, CC-BY-SA 4.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  v0.7 refuses your old database, and that is deliberate
&lt;/h2&gt;

&lt;p&gt;Databases from v0.6 and older are refused at startup. The binary prints what to do and exits. Databases from v0.7.0 through v0.7.2 are refused as well: v0.7.3 spans three schema generations, v4 through v7.&lt;/p&gt;

&lt;p&gt;The reason is that I deleted the startup migration chain. Every boot used to walk a list of incremental changes, so a database could sit at any historical point and one binary had to cope with a dozen table shapes. The parallel-DDL bugs grew out of that (#328, #431, #437): two code paths each applying half of a change, neither finishing.&lt;/p&gt;

&lt;p&gt;Now &lt;code&gt;db/schema.sql&lt;/code&gt; is the single source of DDL, applied once at creation. If the schema version in the file does not match the binary, startup is refused, and no partial state is left to reason about.&lt;/p&gt;

&lt;p&gt;The upgrade path is three steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/backup.sh
&lt;span class="c"&gt;# then start the new binary against an empty data directory&lt;/span&gt;
./mibee-steward &lt;span class="nt"&gt;-config&lt;/span&gt; configs/config.yaml
&lt;span class="c"&gt;# let it rescan; the asset table repopulates itself&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's local mini database follows the same policy, so scan history and SNMPv3 credentials do not carry over. The Go agent's &lt;code&gt;agent.db&lt;/code&gt; is not migrated either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the agent is now Rust
&lt;/h2&gt;

&lt;p&gt;The distributed agent is Rust only now (&lt;code&gt;agent-rs/&lt;/code&gt;, artifacts named &lt;code&gt;mibee-agent-*&lt;/code&gt;). &lt;code&gt;agent.yaml&lt;/code&gt; is key compatible, so nobody has to edit config.&lt;/p&gt;

&lt;p&gt;The argument is old hardware. Anyone with a homelab has a drawer of boards that are still useful and short on RAM, and a collection daemon should not be why one gets retired. My test host was a NanoPi NEO: ARMv7, 512 MB of RAM, systemd, 6.18 kernel, watching the &lt;code&gt;lan-63&lt;/code&gt; segment of my center. Two days of sampling, 96 rounds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;th&gt;Rust&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binary size&lt;/td&gt;
&lt;td&gt;~19 MB&lt;/td&gt;
&lt;td&gt;~4.8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median RSS&lt;/td&gt;
&lt;td&gt;54 MB&lt;/td&gt;
&lt;td&gt;12 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSS band&lt;/td&gt;
&lt;td&gt;38 to 63 MB&lt;/td&gt;
&lt;td&gt;11 to 12 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Go figure already includes a round of lazy corpus compilation I did earlier; before that it sat around 110 MB steady state on that board. On a 512 MB machine, half your RAM is not something I get to spend, and a band that swings from 38 to 63 MB makes capacity planning guesswork.&lt;/p&gt;

&lt;p&gt;The Rust side sits at 12 MB median and stayed between 11 and 12 MB for the whole two days, roughly a quarter of the Go number. That stability matters more than the median when the box also runs dnsmasq and a couple of containers. A &lt;code&gt;/24&lt;/code&gt; sweep on the same hardware is about 1.7x the Go throughput. Artifacts are static musl builds for amd64, arm64 and armv7, plus OpenWrt packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-homed devices stopped ping-ponging
&lt;/h2&gt;

&lt;p&gt;A router with two NICs gets reported by agents on both sides. The old identity logic treated every report as a possible move, so the device row migrated between segments on the scan cadence of whichever agent spoke last. In the UI it looked like a device that could not sit still.&lt;/p&gt;

&lt;p&gt;Two new tables fix it. &lt;code&gt;device_network_presence&lt;/code&gt; holds one row per (device, segment), recording which address answered there, refreshed on every report. &lt;code&gt;device_sightings&lt;/code&gt; is append-only address history keyed by (device, IP, MAC, vantage, source); a repeat of the same tuple refreshes &lt;code&gt;last_seen&lt;/code&gt; and &lt;code&gt;see_count&lt;/code&gt;, a change appends a row.&lt;/p&gt;

&lt;p&gt;The rule now: an out-of-segment report does not move the device row while a presence record from the last 24 hours is fresh, and a genuine move happens only when every other presence has expired. Every bridge apply writes a sighting with its vantage, empty for the center and otherwise the agent id.&lt;/p&gt;

&lt;h2&gt;
  
  
  The topology graph finally has a root
&lt;/h2&gt;

&lt;p&gt;The root bridge used to be guessed by heuristics. The awkward case was the root bridge itself: standing on that device, every designated bridge around it is itself, no edge survives deduplication, and the probe returned nothing for the one device you most wanted to see.&lt;/p&gt;

&lt;p&gt;The probe now reads &lt;code&gt;dot1dStpPortRole&lt;/code&gt; (root, designated, alternate, backup, disabled) and &lt;code&gt;dot1dStpDesignatedRoot&lt;/code&gt; in the same scalar Get, landing in &lt;code&gt;scan_attributes.stp&lt;/code&gt;: own bridge MAC, root bridge MAC, &lt;code&gt;is_root&lt;/code&gt;, and port roles by interface name. &lt;code&gt;detectRoot&lt;/code&gt; consumes that as pass 0, ahead of every heuristic.&lt;/p&gt;

&lt;p&gt;LLDP and CDP had a quieter failure. The center's own device row never had a MAC address, since ARP against a local address takes the kernel loopback shortcut and never puts a frame on the wire, so &lt;code&gt;RecordNeighborsByMAC&lt;/code&gt; could not resolve the far end and frame edges were dropped without a log line. Two fixes: the center fills its own interface MACs into its own device row at wiring time and after every scan (&lt;code&gt;AnchorLocalInterfaces&lt;/code&gt;, fills blanks only, self healing), and unknown LLDP/CDP chassis or NDP MACs get anchored with a &lt;code&gt;#anchor:&amp;lt;mac&amp;gt;&lt;/code&gt; placeholder IP, merging by MAC priority once a real address shows up.&lt;/p&gt;

&lt;p&gt;The interface inventory walks IF-MIB and ipAddrTable into &lt;code&gt;device_interfaces&lt;/code&gt;: ifIndex, name, description, MAC, speed, the IPv4 answering on that port. Vanished ports keep their last sighting row frozen. Routing table edges materialise every non directly connected route whose next hop resolves to a known device, with the destination subnet in metadata; where routes share a hop, the more specific subnet wins. That edge table had no reader before this release, so rows were written and nothing rendered them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Stewards, read-only federation
&lt;/h2&gt;

&lt;p&gt;v0.7.3 added federation between two centers (#379). Each side fully administers its own segments, its own UI, database and scan cadence, while holding a near real time read-only profile of the peer's devices.&lt;/p&gt;

&lt;p&gt;I held one constraint: zero new wire protocol. The peer is an embedded reporter over the same channel the Rust agent uses, so ingestion, anti-entropy state hashes, lease liveness and change events all apply unchanged. What the peer never gets: probe targets, documents, TLS chains, config backups, credentials, raw evidence, and the command channel. No scan, probe or restart can be triggered from the other side.&lt;/p&gt;

&lt;p&gt;Read-only is a decision I made on purpose. Once a remote operator can issue commands in your segment, every bug on either side becomes a shared incident. Devices from agent or peer segments are mirrors, so roughly 15 change routes (edit, delete, bulk, systems, documents, heartbeat config) return 403, local administrators included. The UI hides edit and delete, showing a "remote network" badge instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;peer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;center_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://steward.example.internal&lt;/span&gt;
  &lt;span class="na"&gt;auth_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;token minted on the other side&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;agent_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;peer-basement&lt;/span&gt;        &lt;span class="c1"&gt;# defaults to peer-&amp;lt;hostname&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;cidr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.63.0/24&lt;/span&gt;          &lt;span class="c1"&gt;# defaults to network.cidr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Freshness got its own treatment (#556) because of the 19 minutes above. Data from an agent or peer segment older than 5 minutes turns "Remote" into an amber "Remote · stale" with the last report time and dims the row. Devices seen in several segments get a blue ring and a label (#557).&lt;/p&gt;

&lt;h2&gt;
  
  
  Five new passive ears
&lt;/h2&gt;

&lt;p&gt;The eBPF observer moved down into the agent (#498, default-off &lt;code&gt;ebpf&lt;/code&gt; feature). The kernel side program is the same bpf2go object file the center uses, pinned byte-identical by a drift test; the loader is aya, pure Rust, so no libbpf or BCC gets dragged in. Kernel floor is 5.8, aya takes TCX on 6.6 and above and clsact/netlink below, and it wants root or CAP_BPF plus CAP_NET_ADMIN with BTF optional. Where it cannot load, one warn line is logged and active probing continues.&lt;/p&gt;

&lt;p&gt;ARP and IPv6 ND wire sightings (#497) catch the sleeping IoT class, devices that answer ARP and never send an IP packet, a third capture path alongside active scanning and DHCP leases. Throttling lives inside the BPF program, an LRU map with roughly one event per sender per 30 seconds: 1000 gratuitous ARP frames on the rig produced 1 event and 0 ring buffer drops. DAD probes, sender 0.0.0.0, are filtered in BPF.&lt;/p&gt;

&lt;p&gt;Flow telemetry adds speaks-with edges (#500). Every IPv4 TCP/UDP packet lands in an LRU flow table keyed by (src, dst, proto, dport), about 16k entries by default, drained every 30 seconds. The privacy line is the part I would point at first: flows are folded into device level aggregates only, so per-port flows never leave memory and nothing that lets you reconstruct which service two hosts used is persisted. Sessions between managed devices become faint dashed edges with a byte total in the tooltip, and the first observation of a managed pair emits a &lt;code&gt;speaks_with_new&lt;/code&gt; change event you can feed into lateral movement alerting. Retention defaults to 30 days.&lt;/p&gt;

&lt;p&gt;WS-Discovery probing (#507) is the active channel for cameras: two Probe datagrams to 239.255.255.250:3702, a bounded 2 second window, drawing ProbeMatches out of ONVIF cameras and encoders. Responses are filtered strictly by source IP, since one multicast Probe gets answered by every WS-Discovery device on the segment and skipping that flattens the identity of a whole /24 into one blob. Parsed ONVIF identity is percent-decoded into corpus keys, so &lt;code&gt;Front%20Door&lt;/code&gt; becomes &lt;code&gt;Front Door&lt;/code&gt;. Two ONVIF cameras on the rig answered and landed at 0.98 fusion confidence, inferred type camera.&lt;/p&gt;

&lt;p&gt;SNMP trap receiver (#509) lets devices speak first: linkDown, linkUp, coldStart and authenticationFailure become &lt;code&gt;snmp_trap&lt;/code&gt; passive evidence, and coldStart plus linkUp also feed the discovery funnel. It is off until you turn it on. Port &lt;code&gt;:162&lt;/code&gt; needs CAP_NET_BIND_SERVICE, &lt;code&gt;:1162&lt;/code&gt; does not. One row per source per trap per 10 seconds stops a flapping port flooding you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four bugs only the field could find
&lt;/h2&gt;

&lt;p&gt;My own migration is where #554 came from. The reporter treated every non-429 4xx as terminal, so a 401 marked the batch delivered and dropped it, and after the token came back the agent never retried. Now 401 goes back on the queue with a 5 minute cooldown, empty heartbeats are dropped, and the other 4xx codes (400, 403, 409) stay terminal. A side effect I did not plan: agents ride out a center database rebuild without anyone restarting them.&lt;/p&gt;

&lt;p&gt;On iStoreOS 24.10.8 the LuCI passive discovery toggle reported "updated" while the config file did not change, and the service restarted for nothing (#515). Missing &lt;code&gt;conntrack&lt;/code&gt;, &lt;code&gt;hostapd&lt;/code&gt; and &lt;code&gt;dns_log&lt;/code&gt; blocks failed silently. They are written out if absent now.&lt;/p&gt;

&lt;p&gt;The eBPF observer had never actually worked (#492 to #494). Three independent breakages stacked up: a removed &lt;code&gt;link.AttachTC&lt;/code&gt;, a &lt;code&gt;go generate&lt;/code&gt; that produced nothing quietly, and a &lt;code&gt;doff&lt;/code&gt; read the verifier rejected. It ran, it logged success, it saw nothing. TCX attachment and a CO-RE-free build that needs only clang fixed it.&lt;/p&gt;

&lt;p&gt;One iStoreOS 24.10 box stores its LAN address as &lt;code&gt;192.168.60.1/24&lt;/code&gt;, and I concatenated that straight into a URL, producing &lt;code&gt;http://192.168.60.1/24:8080&lt;/code&gt; in the "open admin UI" link (v0.7.2). Prefix and suffix are stripped before output now. The install script had the same bug and was silently producing an empty &lt;code&gt;network.cidr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And one security fix found in review (#534): gosnmp's TrapListener does not check the community of received messages, so any host able to send UDP to the bound address could land 0.9 confidence evidence and forge coldStart or linkUp sightings. Comparison is constant time and runs before throttling, so a forged packet can no longer eat the real one's 10 second window. There is a &lt;code&gt;mibee_snmp_trap_community_drops&lt;/code&gt; metric, and gosnmp's logger goes through a redacting adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still missing
&lt;/h2&gt;

&lt;p&gt;Upgrading hurts exactly once: back up, fresh database, rescan. The Rust agent has no router &lt;code&gt;.ipk&lt;/code&gt; or &lt;code&gt;.apk&lt;/code&gt; packaging yet, so on OpenWrt the documented path is a tarball plus a procd init script. SNMP trap stops at v1 and v2c; v3 trap has no config key.&lt;/p&gt;

&lt;p&gt;The center is a single instance: SQLite has one writer, no clustering, no HA. Crossing segments means center plus agents, or federation. Federation stays read-only, which I count as a feature.&lt;/p&gt;

&lt;p&gt;Containers are unprivileged by default, so LLDP/CDP raw frames and eBPF need either your own privileged build or the &lt;code&gt;-full&lt;/code&gt; binary with CAP_NET_RAW.&lt;/p&gt;

&lt;p&gt;Deliberately out of scope: alerting rules belong to Alertmanager, visualisation to Grafana, host depth to node_exporter, service layer discovery to Consul, config management to Ansible.&lt;/p&gt;

&lt;p&gt;There are no named outside contributors on this repository yet, so I am not going to promise a response time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeSteward" rel="noopener noreferrer"&gt;https://github.com/Mi-Bee-Studio/MiBeeSteward&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Project page: &lt;a href="https://www.mickeyzzc.tech/projects/mibeesteward" rel="noopener noreferrer"&gt;https://www.mickeyzzc.tech/projects/mibeesteward&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeSteward/tree/main/docs" rel="noopener noreferrer"&gt;https://github.com/Mi-Bee-Studio/MiBeeSteward/tree/main/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;License: AGPL-3.0 with a commercial dual license available, required for closed derivatives or hosted use; the fingerprint corpus is CC-BY-SA 4.0.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>homelab</category>
      <category>selfhosted</category>
      <category>rust</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Mon, 12 Oct 2026 11:56:12 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/-3912</link>
      <guid>https://dev.to/mickey_zzc/-3912</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mickey_zzc/your-ip-camera-says-it-supports-motion-events-it-might-be-lying-o8l" class="crayons-story__hidden-navigation-link"&gt;Your IP camera says it supports motion events. It might be lying&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mickey_zzc" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3972064%2Fd8a4b5b3-f282-4858-b6f6-9f246118a9b0.png" alt="mickey_zzc profile" class="crayons-avatar__image" width="512" height="512"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mickey_zzc" class="crayons-story__secondary fw-medium m:hidden"&gt;
              ZhengZhiCong
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                ZhengZhiCong
                
                
              
              &lt;div id="story-author-preview-content-4838053" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mickey_zzc" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3972064%2Fd8a4b5b3-f282-4858-b6f6-9f246118a9b0.png" class="crayons-avatar__image" alt="" width="512" height="512"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;ZhengZhiCong&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mickey_zzc/your-ip-camera-says-it-supports-motion-events-it-might-be-lying-o8l" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 12&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mickey_zzc/your-ip-camera-says-it-supports-motion-events-it-might-be-lying-o8l" id="article-link-4838053"&gt;
          Your IP camera says it supports motion events. It might be lying
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/go"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;go&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/onvif"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;onvif&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/iot"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;iot&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/mickey_zzc/your-ip-camera-says-it-supports-motion-events-it-might-be-lying-o8l#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>hardware</category>
      <category>iot</category>
      <category>security</category>
    </item>
    <item>
      <title>Your IP camera says it supports motion events. It might be lying</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Mon, 12 Oct 2026 11:43:56 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/your-ip-camera-says-it-supports-motion-events-it-might-be-lying-o8l</link>
      <guid>https://dev.to/mickey_zzc/your-ip-camera-says-it-supports-motion-events-it-might-be-lying-o8l</guid>
      <description>&lt;p&gt;There's a class of bug that only exists because of what hardware you happen to own.&lt;/p&gt;

&lt;p&gt;This one was reported by a user, &lt;a href="https://github.com/genelee26" rel="noopener noreferrer"&gt;genelee26&lt;/a&gt;, against &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr" rel="noopener noreferrer"&gt;MiBeeNvr&lt;/a&gt; — an NVR I maintain. His report, in short:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My TP-Link camera detects motion just fine. The ONVIF event panel shows the events. But recording never triggers, and nothing is ever logged as an error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That combination — &lt;em&gt;events arriving, nothing working, no error&lt;/em&gt; — is usually a parsing problem wearing a disguise. It was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two topics that mean the same thing
&lt;/h2&gt;

&lt;p&gt;ONVIF defines the events service and then more or less lets vendors pick how to express "there is motion."&lt;/p&gt;

&lt;p&gt;The classic topic is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tns1:VideoSource/MotionAlarm     with State = true | false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standardized, rule-engine one is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tns1:RuleEngine/CellMotionDetector/Motion   with IsMotion = true | false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The camera in question — a &lt;strong&gt;TP-Link TL-IPC44AW-COLOR&lt;/strong&gt; (fw 1.0.13) — advertises &lt;code&gt;RuleEngine/CellMotionDetector/Motion&lt;/code&gt;, &lt;code&gt;PeopleDetector&lt;/code&gt;, &lt;code&gt;IntrusionDetector&lt;/code&gt;, &lt;code&gt;LineCrossDetector&lt;/code&gt;, &lt;code&gt;TamperDetector&lt;/code&gt;… and &lt;strong&gt;no &lt;code&gt;VideoSource/MotionAlarm&lt;/code&gt; at all&lt;/strong&gt;. It only ever speaks the second dialect.&lt;/p&gt;

&lt;p&gt;The control case in the same report is a &lt;strong&gt;Hikvision DS-IPC-T12HV3-IA&lt;/strong&gt; (V5.7.12), which publishes &lt;em&gt;both&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That control case is the whole story of why this bug survived so long. If your desk has a Hikvision on it — and most people's does — you can't reproduce it. The camera quietly does the right thing anyway. Only someone whose primary camera is a TP-Link hits it, and once you hit it, it's hard to self-diagnose: the event panel has data in it. Only the recording doesn't happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that would have been wrong
&lt;/h2&gt;

&lt;p&gt;The user filed a solid PR against MiBeeNvr itself, adding recognition in the NVR's &lt;code&gt;internal/onvif/motion.go&lt;/code&gt;. Test-first, verified on two physical cameras. Nothing wrong with the change.&lt;/p&gt;

&lt;p&gt;I asked him to re-target it, and I want to be precise about why, because it's the part worth generalizing.&lt;/p&gt;

&lt;p&gt;"Which ONVIF topics count as a motion signal" is &lt;strong&gt;protocol semantics&lt;/strong&gt;. It is not NVR business logic. Every single person using the ONVIF library underneath is exposed to the same trap — they subscribe successfully, the subscription works, and one vendor family silently never fires. If that knowledge lives in one NVR's &lt;code&gt;internal/&lt;/code&gt; directory, the next developer reads the spec again, captures again, and re-derives the same table from scratch.&lt;/p&gt;

&lt;p&gt;He agreed, and the change became &lt;a href="https://github.com/mickeyzzc/onvif-go/pull/131" rel="noopener noreferrer"&gt;onvif-go#131&lt;/a&gt;. The PR body opens with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Re-targeted from Mi-Bee-Studio/MiBeeNvr#951, as agreed there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single line is, in my opinion, the ideal shape of a cross-project contribution: the reporter keeps credit, the fix lands where the next person will actually find it, and the NVR's in-tree copy can be deleted instead of maintained forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in the library
&lt;/h2&gt;

&lt;p&gt;Two pure functions in the &lt;code&gt;events&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MotionAlarm&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Active&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;Score&lt;/span&gt;  &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="c"&gt;// 0–100; 0 when the device does not provide one&lt;/span&gt;
    &lt;span class="n"&gt;Source&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ParseMotionAlarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="n"&gt;NotificationMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MotionAlarm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;IsMotionTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The details are all things real captures taught us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topic matching is case-insensitive, and every path segment's namespace prefix is dropped&lt;/strong&gt; before comparison. &lt;code&gt;tns1:&lt;/code&gt;, &lt;code&gt;tnsalias:&lt;/code&gt;, and vendor-invented prefixes all compare equal. This matters more than it sounds: in a room full of captures, the same semantic topic shows up under at least three prefixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact path matching.&lt;/strong&gt; &lt;code&gt;…/MotionAlarmExtra&lt;/code&gt; and a truncated &lt;code&gt;…/CellMotionDetector&lt;/code&gt; are both rejected. No prefix-containment fuzziness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State is read from the &lt;code&gt;Data&lt;/code&gt; slot only.&lt;/strong&gt; &lt;code&gt;IsMotion&lt;/code&gt; entries in &lt;code&gt;Source&lt;/code&gt; are ignored. If the state is absent or unparseable you get &lt;code&gt;(MotionAlarm{}, false)&lt;/code&gt; — it does not guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Score&lt;/code&gt; is an optional vendor extension.&lt;/strong&gt; Missing or garbage → &lt;code&gt;0&lt;/code&gt;, and &lt;code&gt;Active&lt;/code&gt; is unaffected.&lt;/li&gt;
&lt;li&gt;Other rule-engine topics (&lt;code&gt;Tamper&lt;/code&gt;, &lt;code&gt;LineCross&lt;/code&gt;, &lt;code&gt;PeopleDetector&lt;/code&gt;) are &lt;strong&gt;deliberately not&lt;/strong&gt; folded in. Scope discipline matters more than coverage here; silently mapping tamper to "motion" would corrupt the caller's recording logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test I like most is &lt;code&gt;TestParseMotionAlarmFromPullMessages&lt;/code&gt;. It runs a &lt;strong&gt;captured Hikvision &lt;code&gt;PullMessages&lt;/code&gt; response body&lt;/strong&gt; — raw, as the camera sent it — through the library's real decode path. Because that capture happens to contain both topics and real &lt;code&gt;Source&lt;/code&gt; entries, the test asserts on &lt;em&gt;what the library actually hands a caller&lt;/em&gt;, not on a hand-built ideal struct. Fixtures from real hardware beat fixtures from imagination.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go test -race ./...&lt;/code&gt; clean, &lt;code&gt;golangci-lint&lt;/code&gt; clean, 91.6% coverage on the &lt;code&gt;events&lt;/code&gt; package. Merged 06:40 UTC, shipped in v2.4.0 at 10:28 the same morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "in the library" is the whole argument
&lt;/h2&gt;

&lt;p&gt;This is the part I'd want a library author to take away.&lt;/p&gt;

&lt;p&gt;If you are writing a protocol library, your real product is not your API surface — it's &lt;strong&gt;the accumulated table of ways real hardware deviates from the spec&lt;/strong&gt;, encoded so the next person doesn't have to rebuild it. Every quirk you leave undocumented is a bug someone re-derives with their own captures next quarter.&lt;/p&gt;

&lt;p&gt;So the rule I've ended up applying to this codebase: &lt;strong&gt;if it takes a physical device and a packet capture to discover, it belongs in a regression fixture, not in the changelog.&lt;/strong&gt; A changelog entry says "we fixed it." A fixture says "this will never come back, and here is the exact bytes that proved it."&lt;/p&gt;

&lt;p&gt;The same version's other three changes are the same idea in different clothes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WS-BaseNotification push receiving&lt;/strong&gt; — the half of the events service where the &lt;em&gt;device&lt;/em&gt; POSTs notifications to you, instead of you long-polling. &lt;code&gt;Subscribe&lt;/code&gt; / &lt;code&gt;RenewPush&lt;/code&gt; / &lt;code&gt;UnsubscribePush&lt;/code&gt; to the subscription's &lt;em&gt;own&lt;/em&gt; manager endpoint (the address devices actually require, with a same-address fallback for firmware that collapses both roles), &lt;code&gt;ErrPushNotSupported&lt;/code&gt; so you can probe and fall back to pull-point, and &lt;code&gt;NotifyHandler&lt;/code&gt; as a ready consumer endpoint. Envelope shapes verified against Hikvision, Uniview, TP-Link and in-house firmware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed device time sync&lt;/strong&gt; — &lt;code&gt;GetDeviceTime&lt;/code&gt; returns actual Go types (UTC + local wall clock, POSIX TZ, NTP/manual mode, DST flag) and goes over the &lt;em&gt;unauthenticated&lt;/em&gt; &lt;code&gt;GetSystemDateAndTime&lt;/code&gt;. That's the important part: a drifted camera clock is exactly the condition that breaks digest auth, so the clock is what you need to read &lt;em&gt;precisely when&lt;/em&gt; authenticated calls are failing. Plus &lt;code&gt;ErrNoCredentials&lt;/code&gt; so a write path with no username fails fast instead of firing a guaranteed-401 request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery listener filtering&lt;/strong&gt; — the passive listener now drops announcements with no ONVIF signal before your handler sees them. A Windows/WSD host broadcasting a generic Hello no longer becomes a permanently-pending ghost in your device list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four came from production pain in one NVR. All four now belong to every consumer of the library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people skip: no hardware needed to test
&lt;/h2&gt;

&lt;p&gt;A protocol fix like this is only useful if the next person can &lt;em&gt;verify&lt;/em&gt; it without buying a TP-Link.&lt;/p&gt;

&lt;p&gt;The repo ships a complete ONVIF &lt;strong&gt;virtual camera server&lt;/strong&gt; (&lt;code&gt;server/&lt;/code&gt;, with an in-memory reference provider in &lt;code&gt;server/simulator/&lt;/code&gt;) — multi-profile, PTZ, imaging, device IO, Media2, and pull-point events, described in camera profiles you configure, with a &lt;code&gt;PublishEvent&lt;/code&gt; seam to inject notifications from your own code. You can stand up a fake camera, run a recorder against it, and test the event path end to end in CI.&lt;/p&gt;

&lt;p&gt;That's also how the client-side API stays honest: the library's own events client round-trips against its own server in tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8081&lt;/span&gt;
&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// a fully simulated ONVIF camera&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever put off writing tests for a device-integration layer because "it needs hardware," this is the escape hatch: build the fake device once, then every future bug gets a reproduction that runs on every commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're building against ONVIF in Go
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/mickeyzzc/onvif-go/v2    &lt;span class="c"&gt;# Go 1.26+, zero third-party dependencies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;onvif&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onvif&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCredentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;onvif&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithAutoClockSkew&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c"&gt;// survive cameras whose clock has drifted&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Works whether the camera sends VideoSource/MotionAlarm&lt;/span&gt;
&lt;span class="c"&gt;// or RuleEngine/CellMotionDetector/Motion. Or both. Or neither.&lt;/span&gt;
&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SubscribeEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotificationMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseMotionAlarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// start recording&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few honest notes about scope and tradeoffs, since nobody enjoys finding them out later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This library is ONVIF signaling only.&lt;/strong&gt; It produces and consumes stream &lt;em&gt;URIs&lt;/em&gt; (RTSP/HTTP/UDP); it does not transport media. Pair it with an RTSP server or client for the media plane. That's a deliberate dependency choice — the module has &lt;strong&gt;zero third-party dependencies&lt;/strong&gt;, which matters if this ends up vendored into firmware or a long-running recorder process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's the actively-developed v2 lineage of &lt;a href="https://github.com/0x524a/onvif-go" rel="noopener noreferrer"&gt;&lt;code&gt;0x524a/onvif-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — a MIT-licensed fork with the v1 tag history preserved. The upstream project is alive and maintained; the two live at different module paths (&lt;code&gt;…/onvif-go/v2&lt;/code&gt; vs &lt;code&gt;…/onvif-go&lt;/code&gt;), so &lt;strong&gt;both can coexist in one build&lt;/strong&gt; while you migrate service by service. Most v1 code compiles after the import-path swap; &lt;a href="https://github.com/mickeyzzc/onvif-go/blob/main/MIGRATION.md" rel="noopener noreferrer"&gt;MIGRATION.md&lt;/a&gt; maps the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests run on ubuntu, windows and macos&lt;/strong&gt;, because the release artifacts are built for all three.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a camera that connects, authenticates, and then behaves strangely anyway, please &lt;a href="https://github.com/mickeyzzc/onvif-go/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; with the make and firmware version. Real-hardware weirdness is the most valuable thing you can hand a protocol library — the response time on these is usually a day or two, and the resolution is a fixture, not a patch note.&lt;/p&gt;

</description>
      <category>go</category>
      <category>onvif</category>
      <category>iot</category>
      <category>opensource</category>
    </item>
    <item>
      <title>MiBeeNvr v0.13.0 Performance Self-Benchmark: Capacity and Resource Curves on Three Hardware Tiers</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:59:18 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/mibeenvr-v0130-performance-self-benchmark-capacity-and-resource-curves-on-three-hardware-tiers-d11</link>
      <guid>https://dev.to/mickey_zzc/mibeenvr-v0130-performance-self-benchmark-capacity-and-resource-curves-on-three-hardware-tiers-d11</guid>
      <description>&lt;p&gt;Choosing an NVR keeps running into the same wall: there are no public performance numbers with stated measurement conditions. Third-party cross-product benchmarks don't exist; what you can find is either vendor-quoted partial metrics (detector inference latency, say) or editorial estimate tables that omit the hardware, bitrate, and framerate conditions — the numbers can't be compared against each other.&lt;/p&gt;

&lt;p&gt;So I measured &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr" rel="noopener noreferrer"&gt;MiBeeNvr&lt;/a&gt;'s own capacity and resource curves: three hardware tiers, recording ramps from 16 to 68 cameras, concurrent playback on top of full load, pure forwarding, an fsync-policy comparison, plus a read-only observation of a 7×24 production machine. The methodology, pass/fail criteria, and test scripts are all published with this post. The goal is not to rank products — with no comparable public methodology, ranking means nothing — but to answer two questions that actually come up during capacity planning: how many cameras a machine of a given tier can carry, and whether the bottleneck lands on CPU, memory, or storage.&lt;/p&gt;

&lt;p&gt;Conclusions first, data and tools below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-spec x86: recording 64×1080p + 4×4K at 24% total CPU (16-thread basis) and 171 MB of NVR process memory, API p50 in the low tens of milliseconds throughout; stacking 32 concurrent playback sessions (FLV / LL-HLS / RTSP) on top produced zero failures. The tier stops at 68 cameras because the traffic generator ran out of headroom, not because the NVR struggled.&lt;/li&gt;
&lt;li&gt;Entry ARM NAS (RK3399 + eMMC-class storage): CPU sits at two tenths while the bottleneck is entirely in storage — once the volume saturates, the health endpoint degrades to 2 s, but recording, per-camera accounting, and recording queries are unaffected. Switch to 30-camera pure forwarding and CPU drops to three tenths with a perfectly clean API distribution: plenty of headroom as a live gateway.&lt;/li&gt;
&lt;li&gt;ARM SBC production box (Banana Pi M5): 18 real cameras running 7×24, API p50 still 13.5 ms during a window with 80% disk iowait — two orders of magnitude away from the saturated eMMC tier's 2 s. Storage medium sets the experience ceiling of the low tier, and that conclusion reproduced in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are &lt;strong&gt;vendor self-test&lt;/strong&gt; numbers, and I'm not going to pretend otherwise: every figure's measurement conditions are in this post, the tools and criteria are public, and anyone can rerun the thing end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Banana Pi M5&lt;/td&gt;
&lt;td&gt;Production (read-only observation)&lt;/td&gt;
&lt;td&gt;Amlogic S905X3, ARMv8 ×4&lt;/td&gt;
&lt;td&gt;4GB&lt;/td&gt;
&lt;td&gt;2.7TB SATA&lt;/td&gt;
&lt;td&gt;bare-metal systemd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RK3399 NAS&lt;/td&gt;
&lt;td&gt;Controlled benchmark (low tier)&lt;/td&gt;
&lt;td&gt;RK3399 ×6&lt;/td&gt;
&lt;td&gt;4GB&lt;/td&gt;
&lt;td&gt;eMMC-class volume&lt;/td&gt;
&lt;td&gt;fnOS + Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x86 workstation&lt;/td&gt;
&lt;td&gt;Controlled benchmark (high tier)&lt;/td&gt;
&lt;td&gt;i5-12600KF, 10C16T&lt;/td&gt;
&lt;td&gt;64GB&lt;/td&gt;
&lt;td&gt;NVMe&lt;/td&gt;
&lt;td&gt;bare metal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x86 laptop&lt;/td&gt;
&lt;td&gt;Traffic generation / idle measurement&lt;/td&gt;
&lt;td&gt;8-core&lt;/td&gt;
&lt;td&gt;16GB&lt;/td&gt;
&lt;td&gt;SSD&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two environment details are worth stating up front. The RK3399 NAS's storage volume is a single-member RAID1 with thin provisioning on an eMMC partition — 2× write amplification. That detail keeps resurfacing in the data below; it is the key to reading this tier. And the production machine happened to be running a historical-archive migration during the observation window, so the same device yielded both a "heavy IO stress" and a "business as usual" window.&lt;/p&gt;

&lt;p&gt;The build under test was a single unified v0.13.0 preview build; the production observer ran a preview build from the same series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Criteria: How It Was Measured and Counted
&lt;/h2&gt;

&lt;p&gt;The four-piece load and measurement setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synthetic cameras&lt;/strong&gt;: pre-rendered MP4s pushed in real time (&lt;code&gt;-re&lt;/code&gt; pacing) over RTMP, at two specs — 1080p25 H.264 2.5 Mbps (2 s GOP, no B-frames) and 4K25 H.264 16 Mbps. The pushers reconnect on drop, matching real camera behavior. Per-camera accounting comes from periodic snapshots of the NVR's &lt;code&gt;/api/streams&lt;/code&gt; counters to verify each camera's ingest bitrate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource sampling&lt;/strong&gt;: on Linux, 5-second reads of raw &lt;code&gt;/proc/stat&lt;/code&gt; counters (so iowait can be broken out), &lt;code&gt;/proc/meminfo&lt;/code&gt;, and NVR process RSS; the Windows host sampled performance counters at the same cadence. Disk figures come from iostat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API probe&lt;/strong&gt;: every 5 seconds, request &lt;code&gt;/api/health&lt;/code&gt; (unauthenticated, lightweight) and &lt;code&gt;/api/recordings?limit=5&lt;/code&gt; (auth + SQLite query path), recording the full latency distribution and failure count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playback&lt;/strong&gt;: real pull clients on FLV / LL-HLS / RTSP watching concurrently, measuring throughput, time-to-first-byte, and live-edge latency (LL-HLS computed from the playlist's PROGRAM-DATE-TIME).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Judgment criteria: CPU includes soft interrupts, iowait is broken out separately; the "steady-state window" is the 4 minutes starting 60–75 s after the last pusher starts; API latencies in tables are percentiles over that window. Every curve is backed by 5-second raw data.&lt;/p&gt;

&lt;p&gt;Configuration criteria: except for the fsync comparison section, everything ran on factory defaults — &lt;code&gt;storage.durability&lt;/code&gt; at its default strict level (fsync on every finalized segment), &lt;code&gt;io.budget_bytes_per_sec&lt;/code&gt; at 0 (background IO budget off), preallocation on. The headline numbers are therefore measured in the default posture, with no opt-in tuning switches polishing them.&lt;/p&gt;

&lt;p&gt;Load conversion: using the community's usual MP/s dimension, 1080p25 = 51.8 MP/s per camera, 4K25 = 207.4 MP/s. This run's x86 peak of 68 cameras ≈ 4,145 MP/s; RK3399 pure forwarding of 30 cameras ≈ 1,555 MP/s. Note that MiBeeNvr records in the compressed domain without decoding, so MP/s here is a throughput dimension — comparable against generic planning methods, not equivalent to decode-style workloads.&lt;/p&gt;

&lt;p&gt;The scripts behind this setup are collected in the appendix at the end of the post.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-Spec x86: Recording Ramp and Concurrent Playback
&lt;/h2&gt;

&lt;p&gt;From 16 cameras up to 68 (64×1080p + 4×4K), 3 minutes of steady state per tier, with per-camera ingest held precisely at 2.5/16 Mbps throughout:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Load&lt;/th&gt;
&lt;th&gt;Total ingest&lt;/th&gt;
&lt;th&gt;Total CPU (16T)&lt;/th&gt;
&lt;th&gt;NVR process memory&lt;/th&gt;
&lt;th&gt;health p50/p95&lt;/th&gt;
&lt;th&gt;recordings p50/p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (1 real camera)&lt;/td&gt;
&lt;td&gt;2.0 Mbps&lt;/td&gt;
&lt;td&gt;11.4%&lt;/td&gt;
&lt;td&gt;57 MB&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16×1080p&lt;/td&gt;
&lt;td&gt;42.2 Mbps&lt;/td&gt;
&lt;td&gt;12.8%&lt;/td&gt;
&lt;td&gt;82 MB&lt;/td&gt;
&lt;td&gt;8.5 / 15.2 ms&lt;/td&gt;
&lt;td&gt;9.4 / 38.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32×1080p&lt;/td&gt;
&lt;td&gt;82.3 Mbps&lt;/td&gt;
&lt;td&gt;14.3%&lt;/td&gt;
&lt;td&gt;105 MB&lt;/td&gt;
&lt;td&gt;8.9 / 14.5 ms&lt;/td&gt;
&lt;td&gt;9.0 / 14.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48×1080p&lt;/td&gt;
&lt;td&gt;122.4 Mbps&lt;/td&gt;
&lt;td&gt;15.3%&lt;/td&gt;
&lt;td&gt;131 MB&lt;/td&gt;
&lt;td&gt;9.6 / 16.7 ms&lt;/td&gt;
&lt;td&gt;9.6 / 12.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64×1080p&lt;/td&gt;
&lt;td&gt;162.4 Mbps&lt;/td&gt;
&lt;td&gt;19.7%&lt;/td&gt;
&lt;td&gt;156 MB&lt;/td&gt;
&lt;td&gt;10.6 / 18.2 ms&lt;/td&gt;
&lt;td&gt;10.1 / 22.7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;68 cameras (4×4K included)&lt;/td&gt;
&lt;td&gt;226.5 Mbps&lt;/td&gt;
&lt;td&gt;24.2%&lt;/td&gt;
&lt;td&gt;171 MB&lt;/td&gt;
&lt;td&gt;8.7 / 15.0 ms&lt;/td&gt;
&lt;td&gt;11.1 / 27.6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvt92qti4d9mzkr0fpv6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvt92qti4d9mzkr0fpv6.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both curves are near-linear: each added 1080p camera costs about 3 MB of memory, and CPU climbs roughly 2 percentage points per 40 Mbps (16-thread basis). The API probe recorded zero failures the whole way (also zero during the 32-viewer stage), and health returned to 1.2–1.9 ms once the test ended.&lt;/p&gt;

&lt;p&gt;On top of the full 68-camera recording load, 32 concurrent playback sessions (12 FLV + 12 LL-HLS + 8 RTSP, 5 minutes):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FLV&lt;/td&gt;
&lt;td&gt;12/12 sessions returned 200, each at a precise full 2.50–2.51 Mbps; TTFB min/p50/p95 = 65/167/256 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LL-HLS&lt;/td&gt;
&lt;td&gt;12/12 streamed normally; startup min/p50/p95 = 438/1414/2095 ms; median live-edge latency 1.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTSP&lt;/td&gt;
&lt;td&gt;8 cameras, three rounds of pulls, zero errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Entry ARM NAS: The Bottleneck Is Storage, Not CPU
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Recording Ramp (8 → 28 cameras, 20 → 124 Mbps)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Load&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;iowait&lt;/th&gt;
&lt;th&gt;NVR memory&lt;/th&gt;
&lt;th&gt;health p50&lt;/th&gt;
&lt;th&gt;recordings p50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8×1080p (20 Mbps)&lt;/td&gt;
&lt;td&gt;20.1%&lt;/td&gt;
&lt;td&gt;59%&lt;/td&gt;
&lt;td&gt;130 MB&lt;/td&gt;
&lt;td&gt;24 ms (drifting into saturation late in the tier)&lt;/td&gt;
&lt;td&gt;20 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16×1080p (40 Mbps)&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;td&gt;162 MB&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;22 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24×1080p (60 Mbps)&lt;/td&gt;
&lt;td&gt;22.6%&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;203 MB&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;21 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24×1080p+4×4K (124 Mbps)&lt;/td&gt;
&lt;td&gt;19.7%&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;td&gt;197 MB&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;22 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjraljg4kkwqa0axsblni.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjraljg4kkwqa0axsblni.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The CPU curve flattens at 20–23%, with the NVR process peaking at 41.8% of a single core. The saturation point is storage: iostat shows the volume at 100% utilization, write waits of 3.4–6.9 s, and actual writes of about 6–7 MB/s (RAID1 amplification included). Behavior after saturation is layered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recording and per-camera accounting are unaffected — per-camera bitrate holds and segments keep flushing;&lt;/li&gt;
&lt;li&gt;Recording queries (the SQLite read path) are unaffected, p50 steady at 20–22 ms throughout;&lt;/li&gt;
&lt;li&gt;The health check degrades to 2 s — internally it runs a statfs against the storage volume, which blocks when the device saturates, making it a surprisingly good "storage is gasping" indicator;&lt;/li&gt;
&lt;li&gt;Playback under extreme saturation: LL-HLS delivered data to 8/8 clients but startup stretched to 88 s and live edge ran 35–53 s; RTSP kept streaming; FLV could not establish new sessions within 15 s (106–135 ms when healthy).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recovery after load removal is quick: health p50 returned to 17–23 ms within the first minute, though background IO kept individual requests at 2 s for about 6 minutes, after which max &amp;lt; 31 ms. No crashes, no process restarts at any point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bp07jqcbg9xspfwzpbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1bp07jqcbg9xspfwzpbk.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean Low-Load Tier (4 cameras, 10 Mbps)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;iowait&lt;/th&gt;
&lt;th&gt;NVR memory&lt;/th&gt;
&lt;th&gt;health p50/p95&lt;/th&gt;
&lt;th&gt;recordings p50/p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;td&gt;77 MB&lt;/td&gt;
&lt;td&gt;24 ms / 1485 ms&lt;/td&gt;
&lt;td&gt;21 ms / 188 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even at 10 Mbps the odd statfs spike appears — this tier's bottleneck is entirely the eMMC volume itself, not CPU and not the software.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pure Forwarding Tier (30 live cameras, no recording, 75 Mbps)
&lt;/h3&gt;

&lt;p&gt;With 30 cameras set to live-only (recording disabled):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;iowait&lt;/th&gt;
&lt;th&gt;NVR memory&lt;/th&gt;
&lt;th&gt;health p50/p95/max&lt;/th&gt;
&lt;th&gt;recordings p50/p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;30.5%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;95 MB&lt;/td&gt;
&lt;td&gt;21 ms / 40 ms / 62 ms&lt;/td&gt;
&lt;td&gt;21 ms / 30 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the write path out of the picture, the API distribution over 30 ingests plus distribution is perfectly clean; pulling FLV from a live camera gave 200, 2.52 Mbps, 135 ms TTFB. As a pure live gateway (preview / cascade / re-publish scenarios) the RK3399 has ample margin — roughly 1,555 MP/s of throughput on three tenths of the CPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  fsync Policy Comparison: strict vs relaxed
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;durability&lt;/code&gt; controls the fsync policy for raw segments: the default strict fsyncs on every finalized segment; relaxed skips explicit fsyncs and leaves it to the filesystem commit interval (a power cut may lose the last few seconds of raw segments; merged/timelapse artifacts are unaffected). To quantify what the switch buys on slow storage, I ran a paired test at identical load and background (rolling-merge jobs running in both cases):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;durability&lt;/th&gt;
&lt;th&gt;health p50 / p95&lt;/th&gt;
&lt;th&gt;iowait&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;NVR memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;16×1080p (40 Mbps)&lt;/td&gt;
&lt;td&gt;strict&lt;/td&gt;
&lt;td&gt;2024 / 3045 ms&lt;/td&gt;
&lt;td&gt;72.5%&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;td&gt;162 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16×1080p (40 Mbps)&lt;/td&gt;
&lt;td&gt;relaxed&lt;/td&gt;
&lt;td&gt;2019 / 2027 ms&lt;/td&gt;
&lt;td&gt;73.2%&lt;/td&gt;
&lt;td&gt;21.1%&lt;/td&gt;
&lt;td&gt;171 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24×1080p (60 Mbps)&lt;/td&gt;
&lt;td&gt;strict (first run)&lt;/td&gt;
&lt;td&gt;2022 / 2033 ms&lt;/td&gt;
&lt;td&gt;72.0%&lt;/td&gt;
&lt;td&gt;22.6%&lt;/td&gt;
&lt;td&gt;203 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24×1080p (60 Mbps)&lt;/td&gt;
&lt;td&gt;strict (paired re-run)&lt;/td&gt;
&lt;td&gt;2020 / 2035 ms&lt;/td&gt;
&lt;td&gt;68.4%&lt;/td&gt;
&lt;td&gt;24.9%&lt;/td&gt;
&lt;td&gt;217 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;24×1080p (60 Mbps)&lt;/td&gt;
&lt;td&gt;relaxed&lt;/td&gt;
&lt;td&gt;2023 / 2040 ms&lt;/td&gt;
&lt;td&gt;54.7%&lt;/td&gt;
&lt;td&gt;31.7%&lt;/td&gt;
&lt;td&gt;131 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Instantaneous write-path metrics did improve: under strict steady state the volume showed write waits of 3.4–3.9 s and constant 100% utilization; switching to relaxed brought write waits down to 0.2–0.3 s and turned utilization from "continuously saturated" to "bursty". But across the whole window, health latency and the saturation threshold did not move — this NAS's storage pressure is the sum of several sources (2× write amplification from the single-member RAID1, background bulk IO from rolling merges, the system's own background reads), and dropping recording fsyncs merely converted sustained pressure into bursty pressure, not enough to pull the device back out of saturation.&lt;/p&gt;

&lt;p&gt;Conclusion: on eMMC-class slow storage, relaxed smooths write queuing and is a reasonable option for deployments that tolerate power-cut loss, but it does not change the capacity ceiling of such storage; on NVMe-class fast storage the default tier is never the bottleneck, so there is no need to flip it. This section also confirms that the headline numbers were all measured at the default strict level, with no help from this switch.&lt;/p&gt;

&lt;p&gt;A footnote: during the comparison, one docker restart of the container hit a runtime hang (docker ps reported Up while the process was gone; stop/start recovered it; the log showed no panic before exit and the kernel logged no OOM — a host-environment event, not an NVR crash). SQLite's unchecked-pointed stream keys were lost as a result (cameras and recordings intact); I recreated them and continued. In production, restart via the normal mechanisms (systemd / app store) rather than docker restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production SBC: Two Observation Windows
&lt;/h2&gt;

&lt;p&gt;A Banana Pi M5 (S905X3 ×4 / 4GB / 2.7TB SATA) runs 18 real cameras (a mix of ONVIF / GB28181 / Xiaomi / MJPEG) 7×24, with 2.1 TB of recordings on disk (a single camera at up to 467 GB / 2014 segments). Read-only observation, no changes made:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Calm window (14 min)&lt;/th&gt;
&lt;th&gt;Heavy-IO window (46 min)¹&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;health p50 / p95 / max&lt;/td&gt;
&lt;td&gt;12.2 / 25.3 / 1015 ms&lt;/td&gt;
&lt;td&gt;13.5 / 53 / 2232 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recordings p50 / p95&lt;/td&gt;
&lt;td&gt;252 / 306 ms&lt;/td&gt;
&lt;td&gt;253 / 338 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingest bitrate / active cameras&lt;/td&gt;
&lt;td&gt;16.1 Mbps / 12&lt;/td&gt;
&lt;td&gt;17.9 Mbps / 12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVR process RSS (min/avg/max)&lt;/td&gt;
&lt;td&gt;182 / 267 / 414 MB&lt;/td&gt;
&lt;td&gt;114 / 194 / 412 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SoC temperature (avg / max)&lt;/td&gt;
&lt;td&gt;51.2 / 54.5 ℃&lt;/td&gt;
&lt;td&gt;52.6 / 58.3 ℃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total CPU (incl. iowait conversion)&lt;/td&gt;
&lt;td&gt;20.1%&lt;/td&gt;
&lt;td&gt;34.5% (mostly iowait)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;¹ The machine was migrating its historical archive during the stress window, with disk iowait around 80%; the NVR process itself used about half a core, and recording continued.&lt;/p&gt;

&lt;p&gt;The same SATA device held health p50 at 13.5 ms under 80% iowait, versus 2 s for the eMMC volume under comparable pressure earlier — two orders of magnitude apart. The storage medium sets the experience ceiling of the low tier, and that conclusion reproduced in production. The 250 ms scale of recordings queries matches its 2.1 TB / tens-of-thousands-of-rows library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries and Reproducing
&lt;/h2&gt;

&lt;p&gt;Not covered: software transcoding scale (H.265→H.264 software transcoding is CPU-bound; production should avoid it by recording sub-streams), WebRTC end-to-end latency (needs browser-side measurement), 10 GbE, cloud-archive throughput, audio tracks, and the &lt;code&gt;io.budget_bytes_per_sec&lt;/code&gt; background IO budget (off by default; background merge load was a constant during the test window, not A/B tested separately). Synthetic streams are video-only.&lt;/p&gt;

&lt;p&gt;Reproducing: the tools are the appendix scripts — synthetic camera sources, the pusher fleet, resource samplers, API probe, per-camera snapshots, the three-protocol pull clients, and the ramp orchestration — pointed at any target host via environment variables. A same-hardware cross-product comparison (say, Frigate or ZoneMinder on the same box under the same load) has no third-party public data either; it would be worth doing, but first someone has to put every product under the same methodology.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr" rel="noopener noreferrer"&gt;Mi-Bee-Studio/MiBeeNvr&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: The Test Harness
&lt;/h2&gt;

&lt;p&gt;The four-piece setup from the methodology section lands as the scripts below. The original test kit hardcoded LAN addresses, passwords, and machine-specific choreography; before publishing I did two things to all of it: &lt;strong&gt;sanitized&lt;/strong&gt; (addresses and credentials now come from environment variables) and &lt;strong&gt;generalized&lt;/strong&gt; (process names, durations, camera counts, and ramp steps are all parameters — nothing is tied to a specific machine). The tools only do measurement; the product-specific parts reduce to two strings, the API paths and the process name. The pusher and pull clients work against any RTMP / HTTP-FLV / LL-HLS service as-is.&lt;/p&gt;

&lt;p&gt;Deployment: the traffic generator runs the pusher fleet and the pull clients; the device under test runs the resource sampler; the API probe can sit on either side. Render the synthetic sources once and reuse them across runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synthetic Camera Sources
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;testsrc2&lt;/code&gt; renders video-only sources with a 2 s GOP and no B-frames, specced like a real IPC main stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# render-cam.sh — render one synthetic camera source (video-only)&lt;/span&gt;
&lt;span class="c"&gt;# usage: render-cam.sh &amp;lt;WxH&amp;gt; &amp;lt;fps&amp;gt; &amp;lt;bitrate_k&amp;gt; &amp;lt;duration_s&amp;gt; &amp;lt;out.mp4&amp;gt; [profile]&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1920x1080&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;FPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;25&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;BR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;2500&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;T&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1800&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;5&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;PROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;6&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-loglevel&lt;/span&gt; error &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"testsrc2=size=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RES&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:rate=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FPS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$T&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-preset&lt;/span&gt; veryfast &lt;span class="nt"&gt;-profile&lt;/span&gt;:v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt;:v &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;k"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-maxrate&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; BR &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;k &lt;span class="nt"&gt;-bufsize&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; BR &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;k &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; FPS &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="nt"&gt;-bf&lt;/span&gt; 0 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One source per spec (4K uses the high profile):&lt;/span&gt;
bash render-cam.sh 1920x1080 25 2500 1800 cam1080.mp4 main
bash render-cam.sh 3840x2160 25 16000 600  cam4k.mp4 high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pusher Fleet
&lt;/h3&gt;

&lt;p&gt;A single pusher is a reconnect loop — real cameras re-push after a network blip, so the load model can't skip that; the fleet script handles batch start/stop and counting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# pusher-loop.sh — one pusher: reconnects 2 s after a drop, mimicking a real camera&lt;/span&gt;
&lt;span class="c"&gt;# usage: pusher-loop.sh &amp;lt;rtmp base&amp;gt; &amp;lt;media file&amp;gt; &amp;lt;stream key&amp;gt;&lt;/span&gt;
&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;ffmpeg &lt;span class="nt"&gt;-loglevel&lt;/span&gt; error &lt;span class="nt"&gt;-re&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="nt"&gt;-f&lt;/span&gt; flv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"pusher_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.log"&lt;/span&gt; 2&amp;gt;&amp;amp;1
  &lt;span class="nb"&gt;sleep &lt;/span&gt;2
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# pushers.sh — pusher fleet: start &amp;lt;rtmp base&amp;gt; &amp;lt;file&amp;gt; &amp;lt;key...&amp;gt; | stop | count&lt;/span&gt;
&lt;span class="nv"&gt;MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;PIDF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pushers.pid
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$MODE&lt;/span&gt; &lt;span class="k"&gt;in
&lt;/span&gt;start&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;3
  &lt;span class="k"&gt;for &lt;/span&gt;KEY &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;nohup &lt;/span&gt;bash pusher-loop.sh &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$!&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PIDF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;sleep &lt;/span&gt;0.4            &lt;span class="c"&gt;# stagger startups to avoid a connect storm&lt;/span&gt;
  &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"started=$#"&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
stop&lt;span class="p"&gt;)&lt;/span&gt;
  pkill &lt;span class="nt"&gt;-f&lt;/span&gt; pusher-loop.sh 2&amp;gt;/dev/null
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PIDF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PIDF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PIDF&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;1
  pkill &lt;span class="nt"&gt;-9&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'rtmp://.*/live/'&lt;/span&gt; 2&amp;gt;/dev/null   &lt;span class="c"&gt;# catch leftover ffmpeg; don't run this near unrelated ffmpeg jobs&lt;/span&gt;
  &lt;span class="nb"&gt;echo &lt;/span&gt;stopped&lt;span class="p"&gt;;;&lt;/span&gt;
count&lt;span class="p"&gt;)&lt;/span&gt;
  pgrep &lt;span class="nt"&gt;-fc&lt;/span&gt; &lt;span class="s1"&gt;'rtmp://.*/live/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Resource Samplers
&lt;/h3&gt;

&lt;p&gt;Reading raw &lt;code&gt;/proc&lt;/code&gt; counters instead of pre-computed percentages is what lets iowait be broken out — the bottleneck calls on the low tier hinge on it. The process name is a parameter, so it samples any service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# sampler.sh — target-host resource sampler: raw counters, one line every 5 s by default&lt;/span&gt;
&lt;span class="c"&gt;# usage: sampler.sh &amp;lt;out.csv&amp;gt; [interval_s] [process name]&lt;/span&gt;
&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;INT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;PROC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;mibee&lt;/span&gt;&lt;span class="p"&gt;-nvr&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ts,user,nice,sys,idle,iowait,irq,softirq,steal,mem_avail_kb,proc_rss_kb,pgpgout"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;C&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^cpu /{print $2","$3","$4","$5","$6","$7","$8","$9}'&lt;/span&gt; /proc/stat&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;MA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/MemAvailable/{print $2}'&lt;/span&gt; /proc/meminfo&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;RSS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/VmRSS/{print $2}'&lt;/span&gt; &lt;span class="s2"&gt;"/proc/&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;/status"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;PG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/pgpgout/{print $2}'&lt;/span&gt; /proc/vmstat&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;$C&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;$MA&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RSS&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;-1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;$PG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows targets get the performance-counter variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sampler.ps1 — Windows target sampler, same idea as the Linux one&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sample.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ProcName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mibee-nvr"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"ts,cpu_busy,mem_avail_mb,proc_ws_mb,disk_w_mb_s"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Encoding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;utf8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Start-Sleep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Interval&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$cpu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Counter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\Processor(_Total)\% Processor Time'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SampleInterval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MaxSamples&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CounterSamples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CookedValue&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$os&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_OperatingSystem&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProcName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$ws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WorkingSet64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1MB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Counter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\PhysicalDisk(_Total)\Disk Write Bytes/sec'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SampleInterval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MaxSamples&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CounterSamples&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CookedValue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1MB&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Add-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"{0},{1:N1},{2},{3},{4:N1}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UtcNow.ToUnixTimeSeconds&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cpu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FreePhysicalMemory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1KB&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  API Probe
&lt;/h3&gt;

&lt;p&gt;The two paths mean different things: health is unauthenticated and lightweight; recordings carries auth plus a SQLite query — a real read path. The probe only logs timestamps and status codes; the latency distribution is post-processing's job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;apiprobe.py — API latency probe
usage: NVR_API=http://&amp;lt;host&amp;gt;:&amp;lt;port&amp;gt; NVR_USER=... NVR_PASS=... python3 apiprobe.py &amp;lt;out.csv&amp;gt; [interval_s]&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVR_API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;basic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVR_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVR_PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;timed_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;basic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;

&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffering&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts,health_ms,health_code,rec_ms,rec_code&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timed_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timed_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/recordings?limit=5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Per-Camera Snapshots
&lt;/h3&gt;

&lt;p&gt;Periodically recording the &lt;code&gt;/api/streams&lt;/code&gt; counters and differencing adjacent snapshots yields each camera's actual ingest bitrate — what the pusher claims doesn't count; what the server receives does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;statsnap.py — per-camera accounting snapshots
usage: NVR_API=... NVR_USER=... NVR_PASS=... python3 statsnap.py &amp;lt;out.jsonl&amp;gt; [interval_s]&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVR_API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;basic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVR_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NVR_PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;basic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffering&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streams&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/streams&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streams&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;err&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pull Clients
&lt;/h3&gt;

&lt;p&gt;The FLV client is small: throughput and time-to-first-byte. The HLS client does more — it follows the master playlist to the variant and computes live-edge latency from PROGRAM-DATE-TIME. Both scripts strip the query string before writing URLs to CSV, so stream tokens never leak into the data files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;flvpull.py — FLV pull client: TTFB, throughput, status code
usage: python3 flvpull.py &amp;lt;stream.flv URL&amp;gt; &amp;lt;out.csv&amp;gt; &amp;lt;duration_s&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttfb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;dur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ttfb&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ttfb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
            &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
&lt;span class="n"&gt;mbps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e6&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ttfb&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ttfb&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mbps&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flv code=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ttfb=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ttfb&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttfb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; mbps=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mbps&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;hlspull.py — HLS/LL-HLS pull client: startup time, throughput, live-edge latency (PDT)
usage: python3 hlspull.py &amp;lt;index.m3u8 URL&amp;gt; &amp;lt;out.csv&amp;gt; &amp;lt;duration_s&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;startup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;lags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;pdt_re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^#EXT-X-PROGRAM-DATE-TIME:(.+)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;var_re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^#EXT-X-STREAM-INF:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;dur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;txt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;startup&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;startup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;var_re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;          &lt;span class="c1"&gt;# master playlist -&amp;gt; follow to the variant
&lt;/span&gt;            &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#EXT-X-STREAM-INF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
                    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdt_re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# live edge = local clock - PDT
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pdt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;lag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pdt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lag&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;lags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lag&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt;
                    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
&lt;span class="n"&gt;mbps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e6&lt;/span&gt;
&lt;span class="n"&gt;lag_avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lags&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;startup&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;startup&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mbps&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lag_avg&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hls segs=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; startup=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;startup&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; mbps=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mbps&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; live_lag_avg_ms=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lag_avg&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Ramp Orchestration
&lt;/h3&gt;

&lt;p&gt;The recording ramp is data-driven: edit the &lt;code&gt;TIERS&lt;/code&gt; array to change the ramp, and the script itself knows nothing about any particular machine. Each tier pushes, climbs for 60 s, holds steady for 180 s, snapshots per-camera accounting, and logs event timestamps into &lt;code&gt;marks.txt&lt;/code&gt; for post-processing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ramp.sh — recording ramp orchestration&lt;/span&gt;
&lt;span class="c"&gt;# usage: NVR_API=http://&amp;lt;host&amp;gt;:&amp;lt;port&amp;gt; RTMP=rtmp://&amp;lt;host&amp;gt;:1935/live \&lt;/span&gt;
&lt;span class="c"&gt;#        NVR_USER=... NVR_PASS=... bash ramp.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NVR_API&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RTMP&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NVR_USER&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NVR_PASS&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TIERS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"cam1080.mp4:16"&lt;/span&gt; &lt;span class="s2"&gt;"cam1080.mp4:16"&lt;/span&gt; &lt;span class="s2"&gt;"cam1080.mp4:16"&lt;/span&gt; &lt;span class="s2"&gt;"cam4k.mp4:4"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;marks.txt
mark&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$M&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
snap&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 30 &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NVR_USER&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$NVR_PASS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NVR_API&lt;/span&gt;&lt;span class="s2"&gt;/api/streams"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"streams-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
mark RAMP_START
&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;tier &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIERS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;n &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;%%&lt;/span&gt;:&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;CNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;##*&lt;/span&gt;:&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CNT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s2"&gt;"s/^/load-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="p"&gt;%.*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-/"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  mark &lt;span class="s2"&gt;"S&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_start"&lt;/span&gt;
  bash pushers.sh start &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RTMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$keys&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;60&lt;span class="p"&gt;;&lt;/span&gt;  mark &lt;span class="s2"&gt;"S&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_steady"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;180&lt;span class="p"&gt;;&lt;/span&gt; snap&lt;span class="p"&gt;;&lt;/span&gt; mark &lt;span class="s2"&gt;"S&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_end"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;mark RAMP_DONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stacking concurrent playback on top of full load works the same way: resolve camera IDs from &lt;code&gt;/api/cameras&lt;/code&gt; by name, hand the ID list to the pull scripts, start a batch each of FLV and LL-HLS, and spot-check RTSP with ffmpeg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# egress.sh — concurrent playback stacked on full recording load&lt;/span&gt;
&lt;span class="c"&gt;# usage: NVR_API=... NVR_TOKEN=... RTSP_BASE=rtsp://&amp;lt;host&amp;gt;:8554 \&lt;/span&gt;
&lt;span class="c"&gt;#        bash egress.sh "&amp;lt;camera ID list, space-separated&amp;gt;" [duration_s]&lt;/span&gt;
&lt;span class="nv"&gt;IDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?&lt;span class="s2"&gt;"camera ID list, resolved from /api/cameras by name"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;DUR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;300&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NVR_API&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NVR_TOKEN&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RTSP_BASE&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; egress-flv.csv&lt;span class="p"&gt;;&lt;/span&gt; : &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; egress-hls.csv
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$IDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;nohup &lt;/span&gt;python3 flvpull.py &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NVR_API&lt;/span&gt;&lt;span class="s2"&gt;/api/cameras/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;/stream.flv?token=&lt;/span&gt;&lt;span class="nv"&gt;$NVR_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; egress-flv.csv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DUR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="k"&gt;done
for &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$IDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;nohup &lt;/span&gt;python3 hlspull.py &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NVR_API&lt;/span&gt;&lt;span class="s2"&gt;/api/cameras/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;/stream/index.m3u8?token=&lt;/span&gt;&lt;span class="nv"&gt;$NVR_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; egress-hls.csv &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DUR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="k"&gt;done
for &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IDS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-8&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;   &lt;span class="c"&gt;# RTSP spot-check on the first 8&lt;/span&gt;
  ffmpeg &lt;span class="nt"&gt;-loglevel&lt;/span&gt; error &lt;span class="nt"&gt;-rtsp_transport&lt;/span&gt; tcp &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RTSP_BASE&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 10 &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="nt"&gt;-f&lt;/span&gt; null - &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; egress-rtsp.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;wait
echo &lt;/span&gt;egress-done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Idle footprint and cold start get their own measurement: start a zero-camera instance with a minimal config, time from process start until health responds, several rounds in a row with the data directory wiped between rounds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# startup.sh — cold start to API-ready latency, several consecutive trials&lt;/span&gt;
&lt;span class="c"&gt;# usage: BIN=./mibee-nvr READY_URL=http://127.0.0.1:9199/api/health bash startup.sh&lt;/span&gt;
&lt;span class="nv"&gt;BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BIN&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;READY_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;READY_URL&lt;/span&gt;:?&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRIALS&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; data db.sqlite&lt;span class="k"&gt;*&lt;/span&gt;
  &lt;span class="nv"&gt;S&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s%N&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-config&lt;/span&gt; config.yaml &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nvr.log 2&amp;gt;&amp;amp;1 &amp;amp;
  &lt;span class="nv"&gt;PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;t &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 600&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 1 &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$READY_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break
    sleep &lt;/span&gt;0.05
  &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nv"&gt;E&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s%N&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"trial &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;E &lt;span class="o"&gt;-&lt;/span&gt; S&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1000000&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt; ms"&lt;/span&gt;
  &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nv"&gt;$PID&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nv"&gt;$PID&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post-processing has little to explain: the raw data is 5-second CSV/JSONL plus event timestamps in &lt;code&gt;marks.txt&lt;/code&gt;; window statistics are percentile queries over timestamp slices, and the charts are matplotlib. Those scripts couple tightly to the data formats so I'll skip them — the idea is exactly the two clauses in this paragraph. Cleanup after a run is a pusher stop plus deleting cameras by name prefix — ordinary API calls, not worth the space.&lt;/p&gt;

</description>
      <category>go</category>
    </item>
    <item>
      <title>MiBee Eye Notebook: turn a spare laptop into a network camera, microphone included</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Wed, 16 Sep 2026 13:32:25 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/mibee-eye-notebook-turn-a-spare-laptop-into-a-network-camera-microphone-included-48l4</link>
      <guid>https://dev.to/mickey_zzc/mibee-eye-notebook-turn-a-spare-laptop-into-a-network-camera-microphone-included-48l4</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The laptop in the drawer
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is, and what it deliberately isn't
&lt;/h2&gt;

&lt;p&gt;The scope is narrow on purpose, and the boundaries are written down as definitional rather than as engineering decisions someone might revisit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is not&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;Inside the machine:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    CAM["Camera&amp;lt;br/&amp;gt;USB or built-in"] --&amp;gt; CAP["Capture&amp;lt;br/&amp;gt;V4L2 read"]
    CAP --&amp;gt; ENC["H.264 encode&amp;lt;br/&amp;gt;openh264"]
    MIC["Microphone&amp;lt;br/&amp;gt;USB or built-in"] --&amp;gt; AUD["Audio encode&amp;lt;br/&amp;gt;G.711 or AAC"]
    ENC --&amp;gt; HUB["Encoded frame hub"]
    HUB --&amp;gt; LOCAL["Local output"]
    LOCAL --&amp;gt; PRE["Browser preview"]
    LOCAL --&amp;gt; REC["MP4 recordings"]
    HUB --&amp;gt; PULL["Pull from outside"]
    PULL --&amp;gt; RTSP["RTSP server"]
    PULL --&amp;gt; OV["ONVIF device"]
    HUB --&amp;gt; PUSH["Push to outside"]
    PUSH --&amp;gt; RTMP["RTMP push"]
    PUSH --&amp;gt; GB["GB28181 device"]
    AUD --&amp;gt; REC&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Default ports: web UI &lt;code&gt;8443&lt;/code&gt; (TLS), RTSP &lt;code&gt;8554&lt;/code&gt;, RTMP &lt;code&gt;1935&lt;/code&gt;, SIP &lt;code&gt;5060&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your microphone is a first-class input
&lt;/h2&gt;

&lt;p&gt;This is the part the board-based implementations don't do, and it's what makes repurposing a laptop interesting.&lt;/p&gt;

&lt;p&gt;The microphone path forks into two encoders for two purposes:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    MIC["Microphone"] --&amp;gt; PCM["PCM capture"]
    PCM --&amp;gt; G711["G.711 encode"]
    PCM --&amp;gt; AAC["AAC encode&amp;lt;br/&amp;gt;optional feature"]
    G711 --&amp;gt; GB["GB28181 stream"]
    AAC --&amp;gt; REC["MP4 recordings"]
    AAC --&amp;gt; RTMP["RTMP push"]
    PLAT["Platform talkback"] --&amp;gt; DEC["G.711 decode"]
    DEC --&amp;gt; SPK["Local speaker"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;488&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything outbound is off until you turn it on
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The rest of the security posture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TLS is mandatory&lt;/strong&gt; on the web UI. rustls only, no HTTP fallback, not even on localhost. A self-signed dev certificate is generated on first run.&lt;/li&gt;
&lt;li&gt;Admin credentials are bcrypt-hashed. Session cookies last 24 hours with &lt;code&gt;HttpOnly; Secure; SameSite=Strict&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Double-submit CSRF on state-changing requests, and a strict CSP header.&lt;/li&gt;
&lt;li&gt;The only anonymous endpoints are &lt;code&gt;/health&lt;/code&gt; and &lt;code&gt;/metrics&lt;/code&gt;. Everything else needs a session.&lt;/li&gt;
&lt;li&gt;Two-factor auth and mutual TLS are explicitly out of scope for v1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only interface is the browser
&lt;/h2&gt;

&lt;p&gt;The web UI isn't a convenience wrapper around a config file — it's the primary surface, and the CLI is the escape hatch.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, then snapshot polling.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    OPEN["Open live view"] --&amp;gt; P1["MSE over HTTP"]
    P1 --&amp;gt; P2["MJPEG fallback"]
    P2 --&amp;gt; P3["Snapshot polling"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;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 &lt;code&gt;currentTime&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  No ffmpeg, no subprocesses
&lt;/h2&gt;

&lt;p&gt;Earlier versions shelled out to ffmpeg for five different jobs. All five are now in-process Rust:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Former ffmpeg job&lt;/th&gt;
&lt;th&gt;Replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H.264 encode&lt;/td&gt;
&lt;td&gt;openh264 (Cisco, BSD-2, built from source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MJPEG decode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jpeg-decoder&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YUYV to JPEG preview&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jpeg-encoder&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio encode&lt;/td&gt;
&lt;td&gt;G.711 μ-law, or optional FDK-AAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MP4 segment muxing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;muxide&lt;/code&gt;, a pure-Rust fMP4 muxer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small things that matter on a machine you also use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol hot-toggle.&lt;/strong&gt; Turning ONVIF, GB28181 or RTMP on and off from the web UI doesn't restart the service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hot-plug.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE event stream.&lt;/strong&gt; Camera online/offline events reach the browser without a refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Platform reality
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Linux x86_64 and aarch64 are the only targets that compile today.&lt;/strong&gt; Windows and macOS are planned and don't build yet. The stated blockers are concrete: &lt;code&gt;libc::getifaddrs&lt;/code&gt; is POSIX-only, parts of the capture code have &lt;code&gt;#[cfg(unix)]&lt;/code&gt; without a Windows branch, and device paths are hardcoded as &lt;code&gt;/dev/videoN&lt;/code&gt;. macOS would need AVFoundation-aware device enumeration on top of that.&lt;/p&gt;

&lt;p&gt;That matters because the obvious user for this product is someone with a spare Windows laptop, and today they can't run it.&lt;/p&gt;

&lt;p&gt;Other things to know before you build a plan on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No releases yet.&lt;/strong&gt; No tags, no GitHub releases, the changelog is entirely under Unreleased at version 0.1.0. If you want it, you compile it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The README lags the code in two places.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The resource figures are targets, not measurements.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One authentication factor.&lt;/strong&gt; A strong password. No 2FA, no mTLS, no per-user ACLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cloud, no mobile app, no push notifications.&lt;/strong&gt; By design, but it's a gap if that's what you wanted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's brand new.&lt;/strong&gt; First pushed on 2026-09-16, one star. I won't make claims about maintenance cadence or response times.&lt;/li&gt;
&lt;li&gt;The toolchain requirement is Rust 1.88 with edition 2024, which is on the new side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The protocol layer is the same two Rust libraries
&lt;/h2&gt;

&lt;p&gt;The ONVIF device service is &lt;code&gt;onvif-device-rs&lt;/code&gt;; the GB28181 device stack, including GB 35114 A-level, is &lt;code&gt;gb28181-rs&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# System dependencies (Linux)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libv4l-dev libasound2-dev libclang-dev
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; video &lt;span class="nv"&gt;$USER&lt;/span&gt;     &lt;span class="c"&gt;# reclaim webcam access; requires a fresh login&lt;/span&gt;

git clone https://github.com/xiqing85/mibee-eye-notebook
&lt;span class="nb"&gt;cd &lt;/span&gt;mibee-eye-notebook
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;

&lt;span class="nb"&gt;cp &lt;/span&gt;config.toml config.local.toml   &lt;span class="c"&gt;# gitignored; keep local overrides here&lt;/span&gt;

cargo run &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--config&lt;/span&gt; config.local.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;https://&amp;lt;host&amp;gt;:8443&lt;/code&gt;. First run walks you through creating the admin account and generates the self-signed certificate, so the browser warning is expected.&lt;/p&gt;

&lt;p&gt;Three config gotchas: &lt;code&gt;[web] advertised_host&lt;/code&gt; 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. &lt;code&gt;[capture] video_device&lt;/code&gt; defaults to &lt;code&gt;/dev/video0&lt;/code&gt;, so check the device number on a multi-camera machine. And if you lose the password, &lt;code&gt;--reset-password&lt;/code&gt; is the way back in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project links
&lt;/h2&gt;

&lt;p&gt;mibee-eye-notebook: &lt;a href="https://github.com/xiqing85/mibee-eye-notebook" rel="noopener noreferrer"&gt;https://github.com/xiqing85/mibee-eye-notebook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Raspberry Pi-class implementations in the same family:&lt;/p&gt;

&lt;p&gt;mibee-eye-go: &lt;a href="https://github.com/xiqing85/mibee-eye-go" rel="noopener noreferrer"&gt;https://github.com/xiqing85/mibee-eye-go&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;mibee-eye-rs: &lt;a href="https://github.com/xiqing85/mibee-eye-rs" rel="noopener noreferrer"&gt;https://github.com/xiqing85/mibee-eye-rs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The protocol libraries underneath:&lt;/p&gt;

&lt;p&gt;onvif-rs: &lt;a href="https://github.com/mickeyzzc/onvif-rs" rel="noopener noreferrer"&gt;https://github.com/mickeyzzc/onvif-rs&lt;/a&gt; (published on crates.io as &lt;code&gt;onvif-device-rs&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;gb28181-rs: &lt;a href="https://github.com/mickeyzzc/gb28181-rs" rel="noopener noreferrer"&gt;https://github.com/mickeyzzc/gb28181-rs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Licence: Apache-2.0 for the capture agent. The two protocol libraries are MIT.&lt;/p&gt;

</description>
      <category>camera</category>
      <category>rust</category>
      <category>laptop</category>
    </item>
    <item>
      <title>MiBee Eye: turn a spare Linux board into an ONVIF camera your NVR already trusts</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Tue, 15 Sep 2026 19:12:30 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/mibee-eye-turn-a-spare-linux-board-into-an-onvif-camera-your-nvr-already-trusts-455b</link>
      <guid>https://dev.to/mickey_zzc/mibee-eye-turn-a-spare-linux-board-into-an-onvif-camera-your-nvr-already-trusts-455b</guid>
      <description>&lt;h2&gt;
  
  
  The camera you already own, and the bill attached to it
&lt;/h2&gt;

&lt;p&gt;Most consumer cameras sold today work like this: the video goes to a company's servers, the useful features sit behind a monthly plan, and the device stops getting firmware the moment the product line is retired. You bought the hardware, but the footage and the feature set aren't really yours.&lt;/p&gt;

&lt;p&gt;The alternative used to mean buying into an ecosystem anyway — a vendor NVR, a vendor app, a vendor account. What's actually missing is the boring middle layer: something that takes a camera sensor on a board you already own and presents it to the software you already run, using the protocols that software already speaks.&lt;/p&gt;

&lt;p&gt;That's what MiBee Eye is. It turns any Linux board into an ONVIF Profile S device with RTSP streaming, and for installations that need it, a GB/T 28181 device. There are two independent implementations, Go and Rust. No account, no cloud round-trip, no subscription. Two terminal commands and a systemd unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it presents to the rest of your network
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;What's there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8080&lt;/td&gt;
&lt;td&gt;ONVIF device service (Device / Media / Imaging) + WS-Discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8554&lt;/td&gt;
&lt;td&gt;RTSP, H.264 over TCP interleaved or UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8088&lt;/td&gt;
&lt;td&gt;Embedded admin UI, English and Chinese&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ONVIF Profile S device.&lt;/strong&gt; Device, Media and Imaging SOAP services plus WS-Discovery multicast announcements, so an NVR finds it without a manual address entry. Against an ONVIF NVR it looks like an ordinary camera.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GB/T 28181 device (optional path).&lt;/strong&gt; SIP registration over UDP or TCP with digest auth, catalog and device-info queries, live/playback/download PS streams, SIP INFO playback control, platform snapshot commands. Relevant if you're integrating with a Chinese standards-based video platform rather than an ONVIF VMS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GB 35114 A-level (compile-time opt-in).&lt;/strong&gt; SM2 certificate registration and keyed-SM3 integrity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous local recording.&lt;/strong&gt; H.264 segments on disk with an &lt;code&gt;index.jsonl&lt;/code&gt; index, retention days and a storage cap. This doubles as the source for GB28181 playback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTMP push&lt;/strong&gt; to a cloud ingest, and JPEG snapshots over &lt;code&gt;GET /snapshot&lt;/code&gt;. The Go build also carries a pure-Go MPEG-TS segmenter, so browsers get HLS without ffmpeg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imaging controls&lt;/strong&gt; for brightness, contrast, saturation, sharpness, white balance and exposure mode. The Rust build adds an OSD watermark — custom text plus a live clock — burned in pre-encode, so it appears on RTSP, RTMP, recordings and the web preview alike.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood everything hangs off one encoded frame hub:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    CAM["Camera&amp;lt;br/&amp;gt;CSI or USB"] --&amp;gt; CAP["Capture&amp;lt;br/&amp;gt;board-native path"]
    CAP --&amp;gt; ENC["H.264 encode&amp;lt;br/&amp;gt;HW or SW"]
    ENC --&amp;gt; HUB["Encoded frame hub"]
    HUB --&amp;gt; RTSP["RTSP :8554"]
    RTSP --&amp;gt; NVR["NVR or VMS"]
    HUB --&amp;gt; REC["Local recording"]
    REC --&amp;gt; GB["GB28181 device"]
    GB --&amp;gt; PLAT["Platform"]
    HUB --&amp;gt; WEB["Web UI :8088"]
    WEB --&amp;gt; BROWSER["Browser"]
    ENC -. tap .-&amp;gt; AI["Detection&amp;lt;br/&amp;gt;opt-in"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Detection is a passive subscriber on that hub. If it's slow, or the model won't load, streaming is unaffected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection that never leaves the board
&lt;/h2&gt;

&lt;p&gt;Object detection is compile-time opt-in, and it runs locally or not at all. Nothing is uploaded, and there's no cloud inference bill because there's no cloud inference.&lt;/p&gt;

&lt;p&gt;The Rust build taps the shared pre-encode YUV buffer, so inference never touches the capture or encoding path. The Go build subscribes to the encoded-frame hub and decodes keyframes for inference.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    YUV["Shared YUV&amp;lt;br/&amp;gt;pre-encode"] --&amp;gt; PRE["Preprocess&amp;lt;br/&amp;gt;YUV to 320 square"]
    PRE --&amp;gt; RUN["ONNX Runtime&amp;lt;br/&amp;gt;NanoDet-Plus"]
    RUN --&amp;gt; POST["Postprocess&amp;lt;br/&amp;gt;argmax, GFL, NMS"]
    POST --&amp;gt; API["REST and SSE&amp;lt;br/&amp;gt;boxes in native px"]
    GUARD["Guardrails&amp;lt;br/&amp;gt;memory and core"] -. controls .-&amp;gt; RUN&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The model is NanoDet-Plus-m 320 in ONNX form, Apache-2.0 licensed. There's a small model registry with runtime hot-swap between a 320 and a 416 variant, plus a YOLOX-nano decoder if you want a different family. Uploading new models at runtime is supported but off by default behind an explicit config gate, since a model file is untrusted input to an inference engine.&lt;/p&gt;

&lt;p&gt;Detection fails open. If the runtime library or the model file is missing, the capability reports &lt;code&gt;ai:false&lt;/code&gt; and the API says so — it never returns fabricated boxes. The trade-off is honest: &lt;code&gt;libonnxruntime.so&lt;/code&gt; and the model are not bundled in the release tarball, so detection is a setup step, not a checkbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which build
&lt;/h2&gt;

&lt;p&gt;Two codebases, same protocols, same web API, same SPEC v1 JSON envelope. NVRs can't tell them apart. The choice is about the board and about how you like to deploy.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    Q["Pick a build"] --&amp;gt; P1["Want the easiest&amp;lt;br/&amp;gt;cross-compile"]
    Q --&amp;gt; P2["Tightest memory&amp;lt;br/&amp;gt;and flash"]
    Q --&amp;gt; P3["Want OSD or&amp;lt;br/&amp;gt;zero subprocesses"]
    P1 --&amp;gt; G["Go build"]
    P2 --&amp;gt; R["Rust build"]
    P3 --&amp;gt; R
    G --&amp;gt; N1["Ships HLS and&amp;lt;br/&amp;gt;a bilingual UI"]
    R --&amp;gt; N2["About 2 MB binary&amp;lt;br/&amp;gt;no subprocesses"]&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fastest path, no cross-compile friction&lt;/td&gt;
&lt;td&gt;Go — zero CGO, stock cross-compile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smallest RAM and flash footprint&lt;/td&gt;
&lt;td&gt;Rust — ~2 MB binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watermark burned into every output&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HLS playback or bilingual UI out of the box&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32-bit board&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What it costs to run
&lt;/h2&gt;

&lt;p&gt;Both builds are static binaries with a systemd unit in the repo. No Docker, no runtime, no language toolchain on the device.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Go build&lt;/th&gt;
&lt;th&gt;Rust build&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binary size&lt;/td&gt;
&lt;td&gt;~15 MB&lt;/td&gt;
&lt;td&gt;~2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;15–25 MB (+~15 MB with the AI build)&lt;/td&gt;
&lt;td&gt;~6–12 MB bare pipeline; 93.8 MB measured full-feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subprocesses&lt;/td&gt;
&lt;td&gt;mtxrpicam or ffmpeg&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU, 720p@15fps&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;td&gt;124–146% measured full-feature (~139% of four cores)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read those conditions before drawing conclusions, because they're not the same measurement.&lt;/p&gt;

&lt;p&gt;The Rust figures come from a run on 2026-09-14 on a Raspberry Pi 3B: 720p at 15fps, hardware M2M encoder, GB28181 registered, local recording on, OSD watermark on, per-frame INFO logging enabled, one RTSP-over-TCP client, 60 seconds, 13 samples. RSS held flat at 93.8 MB. A bare pipeline — no GB28181, no recording, no watermark — sits far lower, which is where the ~6 MB figure in the repo description comes from. Those two numbers are different measurement scopes, and I'd rather show you both than quote the flattering one.&lt;/p&gt;

&lt;p&gt;The Go CPU figure comes from the v0.1.0 release notes, also measured on a Pi 3B. Different scope again, so I'm not going to declare a winner. Both repos ship &lt;code&gt;bench/rpi-bench.sh&lt;/code&gt; if you want your own numbers on your own board.&lt;/p&gt;

&lt;p&gt;For context, there's a Pi 3B — a 2016 board with 1 GB of RAM — holding up a 720p stream with recording and a standards-based registration on top. That's the repurposing argument in one line.&lt;/p&gt;

&lt;p&gt;The repo README also carries a comparison against MediaMTX on memory and CPU for the camera-side role. MediaMTX is a media server and a good one, and it isn't trying to be a camera; the comparison only covers turning a Pi plus a sensor into a device an NVR can discover, and the numbers are indicative measurements from my own deployments rather than a controlled benchmark.&lt;/p&gt;

&lt;p&gt;Onboards without a V4L2 M2M encoder node — x86 boxes, NAS units, most ARM SBCs — the Rust build falls back to an in-process openh264 software encoder, and the Go build to a resident ffmpeg subprocess. That's why "any Linux board" is more than a tagline, and it's also why the subprocess column is worth reading before you pick. Note the CPU budget on weak boards: 720p@15 is the sane ceiling on A53-class silicon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that were hard
&lt;/h2&gt;

&lt;p&gt;The most useful file in the Rust repo isn't code. It's a 386-line interop notebook documenting ten real bugs found while getting a gortsplib-based NVR to actually stay connected. Four of them explain why cheap RTSP implementations fail where ffmpeg succeeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A duplicated &lt;code&gt;Content-Length&lt;/code&gt; header.&lt;/strong&gt; The RTSP DESCRIBE response had the header twice — added once by the response constructor and once by the handler. ffmpeg tolerates it; the NVR's parser rejected the whole response and refused to connect. Strict parsers catch what lenient ones hide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial frames are worse than missing frames.&lt;/strong&gt; One encoded frame usually becomes several RTP packets. Under TCP interleaved mode, sending them individually meant a timeout mid-frame delivered half an FU-A fragment, which a decoder can't reconstruct. Fix: batch every packet of an access unit into a single &lt;code&gt;write_all&lt;/code&gt;, so the frame arrives whole or not at all. Then rewind the RTP sequence numbers for the packets you didn't send, because a gap in the sequence makes a strict NVR declare packet loss. Then drop subsequent P-frames until the next keyframe, because those reference the frame you just threw away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestamps must be strictly monotonic.&lt;/strong&gt; RTP timestamps computed at send time rather than capture time produced duplicates whenever frames came out of the encoder in a burst. ffmpeg reported non-monotonically increasing DTS and the NVR dropped frames. The current implementation takes the capture timestamp and enforces a minimum increment of one frame tick between consecutive packets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pi undervoltage corrupts H.264.&lt;/strong&gt; An ageing Pi 3B with marginal power delivery drops voltage under encoder load, the firmware caps the clock, and the transition produces corrupted NAL units. &lt;code&gt;vcgencmd get_throttled&lt;/code&gt; returned &lt;code&gt;0x50005&lt;/code&gt; — undervoltage happening now plus a frequency cap. The encoder thread now watches firmware throttle state and skips every other frame while throttled, which halves the load, lets the clock recover, and self-corrects. After that change, a 60-second decode test reported zero errors.&lt;/p&gt;

&lt;p&gt;One frontend fix in the same notebook: Chrome 120+ no longer renders &lt;code&gt;multipart/x-mixed-replace&lt;/code&gt; inside an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag, so the MJPEG preview went black. Replacing it with H.264 over WebSocket plus Media Source Extensions fixed it, and reworking the buffering brought preview latency from 3–9 seconds down to under a second.&lt;/p&gt;

&lt;p&gt;Code and test volume, measured 2026-09-15: the Go repo is 93 &lt;code&gt;.go&lt;/code&gt; files and 22,022 lines with 287 test functions across 34 test files; the Rust repo is 63 first-party &lt;code&gt;.rs&lt;/code&gt; files and 25,387 lines with 469 test functions. Documentation is 7 markdown files in Go and 13 in Rust, four of them design documents for unbuilt features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol layer is four libraries you can use on their own
&lt;/h2&gt;

&lt;p&gt;None of those fixes live inside the camera service. The ONVIF and GB28181 logic in both builds is extracted into four standalone libraries — one protocol, two languages — and all four are MIT licensed, which is a shorter licence than the Apache-2.0 the camera service itself carries. Versions and counts in this section were pulled on 2026-09-15.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    P["Your product"] --&amp;gt; O["ONVIF side"]
    P --&amp;gt; G["GB28181 side"]
    O --&amp;gt; OG["onvif-go&amp;lt;br/&amp;gt;client + server"]
    O --&amp;gt; OR["onvif-rs&amp;lt;br/&amp;gt;device server"]
    G --&amp;gt; GG["gb28181-go&amp;lt;br/&amp;gt;UAC, UAS, cascade"]
    G --&amp;gt; GR["gb28181-rs&amp;lt;br/&amp;gt;UAC"]&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Latest tag&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;onvif-go&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;v2.0.0-rc6&lt;/td&gt;
&lt;td&gt;ONVIF client + virtual camera server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onvif-rs&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;v0.6.0&lt;/td&gt;
&lt;td&gt;ONVIF device-side server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gb28181-go&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;v0.10.0&lt;/td&gt;
&lt;td&gt;GB28181 device + platform + cascade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gb28181-rs&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;v0.11.0&lt;/td&gt;
&lt;td&gt;GB28181 device side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;onvif-go&lt;/strong&gt; works from both ends: a client you can point at someone else's camera, and a virtual camera server for testing against. Its &lt;code&gt;go.mod&lt;/code&gt; is a module line and a Go version — zero third-party dependencies, which you can verify by opening one file. It ships prebuilt CLI tools for discovery, diagnostics, a quick-start helper and a server simulator, which is what you want when there's no hardware on the bench. Device interop is exercised against Hikvision, Dahua, Axis, Bosch and ESP32 hardware. v2 was a breaking refactor from v1, and the repo carries a MIGRATION.md.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;onvif-rs&lt;/strong&gt; is the device-side server, published on crates.io as &lt;code&gt;onvif-device-rs&lt;/code&gt; because the &lt;code&gt;onvif-rs&lt;/code&gt; name is held by an abandoned 2018 placeholder. Its response XML is byte-stable, pinned by golden-string tests, which is the property strict NVR parsers actually need. Production code contains no &lt;code&gt;unwrap&lt;/code&gt; or &lt;code&gt;expect&lt;/code&gt; and CI enforces that; auth has a UsernameToken replay guard and a lockout, and handler panics are isolated so one bad request can't take the server down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gb28181-go&lt;/strong&gt; covers the most ground: device (UAC), platform (UAS) and multi-level cascade. The cascade path has loop prevention — each channel carries an &lt;code&gt;OriginDeviceID&lt;/code&gt;, so a middle platform never hands an upper platform its own channels back, and their INVITEs get a 404. The GB/T 28181-2022 alignment lives here too: five new information queries pinned to the standard's annexes, opt-in &lt;code&gt;X-GB-Ver&lt;/code&gt; version negotiation, and a wire-format bug found by reading the spec text rather than a peer implementation (SVAC audio &lt;code&gt;stream_type&lt;/code&gt; corrected from 0x81 to 0x9B).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gb28181-rs&lt;/strong&gt; is the device side of the same thing, sharing golden wire-format contracts with its Go twin. It's what gives the Rust camera build its GB28181 capability.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;onvif-go&lt;/td&gt;
&lt;td&gt;723 (693 tests + 25 benchmarks + 5 fuzz targets)&lt;/td&gt;
&lt;td&gt;177 &lt;code&gt;.go&lt;/code&gt; files, 55,284 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gb28181-go&lt;/td&gt;
&lt;td&gt;574 (563 + 7 + 4)&lt;/td&gt;
&lt;td&gt;161 &lt;code&gt;.go&lt;/code&gt; files, 39,695 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onvif-rs&lt;/td&gt;
&lt;td&gt;174&lt;/td&gt;
&lt;td&gt;21 &lt;code&gt;.rs&lt;/code&gt; files, 8,922 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gb28181-rs&lt;/td&gt;
&lt;td&gt;218&lt;/td&gt;
&lt;td&gt;40 &lt;code&gt;.rs&lt;/code&gt; files, 16,876 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's 1,689 tests across the four. The Go line counts include their test files: 88 files and 27,156 lines in onvif-go, 89 files and 21,114 lines in gb28181-go. The Rust crates keep test modules inline as well as under &lt;code&gt;tests/&lt;/code&gt;. Both declare MSRV 1.80; the GB 35114 feature needs 1.85 or newer.&lt;/p&gt;

&lt;p&gt;If you're building an NVR, a platform, or a device simulator, you can take the library without taking the camera service.&lt;/p&gt;

&lt;p&gt;What you should know before depending on them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;onvif-go is still a release candidate.&lt;/strong&gt; v2.0.0-rc6, and v1 to v2 was a breaking refactor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;onvif-rs covers an NVR-pairing action set.&lt;/strong&gt; Its own release notes say no GetSnapshotUri and no Events, and that compatibility with third-party clients beyond NVR-style consumers is untested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gb28181-rs is device-side only&lt;/strong&gt;, and the Go platform side doesn't implement the 2022 HTTP media plane for recorded-video search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither GB28181 library implements subscriptions, alarm notifications, mobile-position reporting or PTZ command intake yet.&lt;/strong&gt; That's the v0.3.0 scope, and it lands in the libraries first while the camera service only wires to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All four are new.&lt;/strong&gt; onvif-go was created in July 2026, the other three in August 2026, with between 0 and 4 stars. I'm not going to make promises about maintenance cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it doesn't do yet
&lt;/h2&gt;

&lt;p&gt;Worth reading before you plan around it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Profile S only.&lt;/strong&gt; No Media2, no Profile T, no Recording service, no Analytics. If your NVR needs server-side recording control or two-way audio, this isn't the device for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No H.265, no WebRTC, no multi-camera.&lt;/strong&gt; All three have design documents in the Rust repo and empty feature flags in the code. You can enable them and get a log line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motion detection is half-landed.&lt;/strong&gt; The Rust build implements a frame-delta detector in &lt;code&gt;src/motion/&lt;/code&gt; but it isn't wired into the web API yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI needs firmware-side setup.&lt;/strong&gt; The ONNX runtime library and the model aren't in the release archive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No 32-bit artifact from the Rust repo at v0.2.0.&lt;/strong&gt; An upstream width bug blocked it; it's fixed upstream and lands with v0.3.0. The Go repo already ships armv7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's new.&lt;/strong&gt; Both repos were created on 2026-09-12 and have one contributor. I'm not going to make claims about response times or maintenance cadence, and you shouldn't build a procurement decision on them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GB 35114 only goes to A-level&lt;/strong&gt;, and I have no third-party test report to point at.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frozen v0.3.0 scope is a complete GB/T 28181-2022 device-side capability package, published as a synchronized release across both implementations. Three things are explicitly out of scope and will stay that way: SVAC video and GB 35114 B/C levels, which would need SVAC hardware codecs; SIP over WebSocket, which isn't a device-side requirement in practice; and a platform-side (UAS) role, because the product is a device. The 2022 HTTP media plane for recorded-video search is deferred to v0.4.x rather than squeezed in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Go build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xiqing85/mibee-eye-go
&lt;span class="nb"&gt;cd &lt;/span&gt;mibee-eye-go
make build

&lt;span class="nb"&gt;cp &lt;/span&gt;configs/config.example.yaml config.yaml
&lt;span class="c"&gt;# set camera parameters and onvif.username / onvif.password&lt;/span&gt;

&lt;span class="nb"&gt;sudo cp &lt;/span&gt;deploy/mibee-eye.service /etc/systemd/system/
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; mibee-eye
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rust build, cross-compiling from an x86 workstation to a Pi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/xiqing85/mibee-eye-rs
&lt;span class="nb"&gt;cd &lt;/span&gt;mibee-eye-rs

&lt;span class="c"&gt;# fully static, no external toolchain needed&lt;/span&gt;
rustup target add aarch64-unknown-linux-musl
make cross-build

./deploy/install.sh pi@192.168.1.100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your NVR at it. WS-Discovery should surface it on the local network; the manual address is &lt;code&gt;http://&amp;lt;board-ip&amp;gt;:8080/onvif/device_service&lt;/code&gt; and the stream is &lt;code&gt;rtsp://&amp;lt;board-ip&amp;gt;:8554/stream&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two config defaults worth changing before you expose anything: the ONVIF password is empty by default, and &lt;code&gt;gb28181.enabled&lt;/code&gt; is off unless you turn it on. Environment variables with the &lt;code&gt;MIBEE_EYE_&lt;/code&gt; prefix override any config key, which is handy for injecting secrets from systemd rather than writing them into a file.&lt;/p&gt;

&lt;p&gt;One caveat on integrations: the ONVIF surface is the standard one, and the only NVR I have deep interop notes for is a gortsplib-based one. Synology Surveillance Station, Blue Iris and Scrypted consume standard ONVIF Profile S devices, and there's nothing custom here, but I'm not going to claim tested-and-verified for hardware I haven't run. If you try one of them and it misbehaves, that's exactly the kind of bug report the interop notebook exists to collect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project links
&lt;/h2&gt;

&lt;p&gt;mibee-eye-go: &lt;a href="https://github.com/xiqing85/mibee-eye-go" rel="noopener noreferrer"&gt;https://github.com/xiqing85/mibee-eye-go&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;mibee-eye-rs: &lt;a href="https://github.com/xiqing85/mibee-eye-rs" rel="noopener noreferrer"&gt;https://github.com/xiqing85/mibee-eye-rs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The protocol libraries underneath:&lt;/p&gt;

&lt;p&gt;onvif-go: &lt;a href="https://github.com/mickeyzzc/onvif-go" rel="noopener noreferrer"&gt;https://github.com/mickeyzzc/onvif-go&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;onvif-rs: &lt;a href="https://github.com/mickeyzzc/onvif-rs" rel="noopener noreferrer"&gt;https://github.com/mickeyzzc/onvif-rs&lt;/a&gt; (published on crates.io as &lt;code&gt;onvif-device-rs&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;gb28181-go: &lt;a href="https://github.com/mickeyzzc/gb28181-go" rel="noopener noreferrer"&gt;https://github.com/mickeyzzc/gb28181-go&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;gb28181-rs: &lt;a href="https://github.com/mickeyzzc/gb28181-rs" rel="noopener noreferrer"&gt;https://github.com/mickeyzzc/gb28181-rs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Licence: Apache-2.0 for both camera builds, MIT for the four libraries. Note that v0.1.0 shipped under CC BY-NC 4.0, which forbids commercial use, so take v0.2.0 or later; third-party terms are listed in NOTICE.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>linux</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What's new in MiBee NVR v0.11.0: WHIP ingest, mDNS discovery, and browser-side AI on a Pi</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:05:32 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/whats-new-in-mibee-nvr-v0110-whip-ingest-mdns-discovery-and-browser-side-ai-on-a-pi-31dj</link>
      <guid>https://dev.to/mickey_zzc/whats-new-in-mibee-nvr-v0110-whip-ingest-mdns-discovery-and-browser-side-ai-on-a-pi-31dj</guid>
      <description>&lt;p&gt;Home security cameras have a dirty secret: the software is the weak link. You buy a decent ONVIF camera and then you're locked into the vendor's app, or you sign up for a cloud service that bills per camera, or you stand up an NVR that wants a dedicated machine with a Coral TPU before it does anything interesting.&lt;/p&gt;

&lt;p&gt;I wanted the opposite. One binary I could drop on a Raspberry Pi, a web UI that doesn't feel like 2005, cameras that get discovered instead of configured, and no monthly fee. That project became MiBee NVR. It runs on a 512MB memory budget and the README's claim, which I stand by, is that a Pi 3B is the minimum baseline. The repo is at &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr" rel="noopener noreferrer"&gt;github.com/Mi-Bee-Studio/MiBeeNvr&lt;/a&gt;, current version 0.11.0.&lt;/p&gt;

&lt;p&gt;The pitch in one line: single Go binary, zero dependencies (&lt;code&gt;CGO_ENABLED=0&lt;/code&gt;), an embedded Svelte 5 SPA, no cloud, no subscriptions. Install is one &lt;code&gt;curl&lt;/code&gt; piped to &lt;code&gt;bash&lt;/code&gt;, or a prebuilt binary for amd64, arm64, or armv7, or Docker from &lt;code&gt;ghcr.io/mi-bee-studio/mibeenvr&lt;/code&gt;. The UI runs on port 9090 and comes with dark/light themes and English/Chinese out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  ONVIF that behaves
&lt;/h2&gt;

&lt;p&gt;The thing I'm proudest of is the ONVIF handling, because it fixes the pain I kept hitting with real cameras. Cameras on WiFi roam between access points and come back with a new IP. Your NVR still has the old one stored, the feed dies, and you go digging through the router's DHCP table to fix it. MiBee NVR instead enrolls ONVIF cameras automatically in the background when they join the LAN, the way a Hikvision NVR does. No manual scan.&lt;/p&gt;

&lt;p&gt;Two details I like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unauthenticated cameras start recording immediately. If it's open, it's in.&lt;/li&gt;
&lt;li&gt;Authenticated cameras are added as pending until you supply credentials.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the IP self-healing: when a camera changes IP, the NVR relocates it by serial number and reconnects. The unicast probing works across routed subnets, so it isn't limited to one broadcast domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser-side AI, no Coral required
&lt;/h2&gt;

&lt;p&gt;Most NVR setups push detection to a separate box, often a USB Coral or an Nvidia card. MiBee NVR runs inference in the browser tab instead. ONNX Runtime Web with WebGPU acceleration, a 5.4MB &lt;code&gt;yolo11n&lt;/code&gt; model by default (squeezenet is there as a fallback). The backend does zero inference, so the Pi never breaks a sweat, and the video never leaves the device to get "analyzed" somewhere else. If your browser supports WebGPU you get object detection; if it doesn't, you still get a fully working NVR.&lt;/p&gt;

&lt;p&gt;One real constraint worth knowing: without &lt;code&gt;crossOriginIsolated&lt;/code&gt; there's no &lt;code&gt;SharedArrayBuffer&lt;/code&gt;, so the runtime is single-threaded. Fine for one camera, something to remember if you plan to run several detections at once.&lt;/p&gt;

&lt;p&gt;The war story: model files kept coming back corrupted and ORT threw &lt;code&gt;INVALID_PROTOBUF&lt;/code&gt;. Root cause was our own gzip middleware. Calling &lt;code&gt;Close()&lt;/code&gt; on a &lt;code&gt;gzip.Writer&lt;/code&gt; that never wrote anything appends an empty gzip member, about 20 bytes, to the end of binary responses. The ONNX file was fine except for those 20 bytes. The fix tracks whether the writer ever wrote and only calls &lt;code&gt;Close()&lt;/code&gt;/&lt;code&gt;Flush()&lt;/code&gt; after the first &lt;code&gt;Write()&lt;/code&gt;. That's issue #109 if you want the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every protocol, in and out
&lt;/h2&gt;

&lt;p&gt;NVR is a messy word because it covers two directions: getting streams in and getting streams out.&lt;/p&gt;

&lt;p&gt;Inbound, MiBee NVR speaks RTSP (H.264, H.265, MJPEG), HTTP JPEG snapshots, RTMP ingest, SRT ingest, and WHIP, which is WebRTC push. The WHIP support is the headline feature of v0.11.0 and it's fun: you can publish from a browser tab or OBS with a stream key mapped to a camera, no extra software.&lt;/p&gt;

&lt;p&gt;Outbound to your browser: WebRTC via WHEP for sub-second latency, HLS with LL-HLS for on-demand, HTTP-FLV, and raw WebSocket frames. If you need to push somewhere else, there's a native Go relay that forwards any camera to a remote RTMP or RTSP target. Point it at a streaming platform and you have a cheap live production pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording, the part people forget
&lt;/h2&gt;

&lt;p&gt;An NVR that can't scrub is a paperweight. Recordings are MP4 segments with per-camera retention policies, audio capture (AAC, G.711, Opus), and a timelapse mode. Playback chains segments with double buffering so there's no gap when you're watching back, and the VOD timeline covers a full day, so you can drag across recordings and the gaps between them. There's also an AVI frame-browse mode when you need to step through frame by frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for the homelab, not around it
&lt;/h2&gt;

&lt;p&gt;This was a design goal, so the integration list is deliberately boring: MQTT for trigger-based recording (Home Assistant people know what to do), WebDAV and FTP for pulling files off the box, a Prometheus metrics endpoint with full docs, a REST API with BasicAuth and bcrypt, and a stable &lt;code&gt;device_id&lt;/code&gt; in &lt;code&gt;/api/health&lt;/code&gt; so your automation has something to hold onto. Discovery is mDNS (&lt;code&gt;_mibee-nvr._tcp&lt;/code&gt;) with a UDP responder as fallback for networks that block multicast.&lt;/p&gt;

&lt;p&gt;Remote access is your choice: Tailscale, Cloudflare Tunnel, or WebRTC ICE/STUN/TURN configuration. The docs cover all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment story
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One-liner: &lt;code&gt;curl -fsSL https://raw.githubusercontent.com/Mi-Bee-Studio/MiBeeNvr/main/install.sh | sudo bash&lt;/code&gt;, which creates a systemd service, a system user, and a config.&lt;/li&gt;
&lt;li&gt;Binaries for amd64, arm64, and armv7 from GitHub Releases.&lt;/li&gt;
&lt;li&gt;Docker images on GHCR with exact, minor, major, and &lt;code&gt;latest&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;NAS guides for unRAID, Synology, and QNAP, plus fnOS, iStoreOS, and 极空间 if you're in the Chinese NAS ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The China-market side, in one paragraph
&lt;/h2&gt;

&lt;p&gt;MiBee NVR also speaks GB/T 28181, the Chinese national standard for video surveillance, as an experimental SIP platform, and it can adopt Xiaomi cloud cameras (CS2 and legacy TUTK models). If you're not in China, ignore both of those; they exist because the home market runs on them. Everything else in this post is the same code path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering hygiene and licensing
&lt;/h2&gt;

&lt;p&gt;The repo has 3991 Go tests, 389 frontend tests, and 13 Playwright E2E tests, with golangci-lint v2 (gofumpt, perfsprint) enforced in CI. One honest note on licensing: versions up to v0.10.1 are MIT. From v0.11.0 the project moved to AGPL-3.0-only, with a linking exception for &lt;code&gt;pkg/&lt;/code&gt; so you can build against the extension interfaces without triggering AGPL obligations. Running it, recording cameras, watching streams: no obligations, commercial use included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Mi-Bee-Studio/MiBeeNvr/main/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or grab a binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/Mi-Bee-Studio/MiBeeNvr/releases/latest/download/mibee-nvr-arm64
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x mibee-nvr-arm64
./mibee-nvr-arm64 init &lt;span class="nt"&gt;--password&lt;/span&gt; yourpassword
./mibee-nvr-arm64 &lt;span class="nt"&gt;-config&lt;/span&gt; mibee-nvr.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://localhost:9090&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's documentation for everything mentioned here: getting started, a full configuration reference, the REST API, an ONVIF guide, transcoding, metrics, and per-NAS deployment. The architecture doc is a good place to start if you want to read the code with a map.&lt;/p&gt;

&lt;p&gt;If you run it and something breaks, the issues tracker is open. I read everything, and the WHIP ingest that just landed came straight out of user requests.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>opensource</category>
      <category>raspberrypi</category>
      <category>software</category>
    </item>
    <item>
      <title>Your Cameras Can Now Go Live — MiBeeNVR v0.8.1 Brings Native Live Streaming</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Fri, 03 Jul 2026 06:05:11 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/your-cameras-can-now-go-live-mibeenvr-v081-brings-native-live-streaming-7k1</link>
      <guid>https://dev.to/mickey_zzc/your-cameras-can-now-go-live-mibeenvr-v081-brings-native-live-streaming-7k1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhw0ox3ga8j2qnzsmnkm0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhw0ox3ga8j2qnzsmnkm0.png" alt=" " width="315" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Above: Live stream viewed on the mobile app. Relay handled natively by MiBeeNVR's Go implementation — no FFmpeg dependency.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MiBeeNVR v0.8.1 supports pushing any connected camera feed directly to live streaming platforms via RTMP.&lt;/strong&gt; Pure Go implementation — no FFmpeg installation, no external processes. Configure your stream URL and go live.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ 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 &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Runs on Raspberry Pi 3B and above. Supports both H.264 and H.265 cameras.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is MiBeeNVR?
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-camera management and recording (RTSP / ONVIF / Xiaomi dual-camera)&lt;/li&gt;
&lt;li&gt;H.264/H.265 hardware transcoding&lt;/li&gt;
&lt;li&gt;Time-lapse recording and synthesis&lt;/li&gt;
&lt;li&gt;LL-HLS / WebRTC low-latency playback&lt;/li&gt;
&lt;li&gt;One-command Docker deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;One thing was missing, though: live streaming.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Live Streaming: Looks Simple, Hurts Plenty
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Make It Work First
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkimk8wzsphnvrbfdd5aq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkimk8wzsphnvrbfdd5aq.png" alt=" " width="800" height="738"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But here's the problem: &lt;strong&gt;FFmpeg is an external process.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;What we wanted: &lt;strong&gt;a pure Go RTMP streaming implementation, embedded in the NVR process, with zero external dependencies.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Six Layers of Hell in Pure Go Streaming
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;We didn't encounter one problem — we encountered &lt;strong&gt;six stacked problems&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A["L1 Handshake digest&amp;lt;br/&amp;gt;HMAC-SHA256 signing"] --&amp;gt; B["L2 Chunk size&amp;lt;br/&amp;gt;mismatched block size"]
    B --&amp;gt; C["L3 Chunk header&amp;lt;br/&amp;gt;Type 0/1/2/3 format"]
    C --&amp;gt; D["L4 Background read&amp;lt;br/&amp;gt;buffer overflow"]
    D --&amp;gt; E["L5 Metadata fields&amp;lt;br/&amp;gt;missing width/height/fps"]
    E --&amp;gt; F["L6 StreamID byte order&amp;lt;br/&amp;gt;big-endian vs little-endian ⭐"]
    F --&amp;gt; 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&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;After fixing the first four layers — handshake passing, connection established, publish command accepted — &lt;strong&gt;the first video frame would be sent, and 74 milliseconds later, the connection would be reset by the server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That 74ms is critical: it rules out a network timeout (those take ~5 seconds). The server was rejecting the data at the frame level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Boss: A Tale of Byte Order
&lt;/h3&gt;

&lt;p&gt;The root cause was buried so deep that it took us a full day to find.&lt;/p&gt;

&lt;p&gt;The RTMP protocol has a field called &lt;code&gt;MessageStreamID&lt;/code&gt; that identifies which stream a message belongs to. Per the RTMP spec, this field should be &lt;strong&gt;little-endian&lt;/strong&gt;. So we dutifully wrote it as little-endian in our code, exactly as the spec says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But every major implementation — FFmpeg, OBS, gortmplib — uses big-endian.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And so do the live streaming platforms.&lt;/p&gt;

&lt;p&gt;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. &lt;strong&gt;On the same connection — sometimes big-endian, sometimes little-endian. The server received this schizophrenic packet stream and immediately sent an RST.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Go standard Writer&amp;lt;br/&amp;gt;streamID = big-endian"] --&amp;gt; C["Live platform&amp;lt;br/&amp;gt;expects big-endian ✅"]
    B["Our Writer&amp;lt;br/&amp;gt;streamID = little-endian"] --&amp;gt; D["Live platform&amp;lt;br/&amp;gt;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&lt;/code&gt;&lt;/pre&gt;



&lt;blockquote&gt;
&lt;p&gt;TL;DR: &lt;strong&gt;The spec says go left. The entire world goes right. You follow the spec — and the entire world rejects you.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;With all six layers fixed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Pure Go relay streaming works — no FFmpeg dependency&lt;/li&gt;
&lt;li&gt;✅ TCP connection stays stable, continuous streaming without interruption&lt;/li&gt;
&lt;li&gt;✅ Raspberry Pi camera feeds push to live streams in real-time, ~2-3 second latency&lt;/li&gt;
&lt;li&gt;✅ H.264 cameras supported; H.265 streaming coming soon&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Do not enable the &lt;code&gt;use_ffmpeg&lt;/code&gt; option.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;💡 The pure Go relay is the focus of this release. If you encounter Go streaming issues on other platforms, &lt;strong&gt;please prioritize filing an Issue about the Go path&lt;/strong&gt; to help the project keep improving — rather than falling back to FFmpeg.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How to Use It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: MiBeeNVR UI Configuration
&lt;/h3&gt;

&lt;p&gt;In the camera settings, find "Relay Output" and enter your live streaming platform's RTMP URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rtmp://&amp;lt;server&amp;gt;/live/&amp;lt;stream-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get the stream key from your live streaming platform's broadcast settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Docker Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; mibee-nvr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; mibee-nvr-data:/var/lib/mibee-nvr &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/mi-bee-studio/mibee-nvr:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://&amp;lt;device-ip&amp;gt;:8080&lt;/code&gt; to access the admin interface, add your cameras, then configure relay output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Status &amp;amp; Caveats
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Tested platform&lt;/strong&gt;: Douyu Live (companion app mode, LAN streaming) ✅&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theoretically compatible&lt;/strong&gt;: All FMS-standard live streaming backends. However, implementation details vary across platforms — untested ones may present issues. &lt;strong&gt;If you hit problems, please report them on &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;&lt;/strong&gt; and the author will follow up.&lt;/p&gt;

&lt;h4&gt;
  
  
  ⚠️ Important Notice
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Continuous long-term streaming to domestic live platforms is not recommended.&lt;/strong&gt; Each platform has its own streaming policies and restrictions. Prolonged unattended camera streaming may trigger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Throttling (reduced bitrate/resolution)&lt;/li&gt;
&lt;li&gt;Stream key expiration leading to disconnection&lt;/li&gt;
&lt;li&gt;Account warnings or even bans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure to understand your platform's policies and follow their rules. MiBeeNVR provides a technical capability — &lt;strong&gt;how you use it, on which platform, and what you stream is your own responsibility.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;This live streaming project left us with three key takeaways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Comparative Experimentation Is the Gold Standard for Debugging&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Binary Protocol Bugs Require Looking at Bytes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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. &lt;strong&gt;When debugging binary protocols: add a dump first, analyze second.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Spec ≠ Reality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The RTMP spec says &lt;code&gt;MessageStreamID&lt;/code&gt; 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 — &lt;strong&gt;de facto standards are more persuasive than paper specifications.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  About MiBeeNVR
&lt;/h2&gt;

&lt;p&gt;MiBeeNVR is an open-source project by &lt;a href="https://github.com/Mi-Bee-Studio" rel="noopener noreferrer"&gt;Mi-Bee-Studio&lt;/a&gt;, written in Go and optimized for embedded devices like the Raspberry Pi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr" rel="noopener noreferrer"&gt;https://github.com/Mi-Bee-Studio/MiBeeNvr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-camera management (RTSP / ONVIF / Xiaomi dual-camera)&lt;/li&gt;
&lt;li&gt;H.264/H.265 hardware transcoding (V4L2 / VAAPI / NVENC auto-detection)&lt;/li&gt;
&lt;li&gt;Time-lapse recording and synthesis (pure Go muxer, zero encoding overhead)&lt;/li&gt;
&lt;li&gt;LL-HLS / WebRTC low-latency playback&lt;/li&gt;
&lt;li&gt;RTMP live streaming (new in v0.8.1, pure Go)&lt;/li&gt;
&lt;li&gt;One-command Docker deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Supported Platforms&lt;/strong&gt;: Raspberry Pi 3B+ and above / x86_64 / ARM64 / Docker&lt;/p&gt;




&lt;p&gt;&lt;em&gt;MiBeeNVR v0.8.1 has been released. See the full changelog at &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr/releases" rel="noopener noreferrer"&gt;GitHub Release Notes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're building interesting projects with Raspberry Pi, come chat with us on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Embedded Camera Module Selection Handbook — From Basics to Purchasing Reference</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Mon, 29 Jun 2026 23:07:58 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/embedded-camera-module-selection-handbook-from-basics-to-purchasing-reference-3g1i</link>
      <guid>https://dev.to/mickey_zzc/embedded-camera-module-selection-handbook-from-basics-to-purchasing-reference-3g1i</guid>
      <description>&lt;h2&gt;
  
  
  About This Handbook
&lt;/h2&gt;

&lt;p&gt;This handbook is a revised edition of my own "Embedded Camera Module Technical Manual" that I previously compiled and shared online. The original covered eight mainstream sensors: OV2640, OV5640, OV7725, GC0309, OV7670, OV3660, GC2053, and GC4653. This revision expands coverage with 12 additional sensors (OV9281, OV5647, IMX219, IMX273, IMX296, IMX307, IMX335, IMX415, IMX477, IMX678, IMX708, IMX766), for a total of 20. The content was comprehensive, but when I sat down to use it as a purchasing reference, I felt uneasy — a single wrong voltage parameter can fry a board, and I didn't want to ship mistakes I couldn't vouch for.&lt;/p&gt;

&lt;p&gt;So I did one thing: &lt;strong&gt;verified every key parameter against official datasheets&lt;/strong&gt;. Sources include OmniVision's official product pages, GalaxyCore datasheets, SparkFun / DigiKey / Mouser datasheet mirrors, and Espressif's official esp32-camera driver source code.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;most parameters were correct, but there were several critical errors&lt;/strong&gt;, especially around voltage, frame rates, and some marketing terminology. This handbook is the corrected version — every error-prone parameter is annotated with reliability status and evidence. Use it as a purchasing checklist with confidence.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reliability annotation legend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Verified: Parameter matches official datasheet exactly&lt;/li&gt;
&lt;li&gt;⚠️ Note: Original had errors or was misleading; corrected with evidence&lt;/li&gt;
&lt;li&gt;🔍 Unconfirmed: Official datasheet unclear, or varies by module manufacturer&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Quick Selection Guide
&lt;/h2&gt;

&lt;p&gt;Start with this table to find your use case, then read the details below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Top Pick&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Key Advantage&lt;/th&gt;
&lt;th&gt;Price Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Education (Arduino/STM32)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OV7670&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.3MP&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;Richest ecosystem, most tutorials&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wi-Fi streaming (ESP32-CAM)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OV2640&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2MP&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;Built-in JPEG compression&lt;/td&gt;
&lt;td&gt;Low-mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-res photo + autofocus&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OV5640&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5MP&lt;/td&gt;
&lt;td&gt;DVP/MIPI&lt;/td&gt;
&lt;td&gt;Only entry-level with AF&lt;/td&gt;
&lt;td&gt;Mid-high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Night vision / machine vision&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OV7725&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.3MP&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;6.0µm pixels, low-light king&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra low cost (access/POS)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GC0309&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.3MP&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;Lowest price&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3MP upgrade (ESP32)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OV3660&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3MP&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;BSI tech, drop-in for OV2640&lt;/td&gt;
&lt;td&gt;Low-mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security / surveillance&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GC2053&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2MP&lt;/td&gt;
&lt;td&gt;DVP/MIPI&lt;/td&gt;
&lt;td&gt;1/2.9" sensor + 2.8µm pixels&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-end security / AI vision&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GC4653&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4MP&lt;/td&gt;
&lt;td&gt;MIPI only&lt;/td&gt;
&lt;td&gt;4MP + BSI high sensitivity&lt;/td&gt;
&lt;td&gt;Mid-high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raspberry Pi / Jetson entry&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;IMX219&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8MP&lt;/td&gt;
&lt;td&gt;MIPI 2L&lt;/td&gt;
&lt;td&gt;Pi V2 standard, most mature driver&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4K security / NVR&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;IMX415&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8MP&lt;/td&gt;
&lt;td&gt;MIPI 2/4L&lt;/td&gt;
&lt;td&gt;4K STARVIS, rich module ecosystem&lt;/td&gt;
&lt;td&gt;Mid-high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-end 4K security (2024)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;IMX678&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8MP&lt;/td&gt;
&lt;td&gt;MIPI 2/4/8L&lt;/td&gt;
&lt;td&gt;STARVIS 2 + Clear HDR&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1080P night security&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;IMX307&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2MP&lt;/td&gt;
&lt;td&gt;MIPI 2/4L&lt;/td&gt;
&lt;td&gt;2.9µm + STARVIS, strong night vision&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Industrial FA / global shutter&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;IMX273&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.58MP&lt;/td&gt;
&lt;td&gt;MIPI 1L&lt;/td&gt;
&lt;td&gt;Pregius global shutter, 226fps&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;ESP32 users note:&lt;/strong&gt; ESP32 series only supports DVP-interface sensors. &lt;strong&gt;GC4653 is MIPI-only and cannot be used with ESP32&lt;/strong&gt;. OV3660 also only supports DVP (claims of MIPI support in the original document are wrong — see below).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  DVP Interface Basics
&lt;/h2&gt;

&lt;p&gt;DVP (Digital Video Port) transmits uncompressed raw image data via parallel data lines with a sync clock. Understanding DVP signals and timing is prerequisite for wiring and debugging all DVP modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal Definitions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Typical Level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XCLK&lt;/td&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;External master clock, typically 6–24 MHz ✅&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PCLK&lt;/td&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Pixel clock for synchronous data sampling ✅&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSYNC&lt;/td&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Frame sync signal ✅&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HREF / HSYNC&lt;/td&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Line sync signal ✅&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D[7:0]&lt;/td&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;8-bit parallel pixel data bus ✅&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIOC / SCL&lt;/td&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;SCCB / I²C clock ✅&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIOD / SDA&lt;/td&gt;
&lt;td&gt;Bidirectional&lt;/td&gt;
&lt;td&gt;SCCB / I²C data ✅&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PWDN&lt;/td&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;Power-down control&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RESET&lt;/td&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;Hardware reset (active low)&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Timing
&lt;/h3&gt;

&lt;p&gt;A complete frame is bounded by VSYNC. Within a frame, multiple HREF pulses occur, each corresponding to one line of valid pixel data. When both VSYNC and HREF are active, D[7:0] data latched on PCLK rising edge is valid pixel data.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;
The red scan line sweeps left to right. When VSYNC + HREF are both HIGH, each PCLK rising edge latches one pixel byte (D0, D1, D2…).





&lt;h3&gt;
  
  
  SCCB Control Interface
&lt;/h3&gt;

&lt;p&gt;SCCB (Serial Camera Control Bus) is OmniVision's I²C-compatible 2-wire serial protocol. SCL clock frequency supports up to 400 kHz ✅ (OV2640 datasheet Table 9 confirms). If your MCU lacks internal pull-ups, add 4.7 kΩ pull-up resistors on SCL and SDA to 3.3V ✅.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;I²C addresses are sensor-specific, NOT uniform 0x30/0x60!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many online sources casually claim "OV series address is 0x30/0x60" — this is wrong. Verified 7-bit slave addresses per sensor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sensor&lt;/th&gt;
&lt;th&gt;7-bit Address&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OV2640&lt;/td&gt;
&lt;td&gt;0x30&lt;/td&gt;
&lt;td&gt;Datasheet: 8-bit write 0x60 / read 0x61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OV5640&lt;/td&gt;
&lt;td&gt;0x3C&lt;/td&gt;
&lt;td&gt;Datasheet: 8-bit write 0x78 / read 0x79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OV7725&lt;/td&gt;
&lt;td&gt;0x21&lt;/td&gt;
&lt;td&gt;Datasheet: 8-bit write 0x42 / read 0x43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OV7670&lt;/td&gt;
&lt;td&gt;0x21&lt;/td&gt;
&lt;td&gt;Linux kernel driver ov7670.c&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC series&lt;/td&gt;
&lt;td&gt;Vendor-defined&lt;/td&gt;
&lt;td&gt;Check specific datasheet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If SCCB configuration fails, &lt;strong&gt;verify I²C address first&lt;/strong&gt;, not just pull-ups.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  MIPI CSI-2 Interface Basics
&lt;/h2&gt;

&lt;p&gt;When resolution and frame rate go up (1080P@30fps+), DVP's 8-bit parallel bus can't keep up — too many wires, limited speed, poor noise immunity. All Sony IMX sensors below use MIPI CSI-2. Here's the foundation.&lt;/p&gt;

&lt;h3&gt;
  
  
  DVP vs MIPI CSI-2
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;DVP&lt;/th&gt;
&lt;th&gt;MIPI CSI-2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data lines&lt;/td&gt;
&lt;td&gt;8–10 parallel + sync signals&lt;/td&gt;
&lt;td&gt;1–4 differential pairs (lanes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock&lt;/td&gt;
&lt;td&gt;Single-ended XCLK / PCLK&lt;/td&gt;
&lt;td&gt;Differential clock lane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth&lt;/td&gt;
&lt;td&gt;≤ ~200 MB/s&lt;/td&gt;
&lt;td&gt;Up to ~2.5 GB/s (4-lane)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Noise immunity&lt;/td&gt;
&lt;td&gt;Poor, long cables cause artifacts&lt;/td&gt;
&lt;td&gt;Good, differential rejects common-mode noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller complexity&lt;/td&gt;
&lt;td&gt;Low (straight to FIFO)&lt;/td&gt;
&lt;td&gt;High (needs D-PHY + CSI-2 controller)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical controllers&lt;/td&gt;
&lt;td&gt;ESP32, STM32, RP2040&lt;/td&gt;
&lt;td&gt;Raspberry Pi, Jetson, Hi3516, RV1106&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical sensors&lt;/td&gt;
&lt;td&gt;OV2640, OV5640, OV7670&lt;/td&gt;
&lt;td&gt;IMX219, IMX415, IMX678&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Lane Count and Bandwidth
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lanes&lt;/th&gt;
&lt;th&gt;Total Bandwidth&lt;/th&gt;
&lt;th&gt;Suitable For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1-lane&lt;/td&gt;
&lt;td&gt;~500 MB/s&lt;/td&gt;
&lt;td&gt;1080P@30fps RAW8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-lane&lt;/td&gt;
&lt;td&gt;~1 GB/s&lt;/td&gt;
&lt;td&gt;1080P@60fps or 4K@15fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-lane&lt;/td&gt;
&lt;td&gt;~2 GB/s&lt;/td&gt;
&lt;td&gt;4K@30fps+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Purchasing note:&lt;/strong&gt; More lanes isn't always better — your controller's MIPI receiver must support the lane count. For example, RV1106 supports 2-lane/4-lane MIPI input; connecting a 1-lane IMX273 may not work. Check controller specs before buying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ESP32 users note:&lt;/strong&gt; ESP32 (including ESP32-S3) has &lt;strong&gt;no MIPI CSI-2 receiver&lt;/strong&gt;. None of the Sony IMX sensors below work with ESP32. You need a MIPI-capable controller like Raspberry Pi, Jetson, RV1106, Hi3516, etc.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Sensor Technology Principles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Shutter Types: Rolling vs Global
&lt;/h3&gt;

&lt;p&gt;Most sensors in this handbook use &lt;strong&gt;rolling shutter&lt;/strong&gt; — exposing rows sequentially with a time gap between rows. Fast-moving objects appear skewed/warped (&lt;strong&gt;jello effect&lt;/strong&gt;). Only IMX273, OV9281, and IMX296 use &lt;strong&gt;global shutter&lt;/strong&gt; — the entire frame is exposed simultaneously, zero motion distortion.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;
A blue ball moves left to right. Rolling shutter exposes row-by-row, capturing the ball at different positions per row → diagonal skew. Global shutter exposes all rows simultaneously → clean bar.





&lt;h3&gt;
  
  
  Backside Illumination (BSI) vs Frontside Illumination (FSI)
&lt;/h3&gt;

&lt;p&gt;Traditional CMOS sensors (FSI) have metal wiring above the photodiode, blocking some light. &lt;strong&gt;BSI (Backside Illumination)&lt;/strong&gt; flips the sensor so light enters from the back, reaching the photodiode directly — &lt;strong&gt;30%–60% more light&lt;/strong&gt;. STARVIS, Exmor R, and OmniBSI all use BSI technology.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;
Yellow dots are photons. FSI's wiring blocks ~50% of light. BSI flips the structure so all photons reach the sensor.





&lt;h3&gt;
  
  
  HDR Multi-Exposure Fusion
&lt;/h3&gt;

&lt;p&gt;In backlit scenes, short exposure captures highlights (sky) but shadows are black; long exposure captures shadows but highlights are blown. HDR merges multiple exposures into one image, &lt;strong&gt;preserving both highlight and shadow detail&lt;/strong&gt;. IMX307/IMX335/IMX678 all support hardware-level HDR.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
Short exposure preserves highlights, long exposure preserves shadows — merged HDR significantly expands dynamic range. Frames appear sequentially to show the fusion process.





&lt;h3&gt;
  
  
  Quad Bayer Pixel Binning
&lt;/h3&gt;

&lt;p&gt;High-resolution sensors (e.g., IMX766 50MP) have tiny pixels (1.0µm) with insufficient light per pixel. &lt;strong&gt;Quad Bayer&lt;/strong&gt; groups 4 adjacent same-color pixels, reading them as 1 larger pixel — 4× area, effective pixel size doubles (1.0µm → 2.0µm), dramatically improving low-light performance. Full resolution by day, auto-binned by night.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
4 pixels of 1.0µm merge into 1 pixel of 2.0µm. 4× area = 4× light = significantly better SNR.








&lt;h2&gt;
  
  
  OV2640 Module
&lt;/h2&gt;

&lt;p&gt;OmniVision 1/4-inch, 2MP CMOS sensor with built-in JPEG compression engine. The standard choice for ESP32-CAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;OmniVision OV2640&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;2MP (1600 × 1200)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.2 µm × 2.2 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter&lt;/td&gt;
&lt;td&gt;Electronic rolling&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;DVP 8-bit parallel + SCCB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;YUV422, YUV420, RGB565, RGB555, RAW RGB, JPEG&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;UXGA@15fps, SVGA@30fps, CIF@60fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master clock XCLK&lt;/td&gt;
&lt;td&gt;6 – 24 MHz (typical 24 MHz)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;Analog 2.5–3.0V, I/O 1.7–3.3V, Core 1.2/1.3V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;~125 mW (15fps UXGA YUV), ~140 mW (JPEG mode)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Official OmniVision OV2640 datasheet (uctronics.com/download/OV2640_DS.pdf)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Resolution &amp;amp; Frame Rate
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Typical fps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UXGA&lt;/td&gt;
&lt;td&gt;1600 × 1200&lt;/td&gt;
&lt;td&gt;15 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SXGA&lt;/td&gt;
&lt;td&gt;1280 × 1024&lt;/td&gt;
&lt;td&gt;15 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVGA&lt;/td&gt;
&lt;td&gt;800 × 600&lt;/td&gt;
&lt;td&gt;30 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VGA&lt;/td&gt;
&lt;td&gt;640 × 480&lt;/td&gt;
&lt;td&gt;30 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QVGA&lt;/td&gt;
&lt;td&gt;320 × 240&lt;/td&gt;
&lt;td&gt;60 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Typical Pinout (24-PIN FPC)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ Verified: OV2640 modules do use 24-pin 0.5mm pitch FPC (confirmed by OV2640FSL datasheet, Arducam module spec). However, &lt;strong&gt;pin order may vary by module manufacturer&lt;/strong&gt; — always check your specific board's datasheet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pin&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Pin&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;D3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;VCC (3.3V)&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;D4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;D5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;PWDN&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;D6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;RESET&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;D7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;XCLK&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;NC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;VSYNC&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;SIOC (SCL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;HREF&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;SIOD (SDA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;PCLK&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;NC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;D0&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;NC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;D1&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;NC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;D2&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;NC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Purchasing tip:&lt;/strong&gt; OV2640's built-in JPEG compression outputs compressed streams directly, significantly reducing MCU RAM and bus bandwidth requirements. Best price/performance for ESP32-CAM.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  OV5640 Module
&lt;/h2&gt;

&lt;p&gt;OmniVision 1/4-inch, 5MP CMOS sensor with integrated autofocus (AF) voice coil motor driver. The image quality ceiling for entry-level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;OmniVision OV5640&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;5MP (2592 × 1944)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.4 µm × 1.4 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter&lt;/td&gt;
&lt;td&gt;Electronic rolling&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;DVP 8/10-bit OR MIPI CSI-2 (2-lane)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;RAW RGB, RGB565/555/444, YCbCr422, JPEG&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;QSXGA@15fps, 1080P@30fps, 720P@60fps, VGA@90fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master clock XCLK&lt;/td&gt;
&lt;td&gt;6 – 27 MHz (typical 24 MHz)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autofocus&lt;/td&gt;
&lt;td&gt;Integrated VCM driver, contrast-detect AF&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Features&lt;/td&gt;
&lt;td&gt;OmniBSI, HDR, AE/AG/AWB, 2D NR, lens correction, anti-shake&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current&lt;/td&gt;
&lt;td&gt;~140 mA (active), 20 µA (standby)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: OV5640 datasheet v2.03 (cdn.sparkfun.com), OV5640 Product Brief (media.digikey.com)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interface note:&lt;/strong&gt; OV5640 supports both DVP and MIPI CSI-2. Cheap modules typically use DVP 8-bit parallel. MIPI versions are usually for phone/tablet designs. &lt;strong&gt;For AF versions, verify AF_VCC supply&lt;/strong&gt; — the VCM motor needs separate power.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  OV7725 Module
&lt;/h2&gt;

&lt;p&gt;OmniVision high-performance 1/4-inch VGA sensor using OmniPixel² technology, focused on high sensitivity, low noise, and low-light performance. At 6.0 µm pixel size, it's the strongest low-light performer in the 0.3MP class.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;OmniVision OV7725&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;0.3MP (640 × 480)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.0 µm × 6.0 µm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;DVP 8/10-bit parallel + SCCB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;RAW RGB, RGB565/555/444, YCbCr422&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;VGA@60fps, QVGA@120fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating temp&lt;/td&gt;
&lt;td&gt;-20°C to +70°C&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensitivity&lt;/td&gt;
&lt;td&gt;3800 mV/Lux-sec (extremely high)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic range&lt;/td&gt;
&lt;td&gt;60 dB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Features&lt;/td&gt;
&lt;td&gt;OmniPixel², high sensitivity low-light, AE/AG/AWB, edge enhancement, noise suppression&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: OV7725 official product page (ovt.com/products/ov7725), OV7725 datasheet&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;EOL notice:&lt;/strong&gt; OV7725 is marked End-of-Life on OmniVision's website. For new production designs, evaluate alternatives. But surplus/secondhand stock remains plentiful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is 6.0µm so big?&lt;/strong&gt; Both OV7670 and OV7725 are 0.3MP, but OV7670 pixels are only 3.6µm while OV7725 reaches 6.0µm — nearly 3x the pixel area, meaning 3x the light gathering. This is why it crushes same-resolution competitors in low light. For night vision, license plate recognition, or industrial inspection, this is the one.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  GC0309-8225N V2 Module
&lt;/h2&gt;

&lt;p&gt;Based on GalaxyCore GC0309, a 0.3MP fixed-focus module with DVP interface, focused on ultra-high cost-effectiveness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;GalaxyCore GC0309&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/9 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;0.3MP (640 × 480, actual 648×488)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.5 µm × 2.5 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel structure&lt;/td&gt;
&lt;td&gt;4-transistor (4T), low noise&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter&lt;/td&gt;
&lt;td&gt;Electronic rolling&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;DVP 8-bit parallel&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;YCbCr422, RGB565, RAW Bayer&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;VGA@30fps (24MHz clock)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.8V typical (2.7–3.3V range)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lens&lt;/td&gt;
&lt;td&gt;Fixed focus, typical 62° FOV&lt;/td&gt;
&lt;td&gt;🔍 module-vendor defined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Features&lt;/td&gt;
&lt;td&gt;On-chip 10-bit ADC, embedded ISP, AE, AWB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: GC0309 official datasheet V1.0 (2009-12-28, GalaxyCore Inc.)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Voltage correction:&lt;/strong&gt; The original document's "3.3V single supply" is inaccurate. The official datasheet explicitly states "Single power supply requirement (2.8V)", typical 2.8V, range 2.7–3.3V. &lt;strong&gt;Recommended supply is 2.8V LDO&lt;/strong&gt; — 3.3V is within range but not the optimal operating point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; GC0309-8225N V2 is a complete module model customized by module vendors based on the GC0309 sensor. Pin definitions, lens parameters, and FPC interfaces may vary between vendors. Always refer to your specific module's datasheet.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  OV7670 Module
&lt;/h2&gt;

&lt;p&gt;OmniVision's classic 1/6-inch VGA sensor — the most historically significant and ecosystem-mature sensor in embedded vision. The de facto standard for Arduino, STM32, and FPGA education.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;OmniVision OV7670&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/6 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;0.3MP (640 × 480)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;3.6 µm × 3.6 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter&lt;/td&gt;
&lt;td&gt;Electronic rolling&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;DVP 8-bit parallel + SCCB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;YUV422, RGB565, RGB555, RAW RGB, Processed Bayer&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;VGA@30fps, QVGA@60fps, CIF@60fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master clock XCLK&lt;/td&gt;
&lt;td&gt;10 – 24 MHz (typical 24 MHz)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;~60 mW (15fps VGA YUV), standby &amp;lt;20 µA&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: OV7670 official datasheet (w2.electrodragon.com/Chip-dat/OmniVision-dat/OV7670-dat/OV7670-ds.pdf)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Voltage correction (important):&lt;/strong&gt; The original's "2.5–3.0V single voltage" is simplified and misleading. OV7670 actually requires &lt;strong&gt;three separate supply domains&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Supply Domain&lt;/th&gt;
&lt;th&gt;Voltage Range&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DVDD (digital core)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.8V&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires dedicated LDO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVDD (analog)&lt;/td&gt;
&lt;td&gt;2.45–3.0V&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOVDD (I/O)&lt;/td&gt;
&lt;td&gt;1.7–3.0V&lt;/td&gt;
&lt;td&gt;Can connect to 3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Purchasing note: &lt;strong&gt;Cheap non-FIFO modules typically only expose DOVDD&lt;/strong&gt; — DVDD and AVDD are stepped down via on-board LDOs. But bare minimum boards may require external regulation. Non-FIFO versions are extremely demanding on MCU real-time performance; with STM32, pair with AL422B FIFO buffer chip.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  OV3660 Module
&lt;/h2&gt;

&lt;p&gt;OmniVision 3MP (2048×1536) sensor using OmniBSI backside-illumination technology. Natively supported by ESP32's esp32-camera library.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Multiple corrections in this section — read carefully.&lt;/strong&gt; My original version had errors in interface, frame rate, voltage, and current — all corrected against the official datasheet below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;OmniVision OV3660&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/5 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;3MP (2048 × 1536)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.4 µm × 1.4 µm (OmniBSI)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter&lt;/td&gt;
&lt;td&gt;Electronic rolling&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DVP 8-bit parallel ONLY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;RAW RGB, RGB565/555/444, YCbCr422, JPEG compression&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;QXGA@15fps, &lt;strong&gt;1080P@20fps&lt;/strong&gt;, &lt;strong&gt;720P@45fps&lt;/strong&gt;, VGA@60fps&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Core 1.5V, I/O 1.8V/2.8V, Analog 2.6–3.0V&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~98 mA (active), 20 µA (standby)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensitivity&lt;/td&gt;
&lt;td&gt;670 mV/Lux-sec&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic range&lt;/td&gt;
&lt;td&gt;70 dB @ 8x gain&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating temp&lt;/td&gt;
&lt;td&gt;-20°C to 70°C&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XCLK&lt;/td&gt;
&lt;td&gt;6 – 27 MHz&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: OV3660 official datasheet (download.kamami.pl/p1196945-OV3660%20Datasheet.pdf), ESP32 Camera Driver (github.com/espressif/esp32-camera)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Correction 1 — No MIPI:&lt;/strong&gt; The original claimed OV3660 supports "DVP 8-bit OR MIPI CSI-2 (1-lane)" — this is &lt;strong&gt;wrong&lt;/strong&gt;. The official datasheet only mentions Digital Video Parallel Port with no MIPI interface description. ESP32 can use it precisely because it's DVP.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Correction 2 — Frame rate:&lt;/strong&gt; Original claimed 1080P@30fps, 720P@60fps; actual is &lt;strong&gt;1080P@20fps, 720P@45fps&lt;/strong&gt;. Original overstated by ~33%.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Correction 3 — Voltage:&lt;/strong&gt; Original "I/O 1.8–3.3V, core 1.2V, analog 2.8V" is wrong. Actual: core 1.5V, I/O 1.8V or 2.8V, analog 2.6–3.0V.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Correction 4 — Current:&lt;/strong&gt; Original "~130mA" is too high; datasheet specifies 98mA active.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purchasing tip:&lt;/strong&gt; OV3660 offers 50% higher resolution than OV2640 (3MP vs 2MP) with better low-light performance via BSI. If your project already uses OV2640 and you want to upgrade image quality while staying on ESP32, OV3660 is the best drop-in replacement (both DVP, directly supported by esp32-camera).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  GC2053 Module
&lt;/h2&gt;

&lt;p&gt;GalaxyCore 2MP (1920×1080) sensor with 1/2.9-inch large sensor and 2.8µm large pixels, supporting both MIPI and DVP interfaces. Standout low-light performance in its price tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;GalaxyCore GC2053&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.9 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;2MP (1920 × 1080)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.8 µm × 2.8 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter&lt;/td&gt;
&lt;td&gt;Electronic rolling&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2-lane) or DVP&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;RAW Bayer 10-bit / 8-bit&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;1080P@30fps, 720P@60fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.8V, DVDD 1.2V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current&lt;/td&gt;
&lt;td&gt;~57 mA total (analog 17.3 + digital 37.4 + I/O 2.5)&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADC&lt;/td&gt;
&lt;td&gt;10-bit&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: GC2053 datasheet V1.1 (2018-11-29, GalaxyCore / CK Vision)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Current correction:&lt;/strong&gt; Original claimed "~95 mA"; datasheet measures approximately 57mA total (analog 17.3mA + digital 37.4mA + I/O 2.5mA). Actual power consumption is lower than original — thermal and power design can be more relaxed. MIPI 2-lane bandwidth of 1.5Gbps/lane 🔍 pending final datasheet confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why choose GC2053:&lt;/strong&gt; The 1/2.9-inch sensor is larger than both OV2640 (1/4") and OV5640 (1/4"), and 2.8µm pixels are also bigger. Bigger sensor + bigger pixels = more light per pixel = better low-light performance. It has a clear advantage over same-price OV solutions for security surveillance.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  GC4653 Module
&lt;/h2&gt;

&lt;p&gt;GalaxyCore's high-performance 4MP sensor using BSI backside-illumination technology for enhanced low-light sensitivity. A high-end choice for security cameras and intelligent vision applications.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;"STARVIS" terminology correction.&lt;/strong&gt; The original claimed GC4653 uses "STARVIS architecture technology" — this is incorrect. STARVIS is Sony's registered trademark and exclusive technology; GalaxyCore does not use STARVIS. GC4653 actually uses in-house BSI (backside-illuminated) pixel technology for high sensitivity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;GalaxyCore GC4653&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/3 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;4MP (2560 × 1440)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.0 µm × 2.0 µm (BSI)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter&lt;/td&gt;
&lt;td&gt;Electronic rolling&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MIPI CSI-2 ONLY (2-lane, RAW12/RAW10)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;RAW Bayer 12-bit / 10-bit&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;4MP@30fps, 1080P@60fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.8V, DVDD 1.2V, I/O 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Current&lt;/td&gt;
&lt;td&gt;~120 mA&lt;/td&gt;
&lt;td&gt;🔍 pending datasheet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Features&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;BSI high-sensitivity low-light&lt;/strong&gt;, HDR, PDAF phase AF, 2D/3D noise reduction&lt;/td&gt;
&lt;td&gt;⚠️ see above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: GC4653 datasheet (Camemaker), Sipeed Wiki, Sony Semiconductor official site (STARVIS trademark confirmation)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;ESP32 incompatible:&lt;/strong&gt; GC4653 is MIPI-only, incompatible with traditional DVP parallel interface. ESP32 series has no MIPI CSI-2 receiver capability and &lt;strong&gt;cannot use this sensor&lt;/strong&gt;. Requires a controller with MIPI CSI-2, such as HiSilicon Hi3516, Ingenic T31, Rockchip RV1106, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purchasing tip:&lt;/strong&gt; Confirm your controller has MIPI CSI-2 before choosing GC4653. If you're doing an ESP32 project, don't buy this sensor — you won't be able to light it up.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  OV9281 Module
&lt;/h2&gt;

&lt;p&gt;OmniVision 1/4-inch 1MP &lt;strong&gt;global shutter&lt;/strong&gt; sensor using OmniPixel®3-GS technology. Like the IMX273, it's a global shutter sensor, but much cheaper with DVP+MIPI dual interface — the most affordable global shutter option for makers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;OmniVision OV9281&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;1MP (1280 × 800)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;3.0 µm × 3.0 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter type&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Global Shutter&lt;/strong&gt; (OmniPixel®3-GS)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2-lane) + DVP&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW8 / RAW10&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;120 fps&lt;/strong&gt; (1280×800), 180fps (VGA)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.8V, DVDD 1.2V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;156 mW (active), 150 µA (standby)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S/N ratio&lt;/td&gt;
&lt;td&gt;38 dB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic range&lt;/td&gt;
&lt;td&gt;68 dB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production status&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;In production&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;Machine vision, drones, barcode scanning, AR/VR, 3D depth&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: OmniVision OV9281 Product Brief v1.4 (ovt.com/wp-content/uploads/2024/05/OV9281-PB-v1.4-WEB.pdf)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;ESP32 compatibility:&lt;/strong&gt; OV9281 is &lt;strong&gt;NOT&lt;/strong&gt; in the esp32-camera library (classic ESP32/S2/S3). ESP32-P4 has experimental support but unstable (esp-idf #17795). For global shutter on ESP32, there's currently no good option; use Raspberry Pi + Arducam OV9281 module instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why OV9281 over IMX273:&lt;/strong&gt; Both are global shutter, but OV9281 is much cheaper ($15-25 vs $50+) and supports both DVP and MIPI. Downsides: lower resolution (1MP vs 1.58MP) and monochrome only (no color version).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  OV5647 Module
&lt;/h2&gt;

&lt;p&gt;OmniVision 1/4-inch 5MP CMOS sensor with OmniBSI technology. The original Raspberry Pi Camera Module V1 (2013) sensor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;OmniVision OV5647&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;5MP (2592 × 1944)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.4 µm × 1.4 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shutter type&lt;/td&gt;
&lt;td&gt;Rolling shutter&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;OmniBSI backside illumination&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2-lane) + DVP&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW8 / RAW10&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;5MP@15fps, 1080P@30fps, 720P@60fps, VGA@90fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;Analog 2.6–3.0V (typical 2.8V), Core 1.5V, I/O 1.7–3.0V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production status&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;End-of-Life (EOL)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;Raspberry Pi Camera V1, phones, notebooks&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: OmniVision OV5647 datasheet (v-visiontech.com/web/userfiles/download/OV5647DS.pdf), Raspberry Pi official documentation&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Discontinued (EOL):&lt;/strong&gt; OV5647 is on OmniVision's discontinued product list. Surplus stock still available ($8-15), but &lt;strong&gt;not recommended for new designs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selection comparison:&lt;/strong&gt; OV5647 vs OV5640 — both 5MP. OV5647 is the Pi V1 sensor; OV5640 is the ESP32 ecosystem 5MP option (with AF). Pins and drivers are incompatible. For Pi projects on a budget, OV5647 surplus still works; for ESP32, use OV5640.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;NOT in esp32-camera support list.&lt;/strong&gt; ESP32 users should use OV2640 or OV5640.
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  IMX219 Module
&lt;/h2&gt;

&lt;p&gt;Sony 1/4-inch 8MP CMOS sensor with Exmor R backside-illumination technology. The standard sensor for Raspberry Pi Camera Module V2, and the most ubiquitous MIPI camera in the Raspberry Pi ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX219&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;8MP (3280 × 2464)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.12 µm × 1.12 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Exmor R&lt;/strong&gt; BSI (NOT STARVIS)&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2-lane)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;1080P@30fps, 720P@60fps, VGA@90fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.8V, DVDD 1.2V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;~38 mA @2.8V + ~160 mA @1.2V (≈ 0.26 W)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production status&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;In production&lt;/strong&gt; (at least until Jan 2028)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX219 datasheet (dlscorp.com), Raspberry Pi official documentation&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Exmor R is NOT STARVIS:&lt;/strong&gt; Some online articles classify IMX219 as STARVIS — this is wrong. IMX219 uses Exmor R (BSI). STARVIS is Sony's later security-focused technology line with higher sensitivity and dynamic range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purchasing tip:&lt;/strong&gt; The IMX219 module (Raspberry Pi Camera V2) is the lowest-cost entry to MIPI. Pi 4B/5 CSI connector works directly, driver is in mainline kernel, plug-and-play.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX273 Module
&lt;/h2&gt;

&lt;p&gt;Sony Pregius series global shutter sensor, 1/2.9-inch, 1.58MP. Designed for industrial FA cameras and ITS (Intelligent Traffic Systems), 226fps at full resolution — a high-performance choice for high-speed machine vision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX273&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.9 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;1.58MP (1456 × 1088)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;3.45 µm × 3.45 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Pregius Global Shutter&lt;/strong&gt; (NOT STARVIS)&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (1-lane) OR Sub LVDS (8-ch)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10 / RAW12&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;226 fps&lt;/strong&gt; (full resolution, 10-bit)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;3.3V / 1.8V / 1.2V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;Industrial FA cameras, ITS, barcode scanning, defect inspection&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX273 official Flyer (sony-semicon.com/files/62/flyer_industry/IMX273_287_296_297_Flyer.pdf)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Global vs Rolling Shutter:&lt;/strong&gt; Every other sensor in this handbook uses rolling shutter, which causes jello effect on fast-moving objects. IMX273 has a global shutter — the entire image is exposed simultaneously, &lt;strong&gt;zero jello effect on high-speed motion&lt;/strong&gt;. This is the core reason to choose it for industrial vision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; IMX273 comes in two variants — MIPI 1-lane and Sub LVDS 8-ch. Confirm which interface you need before buying.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX307 Module
&lt;/h2&gt;

&lt;p&gt;Sony STARVIS 2MP security sensor, 1/2.8-inch, 2.9µm large pixels. HDR support, a mainstream choice for 1080P security cameras, commonly paired with HiSilicon Hi3516 series SoCs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX307&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.8 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;2.07MP (1920 × 1080)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.9 µm × 2.9 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;STARVIS (original)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2/4-lane) + LVDS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10 / RAW12&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;60 fps (1080P)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR&lt;/td&gt;
&lt;td&gt;Multi-exposure HDR + Digital overlap HDR&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.9V, DVDD 1.2V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;Security surveillance, FA cameras&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX307 official Flyer (sony-semicon.com/files/62/flyer_security/IMX307LQD_LQR_Flyer.pdf)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Highlight:&lt;/strong&gt; 2.9µm large pixels + STARVIS = excellent night vision. Paired with Hi3516C V300 is the classic security combo.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX335 Module
&lt;/h2&gt;

&lt;p&gt;Sony STARVIS 5MP security sensor, 1/2.8-inch. Steps up from IMX307's 2MP to 5MP while maintaining good day/night image quality. Commonly paired with Hi3516 / RV1106.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX335&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.8 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;5.04MP (2592 × 1944)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.0 µm × 2.0 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;STARVIS (original)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2/4-lane)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10 / RAW12&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;60 fps (full resolution, 10-bit)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR&lt;/td&gt;
&lt;td&gt;Multi-exposure HDR + Digital overlap HDR&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.9V, DVDD 1.2V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common SoC pairing&lt;/td&gt;
&lt;td&gt;Hi3516 series, RV1106&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX335 official Flyer (sony-semicon.com/files/62/flyer_security/IMX335LQN_Flyer.pdf)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selection comparison:&lt;/strong&gt; IMX335 vs IMX307 — same 1/2.8" sensor. IMX307 is 2MP/2.9µm (better low light), IMX335 is 5MP/2.0µm (more detail). Choose IMX335 if you need to see far details, IMX307 for night surveillance.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX415 Module
&lt;/h2&gt;

&lt;p&gt;Sony STARVIS 4K (8MP) security sensor, 1/2.8-inch. The workhorse sensor for 4K security cameras, with the most mature module ecosystem — Jetson / Raspberry Pi / RV1106 all have ready drivers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX415&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.8 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;8.29MP (3840 × 2160, 4K UHD)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.45 µm × 1.45 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;STARVIS (original)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2/4-lane)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW8 / RAW10&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;4K up to 90fps (typical 30–60fps)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR&lt;/td&gt;
&lt;td&gt;Multi-exposure HDR&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.9V, DVDD 1.1V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announced&lt;/td&gt;
&lt;td&gt;June 2019&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;4K security, NVR, industrial cameras&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX415 official Flyer (sony-semicon.com/files/62/pdf/p-12_IMX415-AAQR_AAMR_Flyer.pdf)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purchasing tip:&lt;/strong&gt; IMX415 has the best DIY/embedded module ecosystem of all Sony IMX sensors. LuckFox, Radxa, e-con Systems all offer ready-made modules with drivers for Jetson, Raspberry Pi, RV1106. The top pick for 4K MIPI projects.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX678 Module
&lt;/h2&gt;

&lt;p&gt;Sony STARVIS 2 4K (8MP) security sensor, 1/1.8-inch large sensor. A 2024 new release that comprehensively upgrades the IMX415 — bigger sensor, newer STARVIS 2 technology, faster frame rate, stronger HDR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX678&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1/1.8 inch&lt;/strong&gt; (much larger than IMX415's 1/2.8")&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;8.29MP (3840 × 2160, 4K UHD)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.0 µm × 2.0 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;STARVIS 2&lt;/strong&gt; (next-gen)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2/4/8-lane, 4-lane×2ch)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10 / RAW12&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;72 fps&lt;/strong&gt; (4K, 10-bit) / 60fps (12-bit)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR&lt;/td&gt;
&lt;td&gt;Digital overlap HDR + &lt;strong&gt;Clear HDR&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;AVDD 3.3V&lt;/strong&gt;, DVDD 1.1V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;⚠️ see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announced&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common SoC pairing&lt;/td&gt;
&lt;td&gt;Hi3519DV500&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX678 official Flyer (sony-semicon.com/files/62/flyer_security/IMX678-AAQR_AAQR1_Flyer.pdf, © 2024)&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Power note:&lt;/strong&gt; IMX678's AVDD is &lt;strong&gt;3.3V&lt;/strong&gt;, different from IMX335/IMX307/IMX415's 2.9V. Change the LDO output when migrating from other IMX designs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IMX678 vs IMX415:&lt;/strong&gt; Both are 4K 8MP, but IMX678 has a bigger sensor (1/1.8" vs 1/2.8"), bigger pixels (2.0µm vs 1.45µm), newer tech (STARVIS 2), and stronger HDR (Clear HDR). The trade-off is more complex power/lane requirements and higher module cost as a 2024 new release.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX766 Module
&lt;/h2&gt;

&lt;p&gt;Sony 50MP smartphone sensor, 1/1.56-inch large sensor, Quad Bayer array. Used in OPPO Find X3 Pro, Reno7 Pro and other flagship phones.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;NOT recommended for DIY / embedded projects.&lt;/strong&gt; The following is for reference only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX766&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/1.56 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;50MP (8192 × 6144)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.0 µm (4-in-1 binning → 2.0 µm)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel array&lt;/td&gt;
&lt;td&gt;Quad Bayer&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (4-lane)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10 / RAW8&lt;/td&gt;
&lt;td&gt;🔍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;Full res ~24fps; binned 30fps; 1080P 60–120fps&lt;/td&gt;
&lt;td&gt;🔍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 2.8V, DVDD 1.1V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;🔍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone implementations&lt;/td&gt;
&lt;td&gt;OPPO Find X3 Pro, OnePlus, Realme&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: OPPO official specs, IADIY module page, DxOMark. ⚠️ &lt;strong&gt;Sony has not published an official IMX766 datasheet&lt;/strong&gt; — some parameters are from phone manufacturers and module vendors.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Why not recommended for DIY:&lt;/strong&gt; No official datasheet (register config and power sequencing require reverse engineering), needs custom driver (no mainline Linux driver), non-standard FPC, 50MP Quad Bayer needs powerful ISP, many modules include OIS motors needing extra drivers. For high-res MIPI, choose &lt;strong&gt;IMX415&lt;/strong&gt; or &lt;strong&gt;IMX477&lt;/strong&gt; (Raspberry Pi HQ Camera) instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Sony IMX Series Key Parameters Comparison
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;All 15 sensors in one table would exceed mobile screen width. OV/GC comparison table above; Sony IMX series below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;IMX219&lt;/th&gt;
&lt;th&gt;IMX273&lt;/th&gt;
&lt;th&gt;IMX307&lt;/th&gt;
&lt;th&gt;IMX335&lt;/th&gt;
&lt;th&gt;IMX415&lt;/th&gt;
&lt;th&gt;IMX678&lt;/th&gt;
&lt;th&gt;IMX766&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;Exmor R&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pregius GS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;STARVIS&lt;/td&gt;
&lt;td&gt;STARVIS&lt;/td&gt;
&lt;td&gt;STARVIS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;STARVIS 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quad Bayer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolution&lt;/td&gt;
&lt;td&gt;8MP&lt;/td&gt;
&lt;td&gt;1.58MP&lt;/td&gt;
&lt;td&gt;2MP&lt;/td&gt;
&lt;td&gt;5MP&lt;/td&gt;
&lt;td&gt;8MP (4K)&lt;/td&gt;
&lt;td&gt;8MP (4K)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50MP&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4"&lt;/td&gt;
&lt;td&gt;1/2.9"&lt;/td&gt;
&lt;td&gt;1/2.8"&lt;/td&gt;
&lt;td&gt;1/2.8"&lt;/td&gt;
&lt;td&gt;1/2.8"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1/1.8"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1/1.56"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.12 µm&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.45 µm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.9 µm&lt;/td&gt;
&lt;td&gt;2.0 µm&lt;/td&gt;
&lt;td&gt;1.45 µm&lt;/td&gt;
&lt;td&gt;2.0 µm&lt;/td&gt;
&lt;td&gt;1.0 µm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI 2L&lt;/td&gt;
&lt;td&gt;MIPI 1L/LVDS&lt;/td&gt;
&lt;td&gt;MIPI 2/4L&lt;/td&gt;
&lt;td&gt;MIPI 2/4L&lt;/td&gt;
&lt;td&gt;MIPI 2/4L&lt;/td&gt;
&lt;td&gt;MIPI 2/4/8L&lt;/td&gt;
&lt;td&gt;MIPI 4L&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max fps&lt;/td&gt;
&lt;td&gt;1080P@30fps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;226fps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1080P@60fps&lt;/td&gt;
&lt;td&gt;5MP@60fps&lt;/td&gt;
&lt;td&gt;4K@90fps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4K@72fps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50MP@24fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Clear&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global shutter&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVDD&lt;/td&gt;
&lt;td&gt;2.8V&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;td&gt;2.9V&lt;/td&gt;
&lt;td&gt;2.9V&lt;/td&gt;
&lt;td&gt;2.9V&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.3V&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.8V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best use case&lt;/td&gt;
&lt;td&gt;Raspberry Pi V2&lt;/td&gt;
&lt;td&gt;Industrial FA&lt;/td&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;4K Security&lt;/td&gt;
&lt;td&gt;High-end 4K&lt;/td&gt;
&lt;td&gt;⚠️ Phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY recommended&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ Industrial&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ Top pick&lt;/td&gt;
&lt;td&gt;⚠️ Pricey&lt;/td&gt;
&lt;td&gt;❌ Not recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  IMX477 Module
&lt;/h2&gt;

&lt;p&gt;Sony 12.3MP sensor, 1/2.3-inch, with interchangeable C/CS-mount lens. The standard sensor for Raspberry Pi HQ Camera — ideal for projects needing high-quality imaging and interchangeable lenses: microscopy, astronomy, industrial inspection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX477&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.3 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;12.3MP (4056 × 3040)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.55 µm × 1.55 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;BSI stacked CMOS&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (2/4-lane)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW12 / RAW10 / RAW8&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;Full res@60fps (10-bit), 4K@60fps, 1080P@240fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR&lt;/td&gt;
&lt;td&gt;DOL-HDR (Digital Overlap HDR)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lens mount&lt;/td&gt;
&lt;td&gt;C-mount / CS-mount (interchangeable)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production status&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;In production&lt;/strong&gt; (at least until Jan 2030)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announced&lt;/td&gt;
&lt;td&gt;April 2020&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;Raspberry Pi HQ Camera, microscopy/astronomy, industrial&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX477 Flyer, Raspberry Pi HQ Camera product page&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purchasing tip:&lt;/strong&gt; IMX477's biggest advantage is &lt;strong&gt;interchangeable lenses&lt;/strong&gt; — C-mount and CS-mount cover wide-angle to telephoto to macro. Best image quality in the Pi ecosystem. $50-70.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX708 Module
&lt;/h2&gt;

&lt;p&gt;Sony 12MP Quad Bayer sensor, 1/2.3-inch, with PDAF phase-detection autofocus and HDR. The standard sensor for Raspberry Pi Camera Module 3 — the first Pi camera with hardware autofocus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX708&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.3 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;12MP (4608 × 2592)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;1.4 µm × 1.4 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;BSI stacked CMOS, Quad Bayer&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;1080P@50fps, 720P@100fps, 480P@120fps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autofocus&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;PDAF phase-detection autofocus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDR&lt;/td&gt;
&lt;td&gt;Yes (up to 3MP output)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production status&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;In production&lt;/strong&gt; (at least until Jan 2030)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announced&lt;/td&gt;
&lt;td&gt;January 2023&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;Raspberry Pi Camera Module 3 (standard/wide/telephoto)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Raspberry Pi Camera Module 3 product page and product brief&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Highlight:&lt;/strong&gt; IMX708 brings hardware autofocus (PDAF) to Raspberry Pi for the first time. Available in standard, wide, and telephoto versions. $25-30 — best price/performance for Pi MIPI cameras.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  IMX296 Module
&lt;/h2&gt;

&lt;p&gt;Sony Pregius global shutter sensor, 1/2.9-inch, 1.58MP. The standard sensor for Raspberry Pi Global Shutter Camera. Like the IMX273, it's a Pregius global shutter sensor, but with official Pi ecosystem module and driver support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Specs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;Reliability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor&lt;/td&gt;
&lt;td&gt;Sony IMX296&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/2.9 inch&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effective pixels&lt;/td&gt;
&lt;td&gt;1.58MP (1456 × 1088)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;3.45 µm × 3.45 µm&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technology&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pregius Global Shutter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;MIPI CSI-2 (1-lane)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;RAW10&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max frame rate&lt;/td&gt;
&lt;td&gt;60.3 fps (full resolution, 10-bit)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating voltage&lt;/td&gt;
&lt;td&gt;AVDD 3.3V, DVDD 1.2V, IOVDD 1.8V&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lens mount&lt;/td&gt;
&lt;td&gt;C-mount / CS-mount (interchangeable)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production status&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;In production&lt;/strong&gt; (at least until Jan 2032)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announced&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical applications&lt;/td&gt;
&lt;td&gt;Raspberry Pi Global Shutter Camera, high-speed capture, industrial&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Source: Sony IMX296 Flyer, Raspberry Pi Global Shutter Camera product page&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;IMX296 vs IMX273:&lt;/strong&gt; Both are Pregius global shutter, 3.45µm pixels. IMX296 is the official Pi Global Shutter Camera sensor with full driver/docs; IMX273 is more industrial FA focused. For Pi projects choose IMX296, for industrial FA choose IMX273.
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Module Comparison Summary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Parameters Comparison
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Table corrected per official datasheets. Bolded items had errors in the original or need special attention.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;OV2640&lt;/th&gt;
&lt;th&gt;OV5640&lt;/th&gt;
&lt;th&gt;OV7725&lt;/th&gt;
&lt;th&gt;GC0309&lt;/th&gt;
&lt;th&gt;OV7670&lt;/th&gt;
&lt;th&gt;OV3660&lt;/th&gt;
&lt;th&gt;GC2053&lt;/th&gt;
&lt;th&gt;GC4653&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;OmniVision&lt;/td&gt;
&lt;td&gt;OmniVision&lt;/td&gt;
&lt;td&gt;OmniVision&lt;/td&gt;
&lt;td&gt;GalaxyCore&lt;/td&gt;
&lt;td&gt;OmniVision&lt;/td&gt;
&lt;td&gt;OmniVision&lt;/td&gt;
&lt;td&gt;GalaxyCore&lt;/td&gt;
&lt;td&gt;GalaxyCore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolution&lt;/td&gt;
&lt;td&gt;2MP&lt;/td&gt;
&lt;td&gt;5MP&lt;/td&gt;
&lt;td&gt;0.3MP&lt;/td&gt;
&lt;td&gt;0.3MP&lt;/td&gt;
&lt;td&gt;0.3MP&lt;/td&gt;
&lt;td&gt;3MP&lt;/td&gt;
&lt;td&gt;2MP&lt;/td&gt;
&lt;td&gt;4MP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max size&lt;/td&gt;
&lt;td&gt;1600×1200&lt;/td&gt;
&lt;td&gt;2592×1944&lt;/td&gt;
&lt;td&gt;640×480&lt;/td&gt;
&lt;td&gt;640×480&lt;/td&gt;
&lt;td&gt;640×480&lt;/td&gt;
&lt;td&gt;2048×1536&lt;/td&gt;
&lt;td&gt;1920×1080&lt;/td&gt;
&lt;td&gt;2560×1440&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optical format&lt;/td&gt;
&lt;td&gt;1/4"&lt;/td&gt;
&lt;td&gt;1/4"&lt;/td&gt;
&lt;td&gt;1/4"&lt;/td&gt;
&lt;td&gt;1/9"&lt;/td&gt;
&lt;td&gt;1/6"&lt;/td&gt;
&lt;td&gt;1/5"&lt;/td&gt;
&lt;td&gt;1/2.9"&lt;/td&gt;
&lt;td&gt;1/3"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel size&lt;/td&gt;
&lt;td&gt;2.2 µm&lt;/td&gt;
&lt;td&gt;1.4 µm&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.0 µm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.5 µm&lt;/td&gt;
&lt;td&gt;3.6 µm&lt;/td&gt;
&lt;td&gt;1.4 µm&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.8 µm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.0 µm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;DVP/MIPI&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;DVP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DVP only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DVP/MIPI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MIPI only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max fps&lt;/td&gt;
&lt;td&gt;UXGA@15fps&lt;/td&gt;
&lt;td&gt;QSXGA@15fps&lt;/td&gt;
&lt;td&gt;VGA@60fps&lt;/td&gt;
&lt;td&gt;VGA@30fps&lt;/td&gt;
&lt;td&gt;VGA@30fps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1080P@20fps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1080P@30fps&lt;/td&gt;
&lt;td&gt;4MP@30fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output formats&lt;/td&gt;
&lt;td&gt;YUV/RGB/RAW/JPEG&lt;/td&gt;
&lt;td&gt;YUV/RGB/RAW/JPEG&lt;/td&gt;
&lt;td&gt;YUV/RGB/RAW&lt;/td&gt;
&lt;td&gt;YUV/RGB/RAW&lt;/td&gt;
&lt;td&gt;YUV/RGB/RAW&lt;/td&gt;
&lt;td&gt;YUV/RGB/RAW/JPEG&lt;/td&gt;
&lt;td&gt;RAW Bayer&lt;/td&gt;
&lt;td&gt;RAW Bayer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autofocus&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (VCM)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;PDAF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-light&lt;/td&gt;
&lt;td&gt;Average&lt;/td&gt;
&lt;td&gt;Average&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Excellent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Average&lt;/td&gt;
&lt;td&gt;Average&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Excellent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Excellent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voltage&lt;/td&gt;
&lt;td&gt;1.7–3.3V (I/O)&lt;/td&gt;
&lt;td&gt;1.7–3.3V (I/O)&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.8V typical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.8V/2.45-3V/1.7-3V&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.5V core multi-rail&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.8V (I/O)&lt;/td&gt;
&lt;td&gt;1.8V (I/O)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical power&lt;/td&gt;
&lt;td&gt;~125 mW&lt;/td&gt;
&lt;td&gt;~140 mA&lt;/td&gt;
&lt;td&gt;~80 mA&lt;/td&gt;
&lt;td&gt;~60 mA&lt;/td&gt;
&lt;td&gt;~60 mW&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~98 mA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~57 mA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~120 mA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I²C address&lt;/td&gt;
&lt;td&gt;0x30&lt;/td&gt;
&lt;td&gt;0x3C&lt;/td&gt;
&lt;td&gt;0x21&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;0x21&lt;/td&gt;
&lt;td&gt;See DS&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price tier&lt;/td&gt;
&lt;td&gt;Low-mid&lt;/td&gt;
&lt;td&gt;Mid-high&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low-mid&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;td&gt;Mid-high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESP32 compatible&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ needs FIFO&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ DVP mode only&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Selection Recommendations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Education (Arduino/STM32):&lt;/strong&gt; OV7670 — Richest ecosystem, most tutorials. Note three-rail power.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wi-Fi streaming (ESP32-CAM):&lt;/strong&gt; OV2640 — Built-in JPEG, balanced power/quality, de facto standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-res photo (ESP32/STM32):&lt;/strong&gt; OV5640 — 5MP + autofocus, best entry-level quality. Power VCM on AF versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Night vision / machine vision (low-light):&lt;/strong&gt; OV7725 — 6.0µm pixels, low-light king. ⚠️ EOL, surplus stock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ultra low cost (access/POS):&lt;/strong&gt; GC0309-8225N — Lowest price, meets basic needs. Power at 2.8V.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3MP upgrade (ESP32):&lt;/strong&gt; OV3660 — BSI, drop-in for OV2640. Note lower frame rate than OV2640.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security surveillance (indoor/outdoor):&lt;/strong&gt; GC2053 — Large sensor/pixels, MIPI high-speed, low power.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-end security / AI vision:&lt;/strong&gt; GC4653 — 4MP + BSI high sensitivity. ⚠️ MIPI only, not for ESP32.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raspberry Pi / Jetson entry (MIPI):&lt;/strong&gt; IMX219 — Pi Camera V2 standard, most mature driver, plug-and-play.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industrial vision / high-speed capture:&lt;/strong&gt; IMX273 — Global shutter, 226fps, zero jello effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security (1080P nighttime):&lt;/strong&gt; IMX307 — 2.9µm + STARVIS, strong night vision. Pair with Hi3516.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security (high resolution):&lt;/strong&gt; IMX335 — 5MP STARVIS, more detail. Pair with Hi3516/RV1106.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4K security / NVR:&lt;/strong&gt; IMX415 — 4K STARVIS, best module ecosystem, top pick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-end 4K security (2024):&lt;/strong&gt; IMX678 — STARVIS 2 + Clear HDR, best image quality. ⚠️ AVDD 3.3V.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚠️ Don't buy for DIY:&lt;/strong&gt; IMX766 — 50MP phone sensor, no official datasheet, needs custom driver.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Application &amp;amp; Wiring Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ESP32 Typical Wiring
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important disclaimer:&lt;/strong&gt; The pin mapping below is based on common AI-Thinker ESP32-CAM module configurations. &lt;strong&gt;There is no official unified pin standard for ESP32-CAM&lt;/strong&gt; — the esp32-camera library examples explicitly state "pin configurations vary by board". Different vendors (AI-Thinker, Freenove, SunFounder, Espressif official EVB) may all differ. &lt;strong&gt;Always verify which board you have before wiring.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI-Thinker ESP32-CAM common mapping (applies to OV2640 / OV5640 / OV7725):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module Signal&lt;/th&gt;
&lt;th&gt;ESP32 GPIO&lt;/th&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XCLK&lt;/td&gt;
&lt;td&gt;GPIO 27&lt;/td&gt;
&lt;td&gt;Master clock via LEDC PWM at 20 MHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PCLK&lt;/td&gt;
&lt;td&gt;GPIO 22&lt;/td&gt;
&lt;td&gt;Pixel clock input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSYNC&lt;/td&gt;
&lt;td&gt;GPIO 25&lt;/td&gt;
&lt;td&gt;Frame sync input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HREF&lt;/td&gt;
&lt;td&gt;GPIO 23&lt;/td&gt;
&lt;td&gt;Line sync input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D0&lt;/td&gt;
&lt;td&gt;GPIO 32&lt;/td&gt;
&lt;td&gt;Data bit 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D1&lt;/td&gt;
&lt;td&gt;GPIO 35&lt;/td&gt;
&lt;td&gt;Data bit 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D2&lt;/td&gt;
&lt;td&gt;GPIO 34&lt;/td&gt;
&lt;td&gt;Data bit 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D3&lt;/td&gt;
&lt;td&gt;GPIO 5&lt;/td&gt;
&lt;td&gt;Data bit 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D4&lt;/td&gt;
&lt;td&gt;GPIO 39&lt;/td&gt;
&lt;td&gt;Data bit 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D5&lt;/td&gt;
&lt;td&gt;GPIO 18&lt;/td&gt;
&lt;td&gt;Data bit 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D6&lt;/td&gt;
&lt;td&gt;GPIO 36&lt;/td&gt;
&lt;td&gt;Data bit 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D7&lt;/td&gt;
&lt;td&gt;GPIO 19&lt;/td&gt;
&lt;td&gt;Data bit 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIOC&lt;/td&gt;
&lt;td&gt;GPIO 26&lt;/td&gt;
&lt;td&gt;SCCB clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIOD&lt;/td&gt;
&lt;td&gt;GPIO 21&lt;/td&gt;
&lt;td&gt;SCCB data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PWDN&lt;/td&gt;
&lt;td&gt;GPIO 0&lt;/td&gt;
&lt;td&gt;Power-down control (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RESET&lt;/td&gt;
&lt;td&gt;GPIO 15&lt;/td&gt;
&lt;td&gt;Reset control (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Verification: esp32-camera repo examples/camera_example/main/camera_pinout.h (PCLK/VSYNC/HREF confirmed line-by-line against source).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  STM32 Wiring
&lt;/h3&gt;

&lt;p&gt;STM32F4/F7/H7 series feature DCMI (Digital Camera Memory Interface) peripheral for direct hardware reception of DVP data streams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect D0–D7 to DCMI_D0–DCMI_D7 corresponding GPIOs&lt;/li&gt;
&lt;li&gt;PCLK → DCMI_PIXCK&lt;/li&gt;
&lt;li&gt;HSYNC → DCMI_HSYNC&lt;/li&gt;
&lt;li&gt;VSYNC → DCMI_VSYNC&lt;/li&gt;
&lt;li&gt;I²C_SCL/SDA → any I²C peripheral&lt;/li&gt;
&lt;li&gt;Enable DCMI + DMA transfer, target buffer in SRAM or SDRAM&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Power Design
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated regulation:&lt;/strong&gt; Camera modules can draw 100–150 mA 🔍 during capture (varies significantly by sensor — OV5640/GC4653 higher, GC2053/OV7670 lower). Use a dedicated LDO (e.g., AMS1117-3.3) to avoid voltage sag from sharing MCU regulator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoupling caps:&lt;/strong&gt; Place 10 µF + 100 nF parallel decoupling caps near the module VCC pin, as close to the FPC connector as possible. Standard practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal integrity:&lt;/strong&gt; Keep DVP trace length within 10 cm; for longer runs, add 22–33 Ω series termination resistors on PCLK and data lines. ⚠️ Low-speed DVP (PCLK &amp;lt; 30MHz) may not need termination — depends on actual PCB layout.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Troubleshooting
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No image, no PCLK waveform&lt;/td&gt;
&lt;td&gt;XCLK missing or wrong frequency&lt;/td&gt;
&lt;td&gt;Measure XCLK with scope, confirm 6–24 MHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong colors / color cast&lt;/td&gt;
&lt;td&gt;Output format mismatch with MCU decoder&lt;/td&gt;
&lt;td&gt;Check register config, confirm RGB565 or YUV422&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image banding / jitter&lt;/td&gt;
&lt;td&gt;Power ripple or PCLK interference&lt;/td&gt;
&lt;td&gt;Improve power filtering, shorten data lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCCB config failure&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Wrong I²C address&lt;/strong&gt; or no pull-ups&lt;/td&gt;
&lt;td&gt;Check address table in this doc, add 4.7kΩ pull-ups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low frame rate&lt;/td&gt;
&lt;td&gt;Low XCLK or resolution too high&lt;/td&gt;
&lt;td&gt;Raise XCLK to 24 MHz, reduce resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blurry image (OV5640 AF)&lt;/td&gt;
&lt;td&gt;VCM unpowered or AF registers unconfigured&lt;/td&gt;
&lt;td&gt;Verify AF_VCC supply, initialize AF firmware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OV7670 garbled&lt;/td&gt;
&lt;td&gt;DVDD not separately powered&lt;/td&gt;
&lt;td&gt;Confirm on-board 1.8V LDO output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Practical Purchasing Advice
&lt;/h2&gt;

&lt;p&gt;Lessons learned from my own mistakes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose controller first, camera second.&lt;/strong&gt; ESP32 only does DVP — choose OV2640/OV5640/OV7725/OV3660. Only MIPI-capable controllers (RV1106, Hi3516) can use 4MP sensors like GC4653. Your controller's interface defines your options.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check if the module has FIFO.&lt;/strong&gt; OV7670 comes in versions with and without AL422B FIFO. Running non-FIFO on bare STM32 is nearly impossible for normal imaging. The cheap tier is usually non-FIFO.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AF versions need AF_VCC pin.&lt;/strong&gt; OV5640 AF modules need separate VCM motor power. Some cheap modules merge AF power with main supply, some don't expose it. Check pinout before buying.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Voltage mismatch is the most common board-frying cause.&lt;/strong&gt; Don't just connect 3.3V blindly. GC0309's optimal point is 2.8V, OV7670 core needs 1.8V. When unsure, check the voltage table in this doc or read the datasheet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;EOL parts (OV7725) carry inventory risk.&lt;/strong&gt; Officially discontinued; still available on Taobao/LCSC now, but not recommended for new mass production. Fine for prototyping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GalaxyCore datasheets are harder to find than OmniVision.&lt;/strong&gt; If Chinese channels don't have GC-series datasheets, try galaxycore.com.cn official site, or module vendor pages like MuchVision, Camemaker. Sipeed Wiki is also a good supplemental source.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Sony IMX Series Purchasing Supplement
&lt;/h2&gt;

&lt;p&gt;The purchasing advice above focuses on OV/GC series (DVP ecosystem). Here are tips specific to Sony IMX series (MIPI ecosystem):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Where to buy IMX modules:&lt;/strong&gt; IMX219 is widely available on Taobao/LCSC (Pi V2 compatibles). IMX415 modules from LuckFox, Radxa, Camemaker are recommended. IMX335/IMX307 are mostly found through security solution providers. IMX678 is a 2024 new release, modules are still scarce.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't buy phone salvage parts for embedded use.&lt;/strong&gt; Phone sensors like IMX766 are cheap on Taobao (50-300 RMB), but have no datasheet, no driver, and non-standard FPC. You'll most likely waste time trying to light them up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watch the STARVIS generation.&lt;/strong&gt; Sony officially defines three generations: STARVIS (original, 2014), STARVIS 2 (2021, wider dynamic range), STARVIS 3 (latest, LOFIC structure). Confirm which generation you're getting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Global shutter is only in the Pregius line.&lt;/strong&gt; If your project involves high-speed motion capture (conveyor belts, sports analysis, 3D scanning), only Pregius global shutter sensors like IMX273 can avoid jello effect. All other IMX and OV/GC sensors are rolling shutter.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;All parameters in this article verified against the following official datasheets and authoritative sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;OmniVision official product pages — OV2640 / OV5640 / OV7725 / OV7670 / OV3660 (ovt.com)&lt;/li&gt;
&lt;li&gt;OV2640 datasheet — uctronics.com/download/OV2640_DS.pdf&lt;/li&gt;
&lt;li&gt;OV5640 datasheet v2.03 — cdn.sparkfun.com/datasheets/Sensors/LightImaging/OV5640_datasheet.pdf&lt;/li&gt;
&lt;li&gt;OV5640 Product Brief — media.digikey.com/pdf/Data%20Sheets/OmniVision%20PDFs/OV5640_PB_3-4-11.pdf&lt;/li&gt;
&lt;li&gt;OV7725 datasheet — pdf.datasheet.live/10e069da/ovt.com/OV07725-V28A.pdf&lt;/li&gt;
&lt;li&gt;OV7670 datasheet — w2.electrodragon.com/Chip-dat/OmniVision-dat/OV7670-dat/OV7670-ds.pdf&lt;/li&gt;
&lt;li&gt;OV3660 datasheet — download.kamami.pl/p1196945-OV3660%20Datasheet.pdf&lt;/li&gt;
&lt;li&gt;GC0309 datasheet V1.0 (2009-12-28) — GalaxyCore Inc.&lt;/li&gt;
&lt;li&gt;GC2053 datasheet V1.1 (2018-11-29) — GalaxyCore / CK Vision&lt;/li&gt;
&lt;li&gt;GC4653 datasheet — Camemaker; Sipeed Wiki&lt;/li&gt;
&lt;li&gt;Espressif esp32-camera driver source — github.com/espressif/esp32-camera&lt;/li&gt;
&lt;li&gt;Sony Semiconductor Solutions — STARVIS trademark confirmation (STARVIS is Sony-exclusive tech)&lt;/li&gt;
&lt;li&gt;Linux Kernel ov7670.c — I²C address definition (android.googlesource.com)&lt;/li&gt;
&lt;li&gt;SCCB Functional Specification v2.2 — protocol spec&lt;/li&gt;
&lt;li&gt;Sony IMX219 datasheet — dlscorp.com/wp-content/uploads/2018/04/Sony-IMX219-Datasheet.pdf&lt;/li&gt;
&lt;li&gt;Sony IMX273 official Flyer — sony-semicon.com/files/62/flyer_industry/IMX273_287_296_297_Flyer.pdf&lt;/li&gt;
&lt;li&gt;Sony IMX307 official Flyer — sony-semicon.com/files/62/flyer_security/IMX307LQD_LQR_Flyer.pdf&lt;/li&gt;
&lt;li&gt;Sony IMX335 official Flyer — sony-semicon.com/files/62/flyer_security/IMX335LQN_Flyer.pdf&lt;/li&gt;
&lt;li&gt;Sony IMX415 official Flyer — sony-semicon.com/files/62/pdf/p-12_IMX415-AAQR_AAMR_Flyer.pdf&lt;/li&gt;
&lt;li&gt;Sony IMX678 official Flyer — sony-semicon.com/files/62/flyer_security/IMX678-AAQR_AAQR1_Flyer.pdf (© 2024)&lt;/li&gt;
&lt;li&gt;Sony IMX766 — OPPO official specs / IADIY module page / DxOMark&lt;/li&gt;
&lt;li&gt;Sony STARVIS technology — sony-semicon.com/en/technology/security/&lt;/li&gt;
&lt;li&gt;Raspberry Pi Camera documentation — raspberrypi.com/documentation/accessories/camera.html&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>hardware</category>
      <category>iot</category>
      <category>learning</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Privacy-First Open Source ESP32-S3 Camera with RTSP for MiBee NVR</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Sat, 13 Jun 2026 12:12:57 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/building-a-privacy-first-open-source-esp32-s3-camera-with-rtsp-for-frigate-nvr-4ie4</link>
      <guid>https://dev.to/mickey_zzc/building-a-privacy-first-open-source-esp32-s3-camera-with-rtsp-for-frigate-nvr-4ie4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Like many self-hosted enthusiasts, I've been frustrated with the state of affordable IP cameras for years. Most cheap cameras:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Phone home to unknown servers&lt;/li&gt;
&lt;li&gt;Require cloud subscriptions for basic features&lt;/li&gt;
&lt;li&gt;Don't support standard RTSP streams&lt;/li&gt;
&lt;li&gt;Run closed-source firmware you can't audit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;seeed-esp32s3-cam&lt;/strong&gt; - a 100% open source, privacy-first camera firmware that works natively with &lt;strong&gt;MiBeeNvr&lt;/strong&gt;, Blue Iris, and every self-hosted NVR system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardware
&lt;/h2&gt;

&lt;p&gt;This firmware is optimized for the &lt;strong&gt;Seeed Studio ESP32-S3 Sense&lt;/strong&gt; board - an incredibly capable camera module for around &lt;strong&gt;$15 USD&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ESP32-S3 dual-core processor&lt;/li&gt;
&lt;li&gt;OV2640 2MP camera sensor&lt;/li&gt;
&lt;li&gt;Built-in WiFi&lt;/li&gt;
&lt;li&gt;Microphone support&lt;/li&gt;
&lt;li&gt;SD card slot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect for monitoring garages, sheds, entrances, or anywhere you want an inexpensive camera without sacrificing privacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Key Features
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📹 &lt;strong&gt;Native RTSP Server&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ Works with MiBeeNvr / Blue Iris / VLC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🌐 &lt;strong&gt;HTTP MJPEG Streaming&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ Low latency browser preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔍 &lt;strong&gt;On-device Motion Detection&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ Configurable sensitivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🌙 &lt;strong&gt;Night Mode&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ Auto exposure adjustment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚙️ &lt;strong&gt;Web Configuration UI&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ Full settings via browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚡ &lt;strong&gt;Pre-built Binaries&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ Flash in 2 minutes, no compiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔒 &lt;strong&gt;100% Local Only&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ No cloud, no phone home&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📄 &lt;strong&gt;MIT Licensed&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;✅ Open source forever&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🚀 Getting Started in 5 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Flash the Firmware
&lt;/h3&gt;

&lt;p&gt;No compiling needed! Use the pre-built binaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download the latest release from &lt;a href="https://github.com/Mi-Bee-Studio/seeed-esp32s3-cam" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Flash using &lt;code&gt;esptool.py&lt;/code&gt; or your favorite flasher:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;esptool.py &lt;span class="nt"&gt;--chip&lt;/span&gt; esp32s3 write_flash 0x0 firmware.bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Configure WiFi
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Connect to the WiFi hotspot &lt;code&gt;ESP32-CAM-XXXX&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;192.168.4.1&lt;/code&gt; in your browser&lt;/li&gt;
&lt;li&gt;Enter your WiFi credentials&lt;/li&gt;
&lt;li&gt;The camera will reboot and connect to your network&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Access the Web UI
&lt;/h3&gt;

&lt;p&gt;Find your camera's IP address in your router, then open it in a browser. You'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live video preview&lt;/li&gt;
&lt;li&gt;Stream quality settings&lt;/li&gt;
&lt;li&gt;Motion detection configuration&lt;/li&gt;
&lt;li&gt;Network settings&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎯 MiBeeNvr Integration (The Good Stuff)
&lt;/h2&gt;

&lt;p&gt;This is why I built this - &lt;strong&gt;native MiBeeNvr compatibility&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Add this to your MiBeeNvr configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cameras&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;esp32_cam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rtsp://your-camera-ip:8554/stream&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Garage Camera&lt;/span&gt;
    &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;detect_motion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! No hacks, no custom scripts, just standard RTSP. Your camera will show up instantly in the MiBeeNvr web UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MiBeeNvr GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr" rel="noopener noreferrer"&gt;https://github.com/Mi-Bee-Studio/MiBeeNvr&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Technical Details
&lt;/h2&gt;

&lt;p&gt;Built with &lt;strong&gt;ESP-IDF v5.x&lt;/strong&gt; using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hardware-accelerated JPEG encoding&lt;/li&gt;
&lt;li&gt;Optimized memory management for 24/7 operation&lt;/li&gt;
&lt;li&gt;Lightweight RTSP server implementation&lt;/li&gt;
&lt;li&gt;Non-blocking WiFi handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The firmware is designed to run stable indefinitely - I've had cameras running for months without a single reboot.&lt;/p&gt;

&lt;h2&gt;
  
  
  📖 Full Documentation
&lt;/h2&gt;

&lt;p&gt;For a complete step-by-step guide including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced MiBeeNvr configuration&lt;/li&gt;
&lt;li&gt;Performance tuning&lt;/li&gt;
&lt;li&gt;Troubleshooting common issues&lt;/li&gt;
&lt;li&gt;Motion detection optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the &lt;strong&gt;full tutorial&lt;/strong&gt;:&lt;br&gt;
👉 &lt;a href="https://blog.mickeyzzc.tech/en/posts/iot/esp32s3-cam-monitor/" rel="noopener noreferrer"&gt;https://blog.mickeyzzc.tech/en/posts/iot/esp32s3-cam-monitor/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💻 Both Projects Are Open Source
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Camera Firmware:&lt;/strong&gt; &lt;a href="https://github.com/Mi-Bee-Studio/seeed-esp32s3-cam" rel="noopener noreferrer"&gt;https://github.com/Mi-Bee-Studio/seeed-esp32s3-cam&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MiBeeNvr NVR Server:&lt;/strong&gt; &lt;a href="https://github.com/Mi-Bee-Studio/MiBeeNvr" rel="noopener noreferrer"&gt;https://github.com/Mi-Bee-Studio/MiBeeNvr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PRs, feature requests, and bug reports are all welcome! This is a community project - let's build the best open source camera + NVR ecosystem together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;In an era where every IoT device seems to want your data, having fully open, local-only options is more important than ever. This camera + NVR stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Never sends data anywhere&lt;/li&gt;
&lt;li&gt;✅ You can audit every line of code&lt;/li&gt;
&lt;li&gt;✅ You control all configuration&lt;/li&gt;
&lt;li&gt;✅ Works with your existing self-hosted tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No subscriptions, no accounts, no tracking. Just a complete surveillance system that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm actively working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-discovery between cameras and MiBeeNvr&lt;/li&gt;
&lt;li&gt;MQTT support for Home Assistant&lt;/li&gt;
&lt;li&gt;SD card recording on cameras&lt;/li&gt;
&lt;li&gt;Two-way audio&lt;/li&gt;
&lt;li&gt;ONVIF support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me know what features you'd like to see!&lt;/p&gt;

</description>
      <category>esp32</category>
      <category>iot</category>
      <category>opensource</category>
    </item>
    <item>
      <title>VictoriaMetrics Stream Aggregation: A Three-Year Retrospective (2026)</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Sun, 07 Jun 2026 22:56:28 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/victoriametrics-stream-aggregation-a-three-year-retrospective-2026-2ojg</link>
      <guid>https://dev.to/mickey_zzc/victoriametrics-stream-aggregation-a-three-year-retrospective-2026-2ojg</guid>
      <description>&lt;p&gt;It's been exactly three years since the &lt;a href="https://blog.mickeyzzc.tech/en/posts/telemetry/stream-metrics-one/" rel="noopener noreferrer"&gt;first article&lt;/a&gt; in this series was published in March 2023. The VictoriaMetrics ecosystem has changed dramatically since then. Let's revisit the problems we laid out, see what the official project has resolved, and assess where our custom &lt;code&gt;stream-metrics-route&lt;/code&gt; gateway stands today.&lt;/p&gt;




&lt;h2&gt;
  
  
  I. The Problems We Identified in 2023
&lt;/h2&gt;

&lt;p&gt;Here's a quick recap of the core issues from the original post:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;2023 Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1&lt;/td&gt;
&lt;td&gt;Collection gap inflation&lt;/td&gt;
&lt;td&gt;Network jitter or performance issues cause time gaps that inflate stream aggregation deltas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2&lt;/td&gt;
&lt;td&gt;Single-node compute limits&lt;/td&gt;
&lt;td&gt;Stream aggregation has no historical state, fast but single-instance bottlenecked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P3&lt;/td&gt;
&lt;td&gt;Distributed task allocation&lt;/td&gt;
&lt;td&gt;Which compute node should each sample be assigned to?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4&lt;/td&gt;
&lt;td&gt;Out-of-order discarding for same-dimension metrics&lt;/td&gt;
&lt;td&gt;Multiple nodes computing the same dimension with different time windows causes later values to be discarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5&lt;/td&gt;
&lt;td&gt;Resource balancing&lt;/td&gt;
&lt;td&gt;Uneven load across distributed compute nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6&lt;/td&gt;
&lt;td&gt;Task ID dimension explosion&lt;/td&gt;
&lt;td&gt;Stream aggregation inserts node IDs into aggregated time series — the label cardinality grows with every node you add&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To address these, we built &lt;a href="https://github.com/Mi-Bee-Studio/stream-metrics-route" rel="noopener noreferrer"&gt;&lt;code&gt;stream-metrics-route&lt;/code&gt;&lt;/a&gt;, a Go-based distributed stream aggregation gateway.&lt;/p&gt;




&lt;h2&gt;
  
  
  II. Three Years Later — What the Official Project Has Done
&lt;/h2&gt;

&lt;p&gt;I reviewed VictoriaMetrics changelogs from v1.86 through v1.138.0 and the official documentation. Here's the scorecard.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Perfectly Resolved
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Issues P3, P5: Distributed Task Allocation &amp;amp; Resource Balancing
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Official solution:&lt;/strong&gt; vmagent now natively supports &lt;code&gt;-remoteWrite.shardByURL&lt;/code&gt; with &lt;strong&gt;consistent hashing&lt;/strong&gt; sharding.&lt;/p&gt;

&lt;p&gt;Starting from v1.86, basic &lt;code&gt;shardByURL&lt;/code&gt; was introduced. &lt;strong&gt;v1.138.0 (March 2026)&lt;/strong&gt; was the real milestone — it upgraded the data distribution algorithm from round-robin to &lt;strong&gt;consistent hashing&lt;/strong&gt;, which significantly reduces data redistribution ratios during node changes.&lt;/p&gt;

&lt;p&gt;The architecture evolution looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐     ┌─────────────────┐
│ Prometheus      │     │ Prometheus      │
│ Agent 1         │     │ Agent 2         │
└────────┬────────┘     └────────┬────────┘
         │ remote write          │ remote write
         ▼                       ▼
┌─────────────────────────────────────────┐
│           vmagent Cluster               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐│
│  │vmagent-0 │  │vmagent-1 │  │vmagent-2 ││
│  └─────┬────┘  └─────┬────┘  └─────┬────┘│
│        │             │             │     │
│        └─────────────┼─────────────┘     │
│                      ▼                   │
│           ┌──────────────────┐           │
│           │ Consistent Hash │           │
│           └────────┬─────────┘           │
└────────────────────┼─────────────────────┘
                     │ shard by hash
         ┌───────────┼───────────┐
         ▼           ▼           ▼
   ┌──────────┐ ┌──────────┐ ┌──────────┐
   │vmstorage │ │vmstorage │ │vmstorage │
   │    -0    │ │    -1    │ │    -2    │
   └──────────┘ └──────────┘ └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VictoriaMetrics blog provides specific algorithm recommendations. Combined with VictoriaMetrics Operator, you can manage shards via &lt;code&gt;shardCount&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Issue P2: Single-Node Compute Scaling
&lt;/h4&gt;

&lt;p&gt;vmagent now supports horizontal scaling natively with &lt;code&gt;replicas&lt;/code&gt; + &lt;code&gt;shardCount&lt;/code&gt;, including HA support — see &lt;a href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5573" rel="noopener noreferrer"&gt;Issue #5573&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Out-of-Order / Delayed Data Accuracy (P1 — Partial Mitigation)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;v1.112.0 (February 2025)&lt;/strong&gt; was a key release, adding &lt;strong&gt;Aggregation Windows&lt;/strong&gt; — dual-window buffering for histogram and rate calculations. Instead of flushing immediately, the output is delayed by a &lt;code&gt;samples_lag&lt;/code&gt; window, which significantly improves accuracy for late-arriving data. The tradeoff: roughly doubled memory usage (maintaining two aggregation windows simultaneously).&lt;/p&gt;

&lt;p&gt;How it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Collector          vmagent           VictoriaMetrics
   │                  │                    │
   │  sample1 @T0    │                    │
   ├─────────────────►│  Write to          │
   │                  │  Window A (current)│
   │                  │                    │
   │  sample2 @T1    │                    │
   │  (delayed)      │                    │
   ├─────────────────►│  Write to          │
   │                  │  Window B (previous)│
   │                  │                    │
   │                  │  Aggr result A @T2 │
   │                  ├────────────────────►│
   │                  │  Aggr result B @T3 │
   │                  ├────────────────────►│
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the official docs on &lt;a href="https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows" rel="noopener noreferrer"&gt;streaming aggregation windows&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Still Unresolved
&lt;/h3&gt;

&lt;h4&gt;
  
  
  True Distributed Stream Aggregation Coordination
&lt;/h4&gt;

&lt;p&gt;vmagent's stream aggregation is &lt;strong&gt;single-instance&lt;/strong&gt;. There is no coordination mechanism between instances — if two vmagent instances aggregate the same metric, you get duplicate or conflicting output. The official recommendation is to use &lt;code&gt;without&lt;/code&gt;/&lt;code&gt;by&lt;/code&gt; label clauses to divide responsibility between instances, rather than providing a cross-instance coordination protocol.&lt;/p&gt;

&lt;h4&gt;
  
  
  Task ID Dimension Explosion (P6)
&lt;/h4&gt;

&lt;p&gt;Official vmagent still inserts internal labels (such as &lt;code&gt;_aggr&lt;/code&gt;-related labels) into aggregated time series, but lacks a &lt;code&gt;stream_task_id&lt;/code&gt; pre-marking plus dimension control design.&lt;/p&gt;




&lt;h2&gt;
  
  
  III. stream-metrics-route: Current Status and Value
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Code Architecture
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;router.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Routing core — filters metrics based on relabel rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remotecluster.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Dual hashmod scheduling core&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;remotewrite.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remote write HTTP client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kafka.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kafka producer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Core Algorithm (from &lt;code&gt;remotecluster.go&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Dual hashmod scheduling&lt;/span&gt;
&lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sortLabelsHashKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dime&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hashMod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dimension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// First hashmod → task partition ID&lt;/span&gt;

&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"stream_task_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Itoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dime&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;// Insert stream_task_id label&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;hashnode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sortLabelsHashKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filterLabels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Second hashmod → node selection&lt;/span&gt;
&lt;span class="n"&gt;tmpch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hashMod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uplen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashnode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c"&gt;// Which backend writer to send to&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Is It Still Needed in 2026?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Yes — but with an adjusted role.&lt;/strong&gt; The positioning should shift from "full stream aggregation gateway" to &lt;strong&gt;"metric distribution routing gateway + Kafka integration layer."&lt;/strong&gt; The core differentiated value:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual hashmod scheduling + &lt;code&gt;stream_task_id&lt;/code&gt; pre-injection&lt;/strong&gt; — tags metrics at the gateway layer, so all downstream nodes route consistently by this ID. This solves dimension control earlier in the pipeline than the official approach.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-backend async distribution&lt;/strong&gt; — supports async distribution to both Kafka and remote write, solving the "synchronous forwarding blocks the time window" problem from the original post.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native Prometheus relabeling integration&lt;/strong&gt; — works with standard Prometheus relabel configs, no custom syntax to learn.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  IV. Recommended 2026 Hybrid Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐   ┌──────────────────┐
│  Prometheus     │   │  Business System │
│  Agent Cluster  │   │  Metrics (Kafka) │
└────────┬────────┘   └────────┬─────────┘
         │                     │
         ▼                     ▼
    ┌──────────────────────────────┐
    │   stream-metrics-route       │
    │   (Routing Layer)            │
    │   - Dual hashmod scheduling  │
    │   - stream_task_id injection │
    │   - Relabeling               │
    └──────┬──────┬──────┬─────────┘
           │      │      │
    task=0 │ task=1│task=2│
           ▼      ▼      ▼
    ┌─────────────────────────┐
    │   vmagent Cluster       │
    │  (v1.112.0+ with        │
    │   aggregation windows)  │
    └──────────┬──────────────┘
               │
               ▼
    ┌──────────────────┐      ┌────────────┐
    │  VictoriaMetrics │      │   Kafka    │
    │  (Storage)       │      │   (Topic)  │
    └──────────┬───────┘      └────────────┘
               │
               ▼
    ┌──────────────────┐
    │  vmalert         │
    │  Grafana         │
    └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;vmagent version requirement:&lt;/strong&gt; &amp;gt;= v1.112.0, with aggregation windows enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stream aggregation config&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http_request_duration_seconds_bucket'&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
  &lt;span class="na"&gt;without&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;instance&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;enable_windows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# Critical! Enables dual-window buffering&lt;/span&gt;
  &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;rate_sum&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  V. Evolution Recommendations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Short-term
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade vmagent to &amp;gt;= v1.112.0&lt;/td&gt;
&lt;td&gt;Enable &lt;code&gt;enable_windows: true&lt;/code&gt; to improve histogram aggregation accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluate stream-metrics-route necessity&lt;/td&gt;
&lt;td&gt;If you have no Kafka requirement or high-cardinality &lt;code&gt;stream_task_id&lt;/code&gt; control need, consider migrating away&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Medium-term
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retain stream-metrics-route as front-end routing only&lt;/td&gt;
&lt;td&gt;Keep hashmod task allocation + Kafka distribution; remove aggregation responsibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disable raw metric persistence&lt;/td&gt;
&lt;td&gt;Write only stream-aggregated results to storage to reduce volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add metadata management module&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;ruler-handle-process&lt;/code&gt; from the original post (dynamic Record Rule by dimension) is worth building or contributing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Long-term
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contribute &lt;code&gt;stream_task_id&lt;/code&gt; dimension control upstream&lt;/td&gt;
&lt;td&gt;If the dual hashmod design proves out in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Improve monitoring metrics&lt;/td&gt;
&lt;td&gt;Add business-level metrics — queue depth per routing rule, distribution latency, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Assessment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Problem resolution rate&lt;/td&gt;
&lt;td&gt;~50% — 2 of 4 core problems resolved via official upgrades; 2 still need custom solutions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is stream-metrics-route still needed?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — repositioned as "metric distribution routing gateway + Kafka integration layer"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommended architecture&lt;/td&gt;
&lt;td&gt;Prometheus → stream-metrics-route → vmagent v1.112.0+ → VictoriaMetrics Storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three years is a long time in the observability space. The VictoriaMetrics ecosystem has matured significantly — consistent hashing, aggregation windows, and native sharding all address problems that required custom tooling in 2023. But the hard problems around true &lt;em&gt;distributed&lt;/em&gt; stream aggregation coordination and dimension control at the gateway layer remain open.&lt;/p&gt;

&lt;p&gt;If you're running a similar stack, the hybrid approach — letting the official project handle what it's good at (single-node aggregation, storage) while keeping custom routing for what it isn't (distributed coordination, dimension pre-injection, Kafka bridging) — has proven to be the right call for us.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.mickeyzzc.tech/en/posts/telemetry/stream-metrics-two/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>performance</category>
    </item>
    <item>
      <title>Scaling VictoriaMetrics Stream Aggregation: A Deep Dive into Distributed Design（2023）</title>
      <dc:creator>ZhengZhiCong</dc:creator>
      <pubDate>Sun, 07 Jun 2026 22:50:31 +0000</pubDate>
      <link>https://dev.to/mickey_zzc/scaling-victoriametrics-stream-aggregation-a-deep-dive-into-distributed-design-629</link>
      <guid>https://dev.to/mickey_zzc/scaling-victoriametrics-stream-aggregation-a-deep-dive-into-distributed-design-629</guid>
      <description>&lt;p&gt;VictoriaMetrics' stream aggregation is a powerful feature for reducing metric cardinality in real time. But when you push it to millions of time series across a distributed fleet, the native implementation starts to show cracks.&lt;/p&gt;

&lt;p&gt;This post walks through what I found analyzing the stream aggregation source code, the real-world problems that emerged at scale, and the distributed gateway we built to solve them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Community VM Stream Aggregation — Capability Analysis
&lt;/h2&gt;

&lt;p&gt;Stream aggregation was integrated into &lt;code&gt;vmagent&lt;/code&gt; starting from version 1.86 (&lt;a href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3460" rel="noopener noreferrer"&gt;GitHub issue #3460&lt;/a&gt;). Let's look at what it actually does under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Computation: The &lt;code&gt;pushSample&lt;/code&gt; Function
&lt;/h3&gt;

&lt;p&gt;The heart of stream aggregation lives in the &lt;code&gt;pushSample&lt;/code&gt; function. Here's the simplified logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;totalAggrState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;pushSample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outputKey&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fasttime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnixTimestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;deleteDeadline&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intervalSecs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intervalSecs&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;again&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;totalStateValue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lastValues&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;lastValueState&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;vNew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadOrStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vNew&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;sv&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;totalStateValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;deleted&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleted&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;deleted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastValues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inputKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lastValueState&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
            &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastValues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inputKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lv&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ignoreInputDeadline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleteDeadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deleteDeadline&lt;/span&gt;
        &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleteDeadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deleteDeadline&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;deleted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;goto&lt;/span&gt; &lt;span class="n"&gt;again&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core idea: each incoming sample's value is compared against the last seen value for that time series. The &lt;em&gt;delta&lt;/em&gt; is accumulated into a running total. This works well for counters that monotonically increase, but there's a critical detail — the time window logic is simple periodic checking with no sophisticated handling for delayed or out-of-order arrivals.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Stream Aggregation Looks Like in Practice
&lt;/h3&gt;

&lt;p&gt;In theory, stream aggregation should cleanly reduce high-cardinality metrics down to manageable summaries. In practice, the picture is messier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ideal model&lt;/strong&gt;: every sample arrives on time, windows align perfectly, aggregation is lossless&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reality&lt;/strong&gt;: samples arrive late, retries flood old data, gaps appear from network or service issues&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Issues with Native Stream Aggregation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Collection Gap Problem
&lt;/h3&gt;

&lt;p&gt;Collection gaps are inevitable. Network blips, service restarts, GC pauses — any of these can cause a gap in metric collection. For high-precision stream aggregation, gaps create a specific failure mode:&lt;/p&gt;

&lt;p&gt;When a counter's last tracked value is &lt;code&gt;1000&lt;/code&gt;, and a gap causes the next received value to be &lt;code&gt;5000&lt;/code&gt;, the delta is &lt;code&gt;4000&lt;/code&gt; — which may span an unknown number of actual increments. If the gap occurred &lt;em&gt;within&lt;/em&gt; a single aggregation window, the inflated value corrupts that window's result. If the gap crosses window boundaries, you get compounding errors in downstream calculations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distributed Computing Challenges
&lt;/h3&gt;

&lt;p&gt;Stream aggregation doesn't persist historical data, so it's fast — but even the fastest service has single-node limits. When you need to scale horizontally, a cascade of new problems appears:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is vmagent's built-in collection viable at scale?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In testing, enabling vmagent shard + replica collection with real-time stream aggregation caused significant resource spikes. At very large scales, collection gaps became more frequent, which &lt;em&gt;amplified&lt;/em&gt; the calculation errors from gaps rather than mitigating them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which compute node should each sample be assigned to?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without a consistent routing strategy, the same metric dimension gets split across nodes, producing partial results that can't be safely combined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle the same dimension set being computed by multiple nodes with different values?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When multiple nodes produce results for the same dimension within the same time window, VictoriaMetrics triggers its out-of-order handling logic — which discards later values. You lose data silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you balance resources in distributed computation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Uneven distribution means some nodes are overloaded while others sit idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about the new dimensions introduced by routing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you insert a task ID to differentiate compute nodes, you've just added a new dimension that grows with every node you add — defeating part of the purpose of aggregation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design and Implementation: A Distributed Stream Aggregation Gateway
&lt;/h2&gt;

&lt;p&gt;After analyzing these problems, it became clear that a frontend module was needed to address them at the entry point. Since &lt;code&gt;vmgateway&lt;/code&gt; is an enterprise component, we built our own: &lt;strong&gt;&lt;code&gt;vm-receive-route&lt;/code&gt;&lt;/strong&gt;, a distributed stream aggregation gateway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Insight from Source Code
&lt;/h3&gt;

&lt;p&gt;Two aspects of the native implementation are particularly relevant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time window range:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fasttime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnixTimestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;deleteDeadline&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intervalSecs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intervalSecs&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window has a 50% grace period (&lt;code&gt;intervalSecs &amp;gt;&amp;gt; 1&lt;/code&gt;) beyond the configured interval. This is the only protection against late-arriving data — and it's quite generous, which means stale data can still influence results within that extended window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calculation logic:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastValues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inputKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;lv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lastValueState&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastValues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inputKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lv&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ignoreInputDeadline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The aggregation only checks if a previous value exists and is smaller — it doesn't handle the case where a late-arriving sample with a lower value (after a restart, for example) creates a negative delta that's simply ignored. There's no deduplication, no gap detection, no intelligent merging.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Gateway Solves
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Asynchronous Processing
&lt;/h4&gt;

&lt;p&gt;Most &lt;code&gt;remote write&lt;/code&gt; adapters (like &lt;code&gt;prometheus-kafka-adapter&lt;/code&gt;) do synchronous forwarding — they wait for the downstream (Kafka, etc.) to acknowledge before accepting the next batch. Stream aggregation has &lt;em&gt;window constraints&lt;/em&gt;: if the write pipeline blocks and samples arrive late, they miss their window and calculations drift.&lt;/p&gt;

&lt;p&gt;The gateway decouples ingestion from forwarding using an internal buffer. The &lt;code&gt;remote write&lt;/code&gt; endpoint returns immediately, and samples are forwarded asynchronously to the stream aggregation backend. This prevents back-pressure from creating cascading delays.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Time Window Filtering
&lt;/h4&gt;

&lt;p&gt;Since stream aggregation already computes deltas between successive values, there's no need for complex out-of-order handling at this layer. The gateway simply cooperates with the aggregation window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Samples arriving within the window → forward normally&lt;/li&gt;
&lt;li&gt;Samples arriving outside the window → discard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This solves two problems at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus retries that send large backlogs of old samples no longer corrupt real-time results&lt;/li&gt;
&lt;li&gt;The resource overhead of processing those retries is eliminated at the gateway level — the aggregation backend never sees them&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Dimension Control
&lt;/h4&gt;

&lt;p&gt;The stream aggregation component inserts a node ID into each aggregated time series to distinguish labels across compute nodes. But as nodes scale horizontally, the cardinality of &lt;em&gt;that label&lt;/em&gt; scales too. You need a way to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Control dimension growth from node identities&lt;/li&gt;
&lt;li&gt;Route time series by dimension to the correct node&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We designed a &lt;strong&gt;dual hashmod scheduling algorithm&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The gateway assigns a hash-based task ID to each time series based on its stable dimensions (not the node identity)&lt;/li&gt;
&lt;li&gt;The same series always routes to the same compute node, regardless of which gateway instance processed it&lt;/li&gt;
&lt;li&gt;The task ID dimension is bounded by the number of unique dimension combinations, not the number of gateway nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By moving the task ID labeling to the gateway layer, we eliminated the unbounded dimension growth that horizontal scaling would otherwise cause.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Backend Service Migration for Failures
&lt;/h4&gt;

&lt;p&gt;When a compute node fails, its in-memory aggregation state is lost. The gateway detects failures via health checks and reroutes traffic to healthy nodes. Since the dual hashmod ensures consistent routing, the remaining nodes can immediately pick up the work, though there will be a brief period of incomplete aggregation until the state rebuilds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Record Rule Dimension Task Generator
&lt;/h2&gt;

&lt;p&gt;Stream aggregation is great for reducing cardinality of &lt;em&gt;single metrics&lt;/em&gt;. But real-world monitoring scenarios require combining multiple metrics with functions — which is where Prometheus &lt;code&gt;Record Rule&lt;/code&gt; comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with Record Rules at Scale
&lt;/h3&gt;

&lt;p&gt;Consider an HTTP request metric with a &lt;code&gt;req_path&lt;/code&gt; dimension:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before stream aggregation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;a_http_req_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"30021"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dis_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"202"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;req_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/foo?abc=xyz"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;a_http_req_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"30023"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dis_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"202"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;req_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/bar?abc=def"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;a_http_req_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10021"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dis_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"202"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;req_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/api/baz?abc=ghi"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After stream aggregation (dropping &lt;code&gt;req_path&lt;/code&gt; and &lt;code&gt;src_port&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;agg_a_http_req_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dis_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"202"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;agg_a_http_req_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dis_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"500"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;agg_a_http_req_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bj"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;src_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dis_svr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"192.168.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"400"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what you &lt;em&gt;actually&lt;/em&gt; want to display is the success rate per target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum by (dis_svr) (
    rate(a_http_req_total{code=~"2.*"}[5m])
)
/
sum by (dis_svr) (
    rate(a_http_req_total{}[5m])
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use &lt;code&gt;Record Rule&lt;/code&gt; to precompute this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;a_http_req_total:sum:rate:5m&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum by (src_svr, dis_svr, code) (rate(a_http_req_total{}[5m]))&lt;/span&gt;
        &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;a_http_req_total:sum:rate:5m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem? &lt;code&gt;Record Rule&lt;/code&gt; loads &lt;em&gt;all&lt;/em&gt; dimensions of the metric into memory. When dimension counts reach critical thresholds, it triggers OOM. Even below that threshold, higher cardinality means slower computation.&lt;/p&gt;

&lt;p&gt;In production, &lt;code&gt;istio_requests_total&lt;/code&gt; QPS could be delayed by &lt;strong&gt;20 minutes&lt;/strong&gt; at high dimension counts. After applying stream aggregation to reduce from tens of millions of time series down to tens of thousands, the delay dropped to 1-2 minutes — better, but still far from real-time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Dimension-Split Record Rules
&lt;/h3&gt;

&lt;p&gt;The issue is that &lt;code&gt;Record Rule&lt;/code&gt; evaluates one query per group, loading everything into memory. But if you split the query by specific dimension values, you can process them concurrently.&lt;/p&gt;

&lt;p&gt;The static approach after stream aggregation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agg_a_http_req_total:sum:rate:5m-2xx&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum by (src_svr, dis_svr, code) (rate(agg_a_http_req_total{code=~"2.*"}[5m]))&lt;/span&gt;
        &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agg_a_http_req_total:sum:rate:5m&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agg_a_http_req_total:sum:rate:5m-4xx&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum by (src_svr, dis_svr, code) (rate(agg_a_http_req_total{code=~"4.*"}[5m]))&lt;/span&gt;
        &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agg_a_http_req_total:sum:rate:5m&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agg_a_http_req_total:sum:rate:5m-5xx&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum by (src_svr, dis_svr, code) (rate(agg_a_http_req_total{code=~"5.*"}[5m]))&lt;/span&gt;
        &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agg_a_http_req_total:sum:rate:5m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the problem is that in production, dimension labels are dynamic. You can't hardcode splits for every dimension value. You need a &lt;strong&gt;label metadata management system&lt;/strong&gt; that watches dimension combinations and dynamically generates split queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;ruler-handle-process&lt;/code&gt; Component
&lt;/h3&gt;

&lt;p&gt;We built a small metadata watch and rule builder that automates this. Its configuration looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;recode_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
    &lt;span class="na"&gt;recode_to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio_requests_total:sum:rate:5m&lt;/span&gt;
    &lt;span class="na"&gt;metric_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio_requests_total&lt;/span&gt;
    &lt;span class="na"&gt;aggr_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
    &lt;span class="na"&gt;vector_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate&lt;/span&gt;
    &lt;span class="na"&gt;vector_range&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
    &lt;span class="na"&gt;group_by_and_filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;source_workload&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;destination_workload&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cluster&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;namespace&lt;/span&gt;
    &lt;span class="na"&gt;group_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;response_code&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;namespace&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;source_workload_namespace&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;destination_workload_namespace&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;destination_service_name&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cluster&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;reporter&lt;/span&gt;
    &lt;span class="na"&gt;filter_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k8s-hw-bj-xxxxxx"&lt;/span&gt;
    &lt;span class="na"&gt;with_out&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;source_workload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingressgateway-workflows"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component watches the actual dimension combinations under the metric name &lt;code&gt;istio_requests_total&lt;/code&gt; and generates a set of &lt;code&gt;Record Rule&lt;/code&gt; configurations — one per unique dimension combination. Combined with Prometheus's Rule component for concurrent evaluation, this reduced computation latency from minutes to &lt;strong&gt;seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The generated rules look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio_requests_total:sum:rate:5m-7218756fe8a0bc327e818812cefb02f7&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum by (...) (rate(istio_requests_total{cluster="k8s-hw-bj-1-prod", destination_workload="skyaxe-778-flink", ...}[5m]))&lt;/span&gt;
        &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio_requests_total:sum:rate:5m&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio_requests_total:sum:rate:5m-8e30244048f8d5519a6332f309578ed4&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum by (...) (rate(istio_requests_total{cluster="k8s-hw-bj-1-prod", destination_workload="t-bean-portal", ...}[5m]))&lt;/span&gt;
        &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;istio_requests_total:sum:rate:5m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each unique dimension combination gets its own rule group, enabling true concurrent computation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Combinations for Different Scales
&lt;/h2&gt;

&lt;p&gt;Using community open-source components alongside our custom gateway and rule builder, we assembled a tiered architecture that handles everything from small deployments to massive fleets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small Scale: Tens of Thousands of Single Metrics
&lt;/h3&gt;

&lt;p&gt;Minimal setup — vmagent with built-in stream aggregation. No gateway needed. The single-node limits aren't reached yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium Scale: Tens of Thousands of Multi-Metrics
&lt;/h3&gt;

&lt;p&gt;Add Record Rules for computed metrics. Use the dimension-split approach to keep computation fast. Stream aggregation reduces cardinality before Record Rule evaluation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Large Scale: Millions+ of Single Metrics
&lt;/h3&gt;

&lt;p&gt;This is where the distributed gateway comes in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple vmagent instances behind the gateway for horizontal ingestion&lt;/li&gt;
&lt;li&gt;Dual hashmod scheduling ensures consistent routing&lt;/li&gt;
&lt;li&gt;Time window filtering at the gateway prevents retries from polluting results&lt;/li&gt;
&lt;li&gt;Asynchronous forwarding prevents back-pressure from creating cascading delays&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Large Scale: Scenario-Based Computational Aggregation for Millions of Metrics
&lt;/h3&gt;

&lt;p&gt;The full stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stream aggregation&lt;/strong&gt; — first pass cardinality reduction at ingestion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed gateway&lt;/strong&gt; — routing, filtering, and load distribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic rule builder&lt;/strong&gt; — watches dimension metadata and generates concurrent Record Rule groups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule engine&lt;/strong&gt; — evaluates split queries in parallel&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;VictoriaMetrics' stream aggregation is a solid foundation, but it was designed for single-node operation. When you need to scale horizontally, you encounter a series of interconnected problems — collection gaps, dimension explosion, record rule performance bottlenecks — that the native implementation doesn't address.&lt;/p&gt;

&lt;p&gt;The solutions aren't particularly complex individually (async processing, time window filtering, hash-based routing, dimension-aware rule generation), but they need to work together as a coherent system. The gateway pattern — intercepting the &lt;code&gt;remote write&lt;/code&gt; path before it reaches the aggregation layer — proved to be the right abstraction point for injecting these capabilities without modifying upstream code.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ruler-handle-process&lt;/code&gt; component was the second key insight: rather than fighting Prometheus Record Rule's single-query-per-group limitation, we embraced it by dynamically splitting queries by dimension and running them concurrently. This turned a 20-minute computation into a seconds-level operation.&lt;/p&gt;

&lt;p&gt;If you're running VictoriaMetrics at scale and hitting these issues, the patterns described here should be applicable regardless of your specific stack. The gateway approach is generic enough to work with any Prometheus-compatible &lt;code&gt;remote write&lt;/code&gt; backend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.mickeyzzc.tech/en/posts/telemetry/stream-metrics-one/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>monitoring</category>
      <category>performance</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
