Skip to content

Pattern 16: Video Event Mapping

CCTV adapters translate vendor camera/NVR events into PQ domain events under the pq.event.video.* and pq.event.technical.* namespaces. This pattern covers the full event taxonomy, the alert+poll duality, stale analytics events, and NVR-level vs channel-level routing.


PQ Video Event Taxonomy

Channel-level events (emitted on the Channel Thing)

PQ Event Severity When to emit
pq.event.video.motion.detected Info Motion starts (VMD or PIR)
pq.event.video.motion.cleared Info Motion ends (VMD inactive)
pq.event.video.blind Warn Camera offline / video signal lost
pq.event.video.blind.cleared Info Camera back online / signal restored
pq.event.video.quality.degraded Warn Tamper, shelter, defocus, bad video
pq.event.video.quality.restored Info Quality event ended
pq.event.video.analytics.object_detected Info Line crossing, region entrance/exit, face detection, scene change, baggage detection
pq.event.video.recording.failed Error Recording failure on this channel
pq.event.video.preset.activated Info PTZ preset goto completed
pq.event.video.preset.stored Info PTZ preset position saved
pq.event.technical.fault.sensor Warn Audio exception (use sensorType parameter)

NVR-level events (emitted on the NVR Thing)

PQ Event Severity When to emit
pq.event.technical.fault.hardware Error Disk full, disk error, storage failure, illegal access attempt

Catch-all

PQ Event Severity When to emit
pq.event.device.unknown Warn Any vendor event that does not match a known mapping

Use the eventType parameter to carry the original vendor event identifier so the unknown event is not silently lost.


Event Parameters

Some events carry additional context via parameters:

Event Parameter Example values
pq.event.video.motion.detected reason PIR (when triggered by PIR sensor, not VMD)
pq.event.video.quality.degraded reason vendor event type string
pq.event.video.analytics.object_detected objectType vendor event type string
pq.event.technical.fault.hardware reason vendor event type string
pq.event.technical.fault.sensor sensorType audio
pq.event.device.unknown eventType {protocol}:{vendorType}:{state}

Alert + Poll Duality

Video loss (camera offline) can arrive through two independent code paths:

Path Trigger Latency
AlertStream NVR pushes videoloss event Near-real-time
Status poll Periodic REST query for channel status Up to poll interval

Both paths must perform the same three actions:

  1. Set the VideoSignal status slot to video.signal.lost on the Channel
  2. Tear down the go2rtc stream for that channel (see concepts/video-streaming.md)
  3. Emit pq.event.video.blind

On recovery:

  1. Clear the VideoSignal status slot (null)
  2. Emit pq.event.video.blind.cleared

The poll path also catches transitions that happen during an alertStream reconnect window, so both paths are necessary even if the alertStream is reliable.


Stale Analytics Events

Many analytics event types (line crossing, region entrance, face detection, etc.) send only an "active" notification — they never send an "inactive" or "cleared" counterpart. If not handled, the event remains in active state indefinitely in PQ.

The problem: after the object leaves the scene, no event arrives to clear the state.

The pattern:

  1. On receipt of an analytics "active" event, record the current time and the activePostCount field from the event (if the vendor provides it)
  2. Start a short timer (e.g., 5 seconds)
  3. On timer expiry, if no subsequent "active" event for the same channel + type has arrived, emit the corresponding cleared event

This is a "last-seen" timeout pattern. The timer is reset each time a new active event arrives for the same rule, so continuous detections do not spam cleared/detected transitions.

Current state in the reference implementation: StaleEventTimeout (5 s) and activePostCount are defined as constants and tracked in the parsed event record, but the timeout-based clear mechanism is not yet implemented. Analytics events remain in active state until the channel goes offline. This is a known gap.


Channel 0 Suppression

NVRs typically emit system-level events on a virtual channel with ID 0. This channel does not correspond to any real camera. Events on channel 0 must be consumed and suppressed — do not emit a PQ event and do not log a warning.

// Suppress before any other route
router.When(a => a.ChannelId == 0)
      .WithAddress<NvrType>((_, owner) => owner as NvrType)
      .Handle((_, _) => Task.FromResult(true));

Place this route first so it takes priority over any catch-all.


NVR-level vs Channel-level Routing

Route events to the correct Thing:

Event origin Route to Rationale
Camera-specific (motion, tamper, video loss) Channel Thing The event is about a specific camera
NVR hardware (disk, storage, illegal access) NVR Thing The event is about the NVR itself, not any camera
Audio exception Channel Thing The audio sensor is on the camera

For channel-level routing, resolve the Channel by its protocol address (channel ID from the event). For NVR-level routing, resolve directly to the NVR owner.


Catch-all Route

Always register an Unhandled handler so any vendor event without a matching route is visible in the audit log rather than silently dropped. Use owner.Unknown(...) — it applies the RULE-027 two-axis classification (device.unresolved when the address found no Thing, device.unknown otherwise); never hand-pick the event id via a raw EventBuilder.

router.Unhandled(async (owner, evt, reason, ct) =>
    await owner.Unknown(
        Ts(evt),
        eventType: $"{protocol}:{evt.Type}:{evt.State}",
        reason: reason,
        rawData: evt.ToRawString(),
        cancellationToken: ct));

See Also