Skip to content

Commit 9d2cd7f

Browse files
committed
fix: client refuse due to typo in use-go/onvif
- See: use-go/onvif#63 - See: use-go/onvif#64
1 parent d09f4f8 commit 9d2cd7f

1 file changed

Lines changed: 161 additions & 38 deletions

File tree

06-events/main.go

Lines changed: 161 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,36 @@
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/
3030
package main
3131

3232
import (
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 {
5759
type 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

6279
type createPullPointResponse struct {
@@ -91,7 +108,7 @@ type msgPayload struct {
91108
type 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

97114
type 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+
106207
func 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

Comments
 (0)