From 4d53889519d233a5f875e0e877d18cbc12d30a35 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Wed, 3 May 2023 08:02:56 +0300 Subject: [PATCH] Improve support ONVIF client --- internal/onvif/init.go | 28 ++++++++++++++---- pkg/onvif/client.go | 64 ++++++++++++++++++++++++++++-------------- pkg/onvif/helpers.go | 33 ++++++++-------------- www/add.html | 1 + 4 files changed, 77 insertions(+), 49 deletions(-) diff --git a/internal/onvif/init.go b/internal/onvif/init.go index a4f5ad22..01b33702 100644 --- a/internal/onvif/init.go +++ b/internal/onvif/init.go @@ -11,6 +11,7 @@ import ( "io" "net" "net/http" + "net/url" "os" "strconv" "time" @@ -123,17 +124,32 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { var items []api.Stream if src == "" { - hosts, err := onvif.DiscoveryStreamingHosts() + urls, err := onvif.DiscoveryStreamingURLs() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - for _, host := range hosts { - items = append(items, api.Stream{ - Name: host, - URL: "onvif://user:pass@" + host, - }) + for _, rawURL := range urls { + u, err := url.Parse(rawURL) + if err != nil { + log.Warn().Str("url", rawURL).Msg("[onvif] broken") + continue + } + + if u.Scheme != "http" { + log.Warn().Str("url", rawURL).Msg("[onvif] unsupported") + continue + } + + u.Scheme = "onvif" + u.User = url.UserPassword("user", "pass") + + if u.Path == onvif.PathDevice { + u.Path = "" + } + + items = append(items, api.Stream{Name: u.Host, URL: u.String()}) } } else { client, err := onvif.NewClient(src) diff --git a/pkg/onvif/client.go b/pkg/onvif/client.go index a4b6413f..d090baf3 100644 --- a/pkg/onvif/client.go +++ b/pkg/onvif/client.go @@ -17,6 +17,10 @@ import ( type Client struct { url *url.URL + + deviceURL string + mediaURL string + imaginURL string } func NewClient(rawURL string) (*Client, error) { @@ -24,7 +28,25 @@ func NewClient(rawURL string) (*Client, error) { if err != nil { return nil, err } - return &Client{url: u}, nil + + baseURL := "http://" + u.Host + + client := &Client{url: u} + if u.Path == "" { + client.deviceURL = baseURL + PathDevice + } else { + client.deviceURL = baseURL + u.Path + } + + b, err := client.GetCapabilities() + if err != nil { + return nil, err + } + + client.mediaURL = FindTagValue(b, "Media.+?XAddr") + client.imaginURL = FindTagValue(b, "Imaging.+?XAddr") + + return client, nil } func (c *Client) GetURI() (string, error) { @@ -39,7 +61,7 @@ func (c *Client) GetURI() (string, error) { return "", err } if i >= len(tokens) { - return "", errors.New("wrong subtype") + return "", errors.New("onvif: wrong subtype") } token = tokens[i] } @@ -104,7 +126,7 @@ func (c *Client) HasSnapshots() bool { func (c *Client) GetCapabilities() ([]byte, error) { return c.Request( - PathDevice, + c.deviceURL, ` All `, @@ -113,25 +135,25 @@ func (c *Client) GetCapabilities() ([]byte, error) { func (c *Client) GetNetworkInterfaces() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetDeviceInformation() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetProfiles() ([]byte, error) { return c.Request( - PathMedia, ``, + c.mediaURL, ``, ) } func (c *Client) GetStreamUri(token string) ([]byte, error) { return c.Request( - PathMedia, + c.mediaURL, ` RTP-Unicast @@ -144,8 +166,8 @@ func (c *Client) GetStreamUri(token string) ([]byte, error) { func (c *Client) GetSnapshotUri(token string) ([]byte, error) { return c.Request( - PathMedia, - ` + c.imaginURL, + ` `+token+` `, ) @@ -153,26 +175,26 @@ func (c *Client) GetSnapshotUri(token string) ([]byte, error) { func (c *Client) GetSystemDateAndTime() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetServiceCapabilities() ([]byte, error) { // some cameras answer GetServiceCapabilities for media only for path = "/onvif/media" return c.Request( - PathMedia, ``, + c.mediaURL, ``, ) } func (c *Client) SystemReboot() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } func (c *Client) GetServices() ([]byte, error) { return c.Request( - PathDevice, ` + c.deviceURL, ` true `, ) @@ -180,11 +202,15 @@ func (c *Client) GetServices() ([]byte, error) { func (c *Client) GetScopes() ([]byte, error) { return c.Request( - PathDevice, ``, + c.deviceURL, ``, ) } -func (c *Client) Request(path, body string) ([]byte, error) { +func (c *Client) Request(url, body string) ([]byte, error) { + if url == "" { + return nil, errors.New("onvif: unsupported service") + } + buf := bytes.NewBuffer(nil) buf.WriteString( ``, @@ -213,11 +239,7 @@ func (c *Client) Request(path, body string) ([]byte, error) { buf.WriteString(`` + body + ``) client := &http.Client{Timeout: time.Second * 5000} - res, err := client.Post( - "http://"+c.url.Host+path, - `application/soap+xml;charset=utf-8`, - buf, - ) + res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf) if err != nil { return nil, err } @@ -226,7 +248,7 @@ func (c *Client) Request(path, body string) ([]byte, error) { b, err := io.ReadAll(res.Body) if err == nil && res.StatusCode != http.StatusOK { - err = errors.New(res.Status) + err = errors.New("onvif: " + res.Status + " for " + url) } return b, err diff --git a/pkg/onvif/helpers.go b/pkg/onvif/helpers.go index 714bc0fc..c5451c42 100644 --- a/pkg/onvif/helpers.go +++ b/pkg/onvif/helpers.go @@ -3,7 +3,6 @@ package onvif import ( "github.com/AlexxIT/go2rtc/pkg/core" "net" - "net/url" "regexp" "strconv" "strings" @@ -12,11 +11,10 @@ import ( const ( PathDevice = "/onvif/device_service" - PathMedia = "/onvif/media_service" ) func FindTagValue(b []byte, tag string) string { - re := regexp.MustCompile(tag + `[^>]*>([^<]+)`) + re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`) m := re.FindSubmatch(b) if len(m) != 2 { return "" @@ -30,7 +28,7 @@ func UUID() string { return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] } -func DiscoveryStreamingHosts() ([]string, error) { +func DiscoveryStreamingURLs() ([]string, error) { conn, err := net.ListenUDP("udp4", nil) if err != nil { return nil, err @@ -66,7 +64,7 @@ func DiscoveryStreamingHosts() ([]string, error) { return nil, err } - var hosts []string + var urls []string b := make([]byte, 8192) for { @@ -75,37 +73,28 @@ func DiscoveryStreamingHosts() ([]string, error) { break } + //log.Printf("[onvif] discovery response addr=%s:\n%s", addr, b[:n]) + // ignore printers, etc if !strings.Contains(string(b[:n]), "onvif") { continue } - //log.Printf("[onvif] discovery response:\n%s", b[:n]) - - rawURL := FindTagValue(b[:n], "XAddrs") - if rawURL == "" { - continue - } - - u, err := url.Parse(rawURL) - if err != nil { - continue - } - - if u.Scheme != "http" { + url := FindTagValue(b[:n], "XAddrs") + if url == "" { continue } // fix some buggy cameras // http://0.0.0.0:8080/onvif/device_service - if strings.HasPrefix(u.Host, "0.0.0.0") { - u.Host = addr.IP.String() + u.Host[7:] + if strings.HasPrefix(url, "http://0.0.0.0") { + url = "http://" + addr.IP.String() + url[14:] } - hosts = append(hosts, u.Host) + urls = append(urls, url) } - return hosts, nil + return urls, nil } func atoi(s string) int { diff --git a/www/add.html b/www/add.html index 476c65c5..fc8a7c46 100644 --- a/www/add.html +++ b/www/add.html @@ -60,6 +60,7 @@