Camera Updates Publisher
The Camera Snapshot stream lets the operator UI stop polling
/Cameras/operational and /CameraFilters/* every 10 seconds. Frontend.API
serves one edge-cached fleet snapshot; this function supplies the changes in
between so a tile refreshes the moment its camera captures, goes offline,
moves, or gets an AI hit.
How it works
PublishCameraUpdatesCosmosTrigger (Functions/CameraUpdates/CameraUpdatesPublisherFunction.cs)
listens to the change feed of the camera container using the leases
container with the camera-updates- lease prefix, the same pattern as
AddOrchestrationCosmosTrigger. Every batch:
- Filters to
dataType == "operational"documents (the container also holds static, global-settings and boresight documents) and types them through Newtonsoft, because the entities carry Newtonsoft property names such aslastMovedDatethat System.Text.Json would not map. - Hands them to
CameraUpdatesPublisher, a singleton that builds oneCameraUpdateMessageper camera (last version wins inside a batch) and invokesBroadcastCameraUpdateon Frontend.API'sCameraHubthrough the existingISignalRConnectionService.
Each delta carries the public operational fields, the camera's latest image
URL / thumbnail / timestamp / latency read from the document itself
(SingleImageStorageService now patches latestImageUrl,
latestImageTimestamp and the two acquisition-rate fields onto the
operational document in the same Cosmos patch as pan/tilt/zoom, so publishing
does no storage reads), the shared publisherKey (FrontendApiOptions:ApiKey,
which the hub verifies and strips) and statusKinds:
which status fields flipped compared with the last version this process saw
(offline, movement, ai, patrol). The hub fans non-empty
statusKinds out to every operator as a public-safe status event; AI values
themselves never leave the private overlay.
Latency: the change feed is polled. The trigger sets FeedPollDelay = 1000
(the extension default is 5 000 ms), so a delta reaches the hub within about
1 s of the Cosmos write plus the client's 300 ms batch window. With the default
the tile view trailed the location page by 3-6 s (uat, 2026-09-28: p50 2.9 s,
p90 4.8 s), because the location page's image arrives through the direct
BroadcastImageToGroup call from SingleImageStorageService while the tile
view waits for the next feed poll. An empty poll costs about 1 RU per lease, so
1 s polling adds roughly 4 RU/s for the whole camera container (one lease
per physical partition, five today) — noise against a 10k RU/s partition share.
Failure policy: a publish problem is logged and swallowed. The trigger never throws, so the feed cannot stall behind one bad batch; a missed delta is healed by the client's next snapshot resync.
The switch
Two gates, both must be on:
CameraUpdatesOptions:Enabled(App Configuration, defaulttrue) — deploy-time master.- SQL
FeatureFlagrowenable-camera-snapshot-stream— runtime switch, re-read every 30 s and shared with Frontend.API's snapshot builder and the frontend. Missing row = off.
With the flag off the trigger still runs and returns immediately, so the lease
keeps advancing: turning the feature back on later starts from "now" with no
catch-up storm. Rollback is therefore just IsEnabled = 0 on that row.
Options (CameraUpdatesOptions)
| Key | Default | Meaning |
|---|---|---|
Enabled | true | Deploy-time master |
FeatureFlagPollIntervalSeconds | 30 | Flag cache lifetime |
Volume
One hub invoke per operational write: roughly the fleet's capture rate (tens per second). Azure SignalR only bills outbound messages, and a camera's delta only reaches the connections subscribed to it, so cost scales with what operators are looking at, not with how many people are connected.