1Objective
Between 18:00 and 06:00, when a Dahua camera detects a person or a vehicle, the CenterV2 monitoring agent should see a bounding box drawn on the live video.
The constraint that shapes every decision below: CenterV2 will not render third-party metadata. Any design that ships box coordinates alongside the video and hopes the client draws them is dead on arrival.
The box has to be in the pixels. Once it is in the pixels it survives GeoVision, the forward to CenterV2, and the recording — nothing downstream has to understand it.
2Where the project stands
Settled with a full config dump. The overlay node simply does not exist on this hardware.
One core's worth covers roughly 18 cameras. The video path is affordable.
Correct for it and the box sits on the target. Proven on video.
18 MB instead of 4 GB across twelve cameras. This decides the trailer hardware.
| Item | State | Note |
|---|---|---|
| Camera can burn boxes itself | Ruled out | Evidence in §4.1 |
| Box coordinate format | Confirmed | Verified arithmetically against a live event |
| Pilot recorder | Proven | Ran live 2026-08-19; alignment confirmed on video |
| Live RTSP proxy | Running | Built and publishing 2026-08-19 |
| GeoVision integration | Connected | Reading the proxy stream over TCP on port 554 |
Open questions
| Question | Why it matters | Blocking |
|---|---|---|
| GeoVision channel licensing | The recommended design needs two channels per camera. At 12 cameras that is 24. | Needs answer |
| Substream keyframe interval | Currently one every 4 seconds. GeoVision would show black on connect, which reads as a dead camera. | Needs approval |
| MediaMTX download | Required to serve the RTSP stream the proxy produces. | Needs approval |
| Email alerting | Deferred by the operator. No credentials stored anywhere. | Parked |
3Architecture decisions
3.1 A re-encoding proxy is required
Two approaches were rejected. Having the camera burn the boxes is not supported on this hardware (§4.1). Shipping metadata to CenterV2 fails because it will not render it.
Accepted: decode the camera substream, draw, re-encode, republish as RTSP. GeoVision accepts a generic RTSP source, which makes this clean to wire up.
3.2 Two GeoVision channels per camera
Channel A connects straight to the camera mainstream and is what records. Evidence stays clean, with no synthetic annotation burned into it, and if the proxy dies the recording is untouched. Channel B carries the proxy output and is the only channel mapped to CenterV2 for live view.
The cost is double the channel count. The alternative — a single channel through the proxy — puts synthetic marks into evidence video and makes the proxy a single point of failure for evidence. That trade is a management call, not an engineering default.
3.3 The live proxy has to encode continuously
The clip recorder can sleep when the scene is empty. The live proxy cannot: a VMS treats a stream that stops as a failed camera and raises an alarm. Decode plus encode is a permanent floor whenever the proxy is up, not an event-triggered cost.
The obvious optimization — pass the stream through untouched while idle and only re-encode during detections — does not work. Switching codecs mid-session changes the stream headers and breaks timestamp continuity, so the client disconnects. That is exactly the failure we were trying to avoid.
3.4 The box the operator sees comes from our detector, not the camera
The camera's metadata is event-shaped, not track-shaped (§4.3). It cannot produce a box that follows a person across frames. It is valuable as a trigger — "look here now" — but the box drawn on screen should come from the verified Hailo detector.
Drawing unverified camera boxes would give false positives visual legitimacy on the operator's screen. That could make alert quality worse, not better, which is the opposite of the point.
3.5 Trailer hardware: video plumber, not brain
The Intel N150 has four efficiency cores and Quick Sync. It handles the video path if we use hardware encoding rather than software (§5.2). It cannot run continuous neural detection for several cameras — no usable NPU, weak integrated graphics. Boxes have to arrive from the camera or from the Hailo box over the network.
4Research findings
Everything below came from device 172.16.0.36 — an IPC-HFW5442E-Z4E on
firmware 2.840.0000000.13.R, build 2022-01-10.
4.1 The camera cannot burn boxes into the encoded stream Confirmed
A full read of the camera's overlay configuration returns only channel title, timestamp, custom titles, privacy masks, the PTZ on-screen displays, temperature and voltage. There is no rule-overlay or target-overlay node, so there is no flag to switch on for detection boxes.
This explains something worth internalizing: the green boxes visible in the Dahua web interface are not in the video. The browser draws them from a parallel metadata feed. The pixels never carried them, which is why GeoVision has never shown them and why no camera setting will produce them.
Whether a newer WizSense S3/S5 model behaves differently. Worth one check before treating this as a fleet-wide rule.
4.2 Box coordinate format Confirmed
Boxes arrive as corners — not position-plus-size — in Dahua's normalized 0–8192 space rather than pixels. This was verified, not inferred: the payload's own center field is exactly the midpoint of the box it ships with.
| Source | Field | Format | Space |
|---|---|---|---|
| Camera tripwire | Object.BoundingBox | [x1, y1, x2, y2] | 0–8192 |
| Camera smart motion | object[].Rect | [x1, y1, x2, y2] | 0–8192 |
| Frigate HTTP API | data.box | [x, y, w, h] | 0–1 |
| Frigate MQTT | after.box | [x1, y1, x2, y2] | pixels |
The two camera sources disagree with each other. For the same tracked object they reported different rectangles — different detectors, different instants. They must never be averaged or mixed.
These are the third and fourth box formats in the wider Jatagan work. Confusing formats has already produced one false conclusion on the AI Box project, so the rule now is to name the source before reasoning about any box.
4.3 What the metadata does not contain Confirmed
Across four real tripwire events:
- No confidence score. The field is present but always zero. There is nothing to threshold on.
- No trajectory. The track array is empty every time.
- No tracking. The start and stop events carry the identical frozen rectangle. It is a snapshot at the moment of crossing, not a box that follows anyone.
This is what forces decision 3.4.
4.4 Timestamps align to the millisecond Confirmed
Each event carries a presentation timestamp that correlates with the payload clock to within a few milliseconds across sampled pairs. This is the mechanism for placing a box on its exact frame without guessing at network latency.
4.5 Traps worth writing down
| Trap | Consequence | Status |
|---|---|---|
| The smart-motion object ID field is misspelled in the firmware | The parser has to misspell it too, deliberately | Confirmed |
| The event clock field appears to carry local time dressed as UTC | Reading it as true UTC would shift alignment by whole hours | Inferred — calibrate |
| Command-line tooling treats array indices in a URL as wildcards | A configuration write silently failed and returned an empty body | Confirmed & solved |
4.6 Side findings on the tripwire rules
Outside this project's scope, but observed in the same captures and directly affecting alert quality, so recorded here rather than lost.
- The active rule carries eight vertices for what should be a two-point tripwire, with the last two jumping back across the scene. The camera evaluates all eight.
- Observed consequence: two different people each triggered two crossings in opposite directions about two seconds apart. Two out of two. Every person crossing that rule may be generating two alerts. The fix is redrawing the tripwire in the camera, not in code.
- Both active rules have no size filtering at all, so a two-pixel object at the back of the scene qualifies the same as a person at ten meters.
5Measurements
All taken on the engineering workstation (28 cores) against the pilot camera, D1 substream. The trailer hardware is weaker, so these are ratios to scale from, not absolute budgets.
5.1 Idle path — listener plus rolling frame buffer
| Substream rate | One camera | Twelve cameras |
|---|---|---|
| 30 fps | 2.3% of a core | 0.27 cores |
| 15 fps | 1.4% of a core | 0.16 cores |
The drop is not exactly half because the event connection costs the same either way.
5.2 Encoder benchmark — one camera, D1 at 15 fps
| Path | CPU | Twelve cameras, always on |
|---|---|---|
| Pass through, no decode | 0.26% of a core | 0.03 cores |
| Software encode | 7.6% | 0.91 cores |
| Hardware encode (Quick Sync) | 5.3% | 0.64 cores |
NVIDIA encoding is compiled into our ffmpeg but unusable on this workstation — there is no NVIDIA GPU in it. On the N150 expect software encoding to scale far worse and Quick Sync to hold roughly steady, since it is a dedicated block rather than general-purpose cores.
5.3 Memory — the twelve-second pre-roll buffer
A clip has to start before the trigger, otherwise the video begins after the subject has already walked into frame. That means keeping recent video in memory at all times. How we keep it is the single most consequential decision for the trailer hardware.
| Buffer contents | One camera | Twelve cameras |
|---|---|---|
| Decoded frames at 30 fps | ~350 MB | 4.08 GB |
| Decoded frames at 15 fps | ~174 MB | 2.04 GB |
| Compressed packets | ~1.5 MB | 18 MB |
A 200× difference. An N150 machine typically ships with 8–16 GB, so four gigabytes of buffers alone is not viable and two is uncomfortable. Eighteen megabytes is free.
6Pilot results
The pilot ran live against the parking-lot camera on the morning of 2026-08-19. It was never meant to prove we can draw a rectangle — that was never in doubt. It was built to answer one question: does the box land on the right person at the right frame?
Reading the alignment off the video
| Frame | Timestamp | What is on screen |
|---|---|---|
| 62 | 08:25:55.660 | Box drawn; the person is still to the right of it |
| 78 | 08:25:56.704 | The box lands on the person |
| 94 | 08:25:57.788 | The person has walked past; the box sits behind them |
The detection event reached us at 08:25:57.508, but it describes the frame at 08:25:56.704. That is a lag of −0.8 seconds between the event and the video it refers to. Correct for it and the box sits on the target.
The 0.8 seconds includes this workstation's network buffering. On the trailer hardware it will be a different figure. The production answer is the presentation timestamp carried inside each event (§4.4), which points at the exact frame regardless of anyone's latency. This measurement proves the alignment is achievable; it is not a constant to hard-code.
What the window produced
| Measure | Result |
|---|---|
| Detections captured | 15 |
| Clips recorded | 16 |
| Event blocks dropped | 0 |
| Object types seen | Human and Vehicle |
| Usable window after three defects | ~65 of 120 min |
The concept is proven. A box can be placed on the correct target on the correct frame, burned into video that any downstream system will carry without understanding it. The risk that could have killed the proxy is cleared.
Making the box follow the subject
The first clip exposed a limit: each box is frozen at the instant of its detection, so a subject walking through frame produced a rectangle that blinked in place while they moved past it. That looked like the ceiling of what camera data could do. It was not.
Every event carries an object id, and the camera keeps it stable across event types. One person walking to a tripwire produced three events under a single id, with the box at a different position each time. We were receiving a coarse track and discarding it by drawing each box on an independent timer. Grouping the samples by object and filling the gaps between them turns three blinks into one continuous box that moves with the subject.
The camera's own coverage reaches further than expected: the first event fired 1.4 seconds before the subject reached the tripwire, so the box is already on them as they approach, not only at the moment of crossing.
Two honest limits. Interpolation assumes constant velocity between samples, so a subject who reverses direction mid-gap will trace a slightly wrong path. And drawing a frame requires knowing the box that comes after it, so frames are held ~2 seconds before being rendered — on a live stream that is real delay for the operator, and it has to be accepted deliberately rather than discovered later.
A second job for the box: making the VMS see it
Locating the subject for the operator is only half of what the box is for. The other half is mechanical: GeoVision's motion detection has to fire so the event is guaranteed to reach CenterV2. Rather than hope the scene trips it, the box manufactures the motion by pulsing between red and amber — two colours far apart in brightness, which is what a motion detector actually measures.
Contrast was never the problem. Area was. Measured against this scene, where wind and shadows already move 0.60% of the frame between consecutive frames:
| Box style | Pixels changed | % of frame |
|---|---|---|
| 2 px outline (original) | 1,016 | 0.30% |
| 6 px outline | 2,360 | 0.70% |
| 10 px outline | 3,700 | 1.09% |
| 6 px outline + solid corner patch | 10,243 | 3.03% |
An outline has almost no area by definition. The original 2 px box changed half as many pixels as the wind did — invisible to any detector. Even at 10 px it barely matches the noise. So the solid corner patch became the mechanism and the border its companion: together they clear the scene's own noise by more than six times.
The border and the patch deliberately pulse at different rates. The border throbs at 2.5 Hz — calm enough for someone watching monitors all shift. The patch alternates on every single frame, so the full colour step sits between every consecutive pair. Pulsing both slowly left two of every three frame pairs nearly static, which would fail a detector that needs sustained rather than intermittent change.
This injects synthetic motion into video that GeoVision will then report as motion detected. The originating detection is real, so no false alarm is manufactured — but the resulting VMS event looks like independent corroboration and is not. Channel A keeps recording clean from the camera, and the mechanism is written down here so no future reader treats the VMS log as a second opinion.
Two things remain unproven. All of this is measured against our own motion estimator, not GeoVision's — the real test is pointing the VMS at the stream. And the camera's event cadence is irregular: gaps of 1.4 seconds are common but 5-second gaps happen, and a frame can only be interpolated once the box after it has arrived. Where the gap outruns that window the box drops out mid-transit, which is visible in the clip above. Holding the last position longer papers over it; the real fix is a tracker that does not wait on events.
Where per-frame motion would have to come from
The camera reports roughly one box per object every 1 to 1.5 seconds. That is the hard ceiling of camera-supplied data, so anything smoother has to be produced by us.
One possibility was ruled out by measurement. The event stream contains
IntelliFrame markers, which suggested the camera might embed object metadata
inside the video itself — which would have given per-frame boxes for free. Raw H.264
was dumped from both streams and every embedded data unit inspected: 7 in the substream, 21
in the mainstream, all of a single vendor type carrying one byte that
simply counts up once per keyframe. No object data on either stream. That lead is closed and
does not need revisiting.
Smoother motion therefore means either the interpolation above, or a lightweight visual tracker seeded by each camera event — cheap enough for the trailer hardware and self-correcting, since every new event re-anchors it.
7Live integration
On the afternoon of 2026-08-19 the chain ran end to end for the first time: camera to proxy to streaming server to GeoVision to the operator's view. What had been a clip recorder became a live source a VMS can consume.
7.1 What runs now
A continuous proxy decodes the camera substream, draws tracked boxes, and publishes H.264 over RTSP to a local streaming server that GeoVision pulls from. Unlike the clip recorder it never sleeps: a VMS reads a stream that stops as a failed camera, so decode and encode are a permanent floor rather than an event-triggered cost.
7.2 Measured latency
| Configuration | Measured | Predicted beforehand |
|---|---|---|
| Smooth motion (frames held 2 s) | 2.46 s | 2.4 s |
| Responsive (no hold, box steps) | 0.47 s | 0.4 s |
The gap between the two is 1.99 s — exactly the hold, which is what makes the measurement trustworthy. Method: the proxy burns each frame's capture time into the picture, so reading that stamp at the far end measures everything the proxy adds. A first reading of 4.05 s was discarded because it included the cost of opening a cold connection.
The delay is a choice, not a limit. Eighty-three percent of it is the deliberate hold. The camera only reports a box every 1 to 1.5 seconds either way, so interpolation adds no information — only smoothness. For a live operator, half a second is a better trade than two and a half.
7.3 The integration failure worth recording
Connecting GeoVision took about an hour, and almost all of it was spent looking in the wrong place. The stream was published on port 8554 and the URL handed to GeoVision said 8554, but GeoVision ignores the port in the URL and connects on 554, the RTSP default.
The failure mode is what made it slow: a client calling a closed port leaves no trace anywhere. The server log stayed empty, which looked like evidence that nothing was attempting to connect. Hours went into the session description, the encoder profile, the firewall, and the third-party licence — all of them fine. Moving the server to port 554 produced a connection within seconds.
An empty log proves that nothing reached that log. It does not prove that nothing was sent. When a component reports it is trying and the receiver sees nothing, the first question is whether both are talking about the same address — not what is wrong with the payload.
Along the way one real defect surfaced and was fixed: the proxy had the port written into its startup check by hand, so moving the server left it refusing to start against a port nobody was listening on. It now derives the port from the address it publishes to.
8Change log
8.1 Changes to live equipment
| Date | Device | Change | Reversible | Verified |
|---|---|---|---|---|
| 2026-08-19 | 172.16.0.36 | Substream frame rate 30 → 15. Mainstream untouched. | Yes | Config re-read & stream confirmed |
Nothing else on any camera, recorder or remote host has been modified by this project. Every other device interaction was read-only. The frame rate change was explicitly authorized in session; the pending keyframe change is not covered by that authorization and will be raised separately.
8.2 Local artifacts created
| Path | Purpose |
|---|---|
| C:\Users\lhuer\jatagan-bbox\ | Project root |
| bbox_clip_pilot.py | The pilot: listener, buffer, recorder, notifier |
| synthetic_test.py | Injects a fake detection so the record path can be validated without waiting for a person |
| measure_idle.py | Idle CPU measurement |
| measure_ram.py | Buffer memory measurement |
| LOGBOOK.md | Full local version of this document |
| C:\Users\lhuer\.jatagan\ | Credentials, deliberately outside any project folder so they never reach a repository |
| E:\jatagan-bbox-pilot\ | Clip output, sidecar data, run log |
8.3 Defect found and fixed during the pilot
Clip frame rate was hardcoded while the live substream ran at a different rate, so clips would have played at roughly half speed. The buffer now measures the real arrival rate and writes the clip at that rate. Caught by test T5, confirmed fixed by T6.
9Corrections to earlier conclusions
Kept deliberately. Each of these was believed and acted on before being disproven, and the record is more useful with them than without.
9.1 The camera does not give us a free trajectory
An earlier note recorded that tripwire events carried the camera's own computed trajectory, described as "a free second opinion." Four real events show it empty every time. Any design leaning on camera-supplied tracking had no foundation.
9.2 Buffering compressed packets — right call, wrong reason
First stated as the top processor optimization. Then measured: continuous decoding costs 1.4% of a core, so as a processor optimization it is nearly pointless, and it was deprioritized. Then measured from the memory side, where it is a 200× difference.
It is back to priority one — as a memory decision, not a processor one. On a 28-core workstation with abundant RAM the problem is invisible. On the trailer hardware it decides whether the system runs at all.
9.3 "An empty scene costs nothing"
True for the clip recorder, false for the live proxy. See 3.3.
9.4 Three defects in the measuring instrument itself
The pilot lost roughly 55 minutes of a 120-minute window to three defects in the listener, all of them ours. They are recorded because the failure mode matters more than the fix.
| Defect | Effect | Cost |
|---|---|---|
| Header/body split assumed Unix line endings; the camera sends Windows ones | Every event silently discarded | 45 min |
| Read buffer sized larger than the traffic could fill | A detection could sit unread for minutes on a quiet stream | — |
| First attempt at fixing the buffer made it block forever | Stream completely dead | 10 min |
For 45 minutes the status display read "connected" while the listener understood nothing at all. A green light that only proves a socket is open is not a green light. The status line now counts blocks received and blocks dropped separately, so a connected-but-deaf stream announces itself instead of looking like a quiet night. This project exists to stop physical-security work from failing quietly, and the measuring instrument had exactly that flaw built into it.
The working answer was already in the building: the tripwire notifier in the earlier snapshot project has read this same stream correctly for months. Reading it first would have saved all three.
10Resource inventory
Workstation
| Resource | Detail |
|---|---|
| Cores | 28 |
| Python | 3.13.2 — imaging and HTTP libraries present; PyAV not installed (needed for the packet-buffer rewrite) |
| ffmpeg | 8.1.1, bundled with Jatagan Footage Editor |
| Encoders available | Software and Intel Quick Sync both working; NVIDIA present but unusable |
| External disk | Samsung T7 Shield, 931 GB, 786 GB free |
Devices
| Device | Address | Notes |
|---|---|---|
| Pilot camera | 172.16.0.36 | IPC-HFW5442E-Z4E, on-screen label AIC-0-36 / D2P1-184 |
| Hailo filter PC | 172.16.10.61 | Remote shell is PowerShell 5.1. No email configuration exists there, contrary to an early assumption |
| GeoVision recorder | 172.16.11.245 | Accepts generic RTSP sources |
Credentials are never recorded in this document.
Existing code worth reusing
Two files in the earlier snapshot project turned out to cover most of the pilot's ground.
tripwire_email_notifier.py already implements the event listener, a video ring
buffer, box drawing in the camera's coordinate space, and email with anti-spam throttling.
ivs_watch.py contains a tuned algorithm for choosing which frame a detection
belongs to — prior art on exactly the alignment problem this project has to solve.
11Test log
| # | Date | Test | Result |
|---|---|---|---|
| T1 | 08-16 | 15 s event capture | Heartbeats only. Endpoint alive, format not confirmed |
| T2 | 08-16 | 100 s event capture | Heartbeats only. Reported as a null result rather than rounded up |
| T3 | 08-16 | 9 min event capture | Two people captured. Six usable events. Sections 4.2–4.6 all come from this |
| T4 | 08-19 | Preflight check | Camera, ffmpeg, disk, RTSP all pass. Verdict READY |
| T5 | 08-19 | Synthetic clip, 30 fps | Defect found — frame rate mismatch, clip would play at half speed |
| T6 | 08-19 | Synthetic clip after fix | 363 frames, 11.37 s, rate measured correctly |
| T7 | 08-19 | Visual check of a drawn frame | Box renders correctly; label and timestamp present; camera on-screen display intact |
| T8 | 08-19 | Idle CPU at 30 fps | 2.3% of a core |
| T9 | 08-19 | Camera set to 15 fps, re-measure | Confirmed arriving at 15.0 fps; idle CPU 1.4% |
| T10 | 08-19 | Synthetic clip on 15 fps stream | 194 frames, 11.28 s. Correct |
| T11 | 08-19 | Encoder benchmark | See section 5.2 |
| T12 | 08-19 | Live pilot window, real foot traffic | 15 detections, 16 clips, 0 dropped. Alignment measured at −0.8 s. See section 6 |
12Next steps
- Review the pilot output. The question it answers is whether the box lands on the right person at the right frame — not whether we can draw one, which is already settled.
- If alignment holds: stand up the streaming server, build the live proxy, and prove the chain end to end into GeoVision.
- Shorten the substream keyframe interval before the proxy work. A VMS showing four seconds of black on connect reads as a dead camera.
- Confirm GeoVision channel headroom, which decides the recording architecture.
- Move the pre-roll buffer from decoded frames to compressed packets before anything ships to the trailer. This is the 200× memory decision.