99//
1010// ONVIF supports two event delivery mechanisms:
1111//
12- // 1. PullPoint (used here): The client creates a subscription, then
13- // periodically "pulls" events from the camera. This is simpler
14- // because it works through NAT/firewalls — the client initiates
15- // all connections. Most VMS software uses this approach.
12+ // 1. PullPoint (used here): The client creates a subscription, then
13+ // periodically "pulls" events from the camera. This is simpler
14+ // because it works through NAT/firewalls — the client initiates
15+ // all connections. Most VMS software uses this approach.
1616//
17- // 2. WS-BaseNotification (push): The camera pushes events to a
18- // client-hosted HTTP endpoint. Requires the client to run an HTTP
19- // server that the camera can reach — problematic with NAT/firewalls.
17+ // 2. WS-BaseNotification (push): The camera pushes events to a
18+ // client-hosted HTTP endpoint. Requires the client to run an HTTP
19+ // server that the camera can reach — problematic with NAT/firewalls.
2020//
2121// PullPoint workflow:
22- // 1. CreatePullPointSubscription — camera creates a subscription and
23- // returns a subscription endpoint URL.
24- // 2. PullMessages (loop) — client polls the subscription endpoint.
25- // The camera blocks the response until events occur or timeout.
26- // This is similar to HTTP long polling.
27- // 3. Unsubscribe — clean up when done (important to free camera resources).
22+ // 1. CreatePullPointSubscription — camera creates a subscription and
23+ // returns a subscription endpoint URL.
24+ // 2. PullMessages (loop) — client polls the subscription endpoint.
25+ // The camera blocks the response until events occur or timeout.
26+ // This is similar to HTTP long polling.
27+ // 3. Unsubscribe — clean up when done (important to free camera resources).
2828//
2929// Run: go run ./06-events/
3030package main
3131
3232import (
33+ "bytes"
3334 "encoding/xml"
3435 "fmt"
35- "io/ioutil "
36+ "io"
3637 "log"
3738 "net/http"
3839 "os"
3940 "os/signal"
41+ "strings"
4042 "syscall"
4143 "time"
4244
@@ -57,6 +59,21 @@ type soapEnvelope struct {
5759type soapBodyMsg struct {
5860 CreateResponse createPullPointResponse `xml:"CreatePullPointSubscriptionResponse"`
5961 PullResponse pullMessagesResponse `xml:"PullMessagesResponse"`
62+ Fault soapFault `xml:"Fault"`
63+ }
64+
65+ type soapFault struct {
66+ Code soapFaultCode `xml:"Code"`
67+ Reason soapFaultReason `xml:"Reason"`
68+ }
69+
70+ type soapFaultCode struct {
71+ Value string `xml:"Value"`
72+ Subcode string `xml:"Subcode>Value"`
73+ }
74+
75+ type soapFaultReason struct {
76+ Text string `xml:"Text"`
6077}
6178
6279type createPullPointResponse struct {
@@ -91,7 +108,7 @@ type msgPayload struct {
91108type innerMessage struct {
92109 UtcTime string `xml:"UtcTime,attr"`
93110 Source simpleItems `xml:"Source"`
94- Data simpleItems `xml:"Data"`
111+ Data simpleItems `xml:"Data"`
95112}
96113
97114type simpleItems struct {
@@ -103,6 +120,90 @@ type simpleItem struct {
103120 Value string `xml:"Value,attr"`
104121}
105122
123+ // topicNode is one node in the topic tree returned by GetEventProperties.
124+ type topicNode struct {
125+ Name string
126+ Children []* topicNode
127+ }
128+
129+ // schemaNodes are element names that describe message structure, not topics.
130+ // We skip them when printing the topic tree.
131+ var schemaNodes = map [string ]bool {
132+ "MessageDescription" : true ,
133+ "Source" : true ,
134+ "Data" : true ,
135+ "SimpleItemDescription" : true ,
136+ "ElementItemDescription" : true ,
137+ }
138+
139+ // parseTopicChildren reads child elements from decoder until the matching
140+ // EndElement and builds a slice of topicNodes.
141+ func parseTopicChildren (decoder * xml.Decoder ) ([]* topicNode , error ) {
142+ var nodes []* topicNode
143+ for {
144+ tok , err := decoder .Token ()
145+ if err != nil {
146+ return nodes , err
147+ }
148+ switch t := tok .(type ) {
149+ case xml.StartElement :
150+ children , err := parseTopicChildren (decoder )
151+ node := & topicNode {Name : t .Name .Local , Children : children }
152+ nodes = append (nodes , node )
153+ if err != nil {
154+ return nodes , err
155+ }
156+ case xml.EndElement :
157+ return nodes , nil
158+ }
159+ }
160+ }
161+
162+ // printTopicTree prints the topic tree with indentation, skipping schema nodes.
163+ func printTopicTree (nodes []* topicNode , indent int ) {
164+ for _ , n := range nodes {
165+ if schemaNodes [n .Name ] {
166+ continue
167+ }
168+ fmt .Printf ("%s%s\n " , strings .Repeat (" " , indent ), n .Name )
169+ printTopicTree (n .Children , indent + 1 )
170+ }
171+ }
172+
173+ // printSupportedTopics calls GetEventProperties and prints the topic tree.
174+ func printSupportedTopics (dev interface {
175+ CallMethod (interface {}) (* http.Response , error )
176+ }) error {
177+ resp , err := dev .CallMethod (event.GetEventProperties {})
178+ if err != nil {
179+ return fmt .Errorf ("GetEventProperties: %w" , err )
180+ }
181+ defer resp .Body .Close ()
182+
183+ raw , err := io .ReadAll (resp .Body )
184+ if err != nil {
185+ return err
186+ }
187+ if resp .StatusCode != http .StatusOK {
188+ return fmt .Errorf ("HTTP %d: %s" , resp .StatusCode , string (raw ))
189+ }
190+
191+ decoder := xml .NewDecoder (bytes .NewReader (raw ))
192+ for {
193+ tok , err := decoder .Token ()
194+ if err != nil {
195+ break
196+ }
197+ if se , ok := tok .(xml.StartElement ); ok && se .Name .Local == "TopicSet" {
198+ nodes , _ := parseTopicChildren (decoder )
199+ printTopicTree (nodes , 0 )
200+ return nil
201+ }
202+ }
203+ fmt .Println (" (no TopicSet found in response)" )
204+ return nil
205+ }
206+
106207func main () {
107208 cfg , err := config .Load ()
108209 if err != nil {
@@ -121,6 +222,13 @@ func main() {
121222 fmt .Println ("=== ONVIF Event Service (PullPoint) ===" )
122223 fmt .Println ()
123224
225+ // ─── 0. GetEventProperties — supported topics ────────────────────────
226+ fmt .Println ("--- Supported Event Topics ---" )
227+ if err := printSupportedTopics (dev ); err != nil {
228+ fmt .Printf (" (could not retrieve topics: %v)\n " , err )
229+ }
230+ fmt .Println ()
231+
124232 // ─── 1. CreatePullPointSubscription ─────────────────────────────────
125233 // This tells the camera to start buffering events for us.
126234 // The camera returns a subscription reference URL that we'll use
@@ -129,39 +237,58 @@ func main() {
129237 // InitialTerminationTime sets how long the subscription lives.
130238 // If we don't renew it (via Renew), it auto-expires to free
131239 // camera resources. PT60S = 60 seconds in ISO 8601 duration format.
240+ //
241+ // NOTE: The use-go/onvif library's CreatePullPointSubscription struct has
242+ // a typo in the SubscriptionPolicy XML tag ("wsnt:sSubscriptionPolicy"
243+ // instead of "wsnt:SubscriptionPolicy"), which causes cameras to reject
244+ // the request with a SOAP Sender fault.
245+ // See: https://github.com/use-go/onvif/issues/63
246+ //
247+ // Once that issue is fixed, this can be simplified to:
248+ //
249+ // createReq := event.CreatePullPointSubscription{}
250+ // resp, err := dev.CallMethod(createReq)
251+ //
252+ // Until then, we send the SOAP request manually.
132253 fmt .Println ("Creating PullPoint subscription..." )
133254
134- createReq := event.CreatePullPointSubscription {
135- // We leave Filter empty to receive ALL events.
136- // In production, you would filter by topic to reduce noise:
137- // Filter: event.FilterType{
138- // TopicExpression: event.TopicExpressionType{
139- // Dialect: "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet",
140- // TopicKinds: "tns1:VideoAnalytics//. ",
141- // },
142- // },
255+ eventEndpoint := dev .GetEndpoint ("events" )
256+ if eventEndpoint == "" {
257+ log .Fatal ("Event service endpoint not found on this device" )
143258 }
144259
145- resp , err := dev .CallMethod (createReq )
260+ const createPullPointSOAP = `<?xml version="1.0" encoding="UTF-8"?>
261+ <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
262+ xmlns:tev="http://www.onvif.org/ver10/events/wsdl"
263+ xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2">
264+ <s:Body>
265+ <tev:CreatePullPointSubscription>
266+ <wsnt:InitialTerminationTime>PT60S</wsnt:InitialTerminationTime>
267+ </tev:CreatePullPointSubscription>
268+ </s:Body>
269+ </s:Envelope>`
270+
271+ httpResp , err := http .Post (eventEndpoint , "application/soap+xml; charset=utf-8" , strings .NewReader (createPullPointSOAP ))
146272 if err != nil {
147273 log .Fatalf ("CreatePullPointSubscription failed: %v" , err )
148274 }
149- defer resp .Body .Close ()
275+ defer httpResp .Body .Close ()
150276
151- body , err := ioutil .ReadAll (resp .Body )
277+ body , err := io .ReadAll (httpResp .Body )
152278 if err != nil {
153279 log .Fatalf ("Failed to read response: %v" , err )
154280 }
155281
156- if resp .StatusCode != http .StatusOK {
157- log .Fatalf ("CreatePullPointSubscription returned HTTP %d:\n %s" , resp .StatusCode , string (body ))
158- }
159-
160282 var createEnv soapEnvelope
161283 if err := xml .Unmarshal (body , & createEnv ); err != nil {
162284 log .Fatalf ("Failed to parse CreatePullPointSubscription response: %v" , err )
163285 }
164286
287+ if f := createEnv .Body .Fault ; f .Code .Value != "" {
288+ log .Fatalf ("CreatePullPointSubscription SOAP Fault:\n Code : %s\n Subcode : %s\n Reason : %s\n Raw XML :\n %s" ,
289+ f .Code .Value , f .Code .Subcode , f .Reason .Text , string (body ))
290+ }
291+
165292 subAddr := createEnv .Body .CreateResponse .SubscriptionReference .Address
166293 fmt .Printf (" Subscription created!\n " )
167294 fmt .Printf (" Endpoint : %s\n " , subAddr )
@@ -180,7 +307,6 @@ func main() {
180307 fmt .Println ("Trigger motion in front of the camera to see events." )
181308 fmt .Println ()
182309
183- // Set up graceful shutdown with Ctrl+C
184310 sigChan := make (chan os.Signal , 1 )
185311 signal .Notify (sigChan , syscall .SIGINT , syscall .SIGTERM )
186312
@@ -203,9 +329,8 @@ func main() {
203329
204330 pollCount ++
205331
206- // PullMessages request: wait up to 5 seconds, accept up to 100 messages
207332 pullReq := event.PullMessages {
208- Timeout : "PT5S" , // ISO 8601 duration: 5 seconds
333+ Timeout : "PT5S" ,
209334 MessageLimit : 100 ,
210335 }
211336
@@ -216,7 +341,7 @@ func main() {
216341 continue
217342 }
218343
219- pullBody , err := ioutil .ReadAll (pullResp .Body )
344+ pullBody , err := io .ReadAll (pullResp .Body )
220345 pullResp .Body .Close ()
221346
222347 if pullResp .StatusCode != http .StatusOK {
@@ -237,7 +362,6 @@ func main() {
237362 continue
238363 }
239364
240- // Print each received event
241365 for _ , msg := range msgs {
242366 eventCount ++
243367 timestamp := msg .Message .Inner .UtcTime
@@ -274,8 +398,7 @@ cleanup:
274398 // (typically 5-10). Leaked subscriptions can prevent new clients
275399 // from receiving events until they auto-expire.
276400 fmt .Println ("Unsubscribing..." )
277- unsubReq := event.Unsubscribe {}
278- unsubResp , err := dev .CallMethod (unsubReq )
401+ unsubResp , err := dev .CallMethod (event.Unsubscribe {})
279402 if err != nil {
280403 log .Printf ("Unsubscribe failed: %v (subscription will auto-expire)" , err )
281404 } else {
0 commit comments