mirror of
https://github.com/kerberos-io/onvif.git
synced 2026-08-23 15:08:33 +00:00
Replaces the bare fmt.Errorf wrappers on the Errors channel with three
typed errors and adds an Event.AfterReconnect flag so consumers can
distinguish post-recreate replay events from live ones.
Typed errors
------------
ErrPullFailed, ErrRenewFailed, ErrRecreateFailed all implement
Unwrap() and Op() Op. Consumers can branch with errors.As without
parsing strings:
var pull ErrPullFailed
if errors.As(e, &pull) { /* transient; logged */ }
var recreate ErrRecreateFailed
if errors.As(e, &recreate) { /* alert: camera may be offline */ }
Op() returns OpPull / OpRenew / OpRecreate for cases where the caller
wants to log the operation name without unwrapping. Both addressed
the review's 'highest-leverage v1 change' concern about bare error on
the Errors channel.
AfterReconnect observability
----------------------------
ONVIF cameras replay each property's current value with
PropertyInitialized whenever a new pull-point subscription is
established (per the Event Service spec). A consumer doing edge
detection on motion = StateActive would otherwise see a phantom
'motion started' for every active property after every reconnect.
The pull loop now tracks an afterReconnect flag local to the
goroutine: set to true when attemptRecreate returns justRecreated,
applied to every emitted event, cleared on the first non-Initialized
event we see. This bounds the replay window naturally — once the
camera has finished sending current state, the next event tells us
we're live.
attemptRecreate now returns (justRecreated, cont) so the pull loop
knows whether the just-completed recreate succeeded vs. the call
returning due to ctx-cancel during backoff.
Test coverage
-------------
* errors_test.go: typed-error Unwrap/Op assertions plus
Stream-level proof that pull and recreate failures arrive on the
Errors channel wearing the right type.
* AfterReconnect flag: drives the stream through a failure, observes
the next event carries the flag and the one after does not.
172 lines
5.6 KiB
Go
172 lines
5.6 KiB
Go
package stream
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Kind is the normalized category of an ONVIF event, independent of the
|
|
// camera vendor's topic naming.
|
|
type Kind uint8
|
|
|
|
const (
|
|
// KindUnknown is the zero value; used when a topic does not match any
|
|
// known classification.
|
|
KindUnknown Kind = iota
|
|
// KindMotion covers motion detection from any vendor (e.g. AXIS
|
|
// VideoSource/MotionAlarm, Hikvision RuleEngine/CellMotionDetector).
|
|
KindMotion
|
|
// KindTampering covers true tamper alarms (lens cover, scene
|
|
// substitution). Imaging-quality alarms map to KindImageQuality.
|
|
KindTampering
|
|
// KindImageQuality covers VideoSource imaging alarms such as
|
|
// ImageTooDark, ImageTooBright and ImageTooBlurry. Most integrators
|
|
// treat these separately from tamper because they fire on legitimate
|
|
// sunset/dawn/condensation transitions.
|
|
KindImageQuality
|
|
// KindDigitalInput covers external sensor inputs wired to the camera.
|
|
KindDigitalInput
|
|
// KindDigitalOutput covers relay output state changes on the camera.
|
|
KindDigitalOutput
|
|
// KindObjectDetected covers analytics-based object/person/vehicle
|
|
// detection events.
|
|
KindObjectDetected
|
|
// KindAudioAlarm covers audio-level / loud-noise alarms.
|
|
KindAudioAlarm
|
|
)
|
|
|
|
// String implements fmt.Stringer.
|
|
func (k Kind) String() string {
|
|
switch k {
|
|
case KindUnknown:
|
|
return "Unknown"
|
|
case KindMotion:
|
|
return "Motion"
|
|
case KindTampering:
|
|
return "Tampering"
|
|
case KindImageQuality:
|
|
return "ImageQuality"
|
|
case KindDigitalInput:
|
|
return "DigitalInput"
|
|
case KindDigitalOutput:
|
|
return "DigitalOutput"
|
|
case KindObjectDetected:
|
|
return "ObjectDetected"
|
|
case KindAudioAlarm:
|
|
return "AudioAlarm"
|
|
default:
|
|
return fmt.Sprintf("Kind(%d)", uint8(k))
|
|
}
|
|
}
|
|
|
|
// State is the active/inactive level carried by a boolean ONVIF property
|
|
// event (e.g. IsMotion=true/false). StateUnknown is used both when the
|
|
// value cannot be parsed and when the topic is edge-triggered and carries
|
|
// no boolean state (e.g. LineDetector/Crossed).
|
|
type State uint8
|
|
|
|
const (
|
|
StateUnknown State = iota
|
|
StateActive
|
|
StateInactive
|
|
)
|
|
|
|
// String implements fmt.Stringer.
|
|
func (s State) String() string {
|
|
switch s {
|
|
case StateUnknown:
|
|
return "Unknown"
|
|
case StateActive:
|
|
return "Active"
|
|
case StateInactive:
|
|
return "Inactive"
|
|
default:
|
|
return fmt.Sprintf("State(%d)", uint8(s))
|
|
}
|
|
}
|
|
|
|
// PropertyOperation mirrors the ONVIF wsnt:PropertyOperation attribute and
|
|
// indicates whether a message is the first sighting of a property
|
|
// (Initialized), a transition (Changed) or the property going away
|
|
// (Deleted). PropertyUnknown is used both when the attribute is absent on
|
|
// the wire (the spec allows it) and when the value is unrecognised.
|
|
type PropertyOperation uint8
|
|
|
|
const (
|
|
PropertyUnknown PropertyOperation = iota
|
|
PropertyInitialized
|
|
PropertyChanged
|
|
PropertyDeleted
|
|
)
|
|
|
|
// String implements fmt.Stringer.
|
|
func (p PropertyOperation) String() string {
|
|
switch p {
|
|
case PropertyUnknown:
|
|
return "Unknown"
|
|
case PropertyInitialized:
|
|
return "Initialized"
|
|
case PropertyChanged:
|
|
return "Changed"
|
|
case PropertyDeleted:
|
|
return "Deleted"
|
|
default:
|
|
return fmt.Sprintf("PropertyOperation(%d)", uint8(p))
|
|
}
|
|
}
|
|
|
|
// Event is a single normalized notification from an ONVIF device.
|
|
//
|
|
// Kind, State and Operation are the normalized fields most callers should
|
|
// switch on. Topic, Source and Data preserve the original ONVIF data so
|
|
// callers can inspect the wire form without re-parsing SOAP.
|
|
//
|
|
// Source and Data are maps from ONVIF SimpleItem Name to Value because
|
|
// notifications can carry multiple items: AXIS Object Analytics for
|
|
// example emits active, classType and confidence in the same Data list,
|
|
// and standard DigitalInput notifications carry both InputToken in Source
|
|
// and LogicalState in Data.
|
|
type Event struct {
|
|
// Kind is the normalized event category.
|
|
Kind Kind
|
|
// State is the active/inactive value carried by a boolean event.
|
|
// StateUnknown for edge-triggered events (LineDetector/Crossed) that
|
|
// carry no boolean property.
|
|
State State
|
|
// Operation is the ONVIF property lifecycle
|
|
// (Initialized/Changed/Deleted).
|
|
Operation PropertyOperation
|
|
// DeviceID identifies the camera that produced the event. Set by the
|
|
// Stream from the caller-supplied identifier so a single channel can
|
|
// fan in events from multiple devices.
|
|
DeviceID string
|
|
// Source is the ONVIF Source SimpleItem map (e.g. InputToken,
|
|
// VideoSourceConfigurationToken, Rule). Empty when the notification
|
|
// has no Source section.
|
|
Source map[string]string
|
|
// Data is the ONVIF Data SimpleItem map (e.g. IsMotion, LogicalState,
|
|
// active, classType). Empty when the notification has no Data
|
|
// section.
|
|
Data map[string]string
|
|
// Topic is the raw ONVIF topic string, e.g.
|
|
// tns1:VideoSource/MotionAlarm.
|
|
Topic string
|
|
// Timestamp is when the stream observed the event locally.
|
|
Timestamp time.Time
|
|
// DeviceTime is the camera-reported wsnt:UtcTime, when present and
|
|
// parseable. Zero if the camera omits the attribute or sends an
|
|
// unparseable value. Many cameras have drifting clocks; prefer
|
|
// Timestamp for ordering and DeviceTime only for forensics or
|
|
// cross-camera correlation when caller manages NTP.
|
|
DeviceTime time.Time
|
|
// AfterReconnect is true for events delivered after the Stream
|
|
// silently recreated its pull-point subscription. ONVIF cameras
|
|
// replay each property's current value with PropertyInitialized on
|
|
// a new subscription, which would otherwise look like a flood of
|
|
// new state changes to a consumer doing edge-detection. Watch this
|
|
// flag to suppress duplicate handling, or treat it as a normal
|
|
// event if you only care about steady-state level. Cleared on the
|
|
// first event whose Operation is not PropertyInitialized.
|
|
AfterReconnect bool
|
|
}
|