diff --git a/machinery/src/onvif/events.go b/machinery/src/onvif/events.go index 5761614..aff71d1 100644 --- a/machinery/src/onvif/events.go +++ b/machinery/src/onvif/events.go @@ -123,6 +123,13 @@ func dispatchEvent(ctx context.Context, ev stream.Event, configuration *models.C if ev.State != stream.StateActive { return } + // A camera replays every property topic's current state as + // Initialized on each new subscription, so treating that as a + // trigger lets a flapping pull-point manufacture motion. + if ev.Operation == stream.PropertyInitialized { + log.Log.Debug("onvif.dispatchEvent(): subscription state replay, not a trigger: topic=" + ev.Topic) + return + } if configuration.Config.Capture.Recording == "false" { return } diff --git a/machinery/src/onvif/events_test.go b/machinery/src/onvif/events_test.go index 2fd0682..597cc5b 100644 --- a/machinery/src/onvif/events_test.go +++ b/machinery/src/onvif/events_test.go @@ -123,6 +123,54 @@ func TestDispatchEvent_LogsTheTriggeringTopic(t *testing.T) { "the dispatched event's Kind must appear in the log") } +// TestDispatchEvent_PropertyOperation — a camera replays the current +// state of every property topic as Initialized whenever a pull-point +// subscription is created. If that counts as a trigger, every +// reconnect restarts a recording for any motion property that happens +// to be active, and a flapping subscription manufactures motion out of +// nothing. Only reject Initialized specifically: PropertyOperation is +// optional per WS-Notification and absent on many non-property events, +// which decode reports as PropertyUnknown. +func TestDispatchEvent_PropertyOperation(t *testing.T) { + tests := []struct { + name string + op stream.PropertyOperation + wantSend bool + }{ + {"changed is a real transition", stream.PropertyChanged, true}, + {"absent attribute still counts", stream.PropertyUnknown, true}, + {"initialized is a subscription state replay", stream.PropertyInitialized, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := makeConfig("true", "true", "cam-1") + comm := makeCommunication(1) + ev := stream.Event{ + Kind: stream.KindMotion, + State: stream.StateActive, + Operation: tt.op, + Topic: "tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1", + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + dispatchEvent(ctx, ev, cfg, comm) + + select { + case <-comm.HandleMotion: + if !tt.wantSend { + t.Fatalf("%v must not trigger a recording", tt.op) + } + case <-time.After(100 * time.Millisecond): + if tt.wantSend { + t.Fatalf("%v must trigger a recording", tt.op) + } + } + }) + } +} + func TestDispatchEvent_RecordingDisabled_DoesNotSend(t *testing.T) { cfg := makeConfig("false", "true", "cam-1") comm := makeCommunication(1)