From 176e0d8f3c5b4157f7f0f1a46e4d5d3e15dbceca Mon Sep 17 00:00:00 2001 From: Sebastian Norling <1932208+Bazze@users.noreply.github.com> Date: Thu, 21 May 2026 14:42:02 +0200 Subject: [PATCH] feat(examples): add event/stream CLI for live camera verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a small command at examples/event/stream that opens a real ONVIF event stream against a camera and prints decoded events one per line. Intended for verifying the classifier against actual hardware (AXIS in particular) and as runnable documentation for new consumers of the package — point it at a configured camera, trigger motion, watch the events arrive. Behaviour --------- * Required flags: -xaddr, -username, -password (matches existing examples/event/* commands so anyone running the older subscribe / pullmessage demos already knows the shape). * Optional -filter passes through to Options.TopicFilter; default empty so AXIS works out of the box. * -duration N stops after N (default 0 = run until Ctrl-C). * Prints kind/state/op/topic on each event plus source and data maps when present, so multi-item ONVIF payloads (AXIS AOA active+classType+confidence, DigitalInput InputToken+LogicalState) are visible without re-reading PullMessages SOAP. * Errors channel surfaced to stderr via log; the stream auto-recovers per the reconnect logic in stream.go so transient errors do not terminate the demo. Not part of any CI; not a production tool — this is a verification harness. --- examples/event/stream/main.go | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 examples/event/stream/main.go diff --git a/examples/event/stream/main.go b/examples/event/stream/main.go new file mode 100644 index 0000000..a795ed4 --- /dev/null +++ b/examples/event/stream/main.go @@ -0,0 +1,107 @@ +// Command streamtest opens an event stream against an ONVIF camera and +// prints decoded events as they arrive. Useful for verifying the +// classifier against real-camera topics; not intended as a production +// tool. +// +// Example: +// +// go run ./examples/event/stream \ +// -xaddr 192.168.1.10 \ +// -username root -password admin \ +// -duration 60s +// +// The xaddr is the camera's host or host:port (the library appends +// /onvif/device_service); pass with no protocol prefix. +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/kerberos-io/onvif" + "github.com/kerberos-io/onvif/event/stream" +) + +func main() { + xaddr := flag.String("xaddr", "", "camera host or host:port (required)") + username := flag.String("username", "", "ONVIF user (required)") + password := flag.String("password", "", "ONVIF password (required)") + deviceID := flag.String("device-id", "", "logical name printed with each event (default: xaddr)") + filter := flag.String("filter", "", "raw ONVIF ConcreteSet topic filter (empty = all topics, works on AXIS)") + pullTimeout := flag.Duration("pull-timeout", 5*time.Second, "server-side wait per PullMessages call") + duration := flag.Duration("duration", 0, "stop after this long (0 = run until Ctrl-C)") + flag.Parse() + + if *xaddr == "" || *username == "" || *password == "" { + flag.Usage() + os.Exit(2) + } + if *deviceID == "" { + *deviceID = *xaddr + } + + dev, err := onvif.NewDevice(onvif.DeviceParams{ + Xaddr: *xaddr, + Username: *username, + Password: *password, + AuthMode: onvif.UsernameTokenAuth, + }) + if err != nil { + log.Fatalf("connect: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + if *duration > 0 { + var done context.CancelFunc + ctx, done = context.WithTimeout(ctx, *duration) + defer done() + } + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + cancel() + }() + + s, err := stream.NewStream(ctx, dev, stream.Options{ + DeviceID: *deviceID, + TopicFilter: *filter, + PullTimeout: *pullTimeout, + }) + if err != nil { + log.Fatalf("open stream: %v", err) + } + defer s.Close() + + log.Printf("streaming from %s (device-id=%s, filter=%q)", *xaddr, *deviceID, *filter) + for { + select { + case <-ctx.Done(): + log.Printf("done (%v)", ctx.Err()) + return + case ev, ok := <-s.Events(): + if !ok { + return + } + fmt.Printf("%s kind=%-15s state=%-9s op=%-12s topic=%s", + ev.Timestamp.Format(time.RFC3339), ev.Kind, ev.State, ev.Operation, ev.Topic) + if len(ev.Source) > 0 { + fmt.Printf(" source=%v", ev.Source) + } + if len(ev.Data) > 0 { + fmt.Printf(" data=%v", ev.Data) + } + fmt.Println() + case e := <-s.Errors(): + log.Printf("stream error: %v", e) + } + } +}