package onvif import ( "bytes" "errors" "html" "io" "net/http" "net/url" "regexp" "strings" "time" ) const PathDevice = "/onvif/device_service" type Client struct { url *url.URL deviceURL string mediaURL string imaginURL string } func NewClient(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err } baseURL := "http://" + u.Host client := &Client{url: u} if u.Path == "" { client.deviceURL = baseURL + PathDevice } else { client.deviceURL = baseURL + u.Path } b, err := client.DeviceRequest(DeviceGetCapabilities) 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) { query := c.url.Query() token := query.Get("subtype") // support empty if i := atoi(token); i >= 0 { tokens, err := c.GetProfilesTokens() if err != nil { return "", err } if i >= len(tokens) { return "", errors.New("onvif: wrong subtype") } token = tokens[i] } getUri := c.GetStreamUri if query.Has("snapshot") { getUri = c.GetSnapshotUri } b, err := getUri(token) if err != nil { return "", err } rawURL := FindTagValue(b, "Uri") rawURL = strings.TrimSpace(html.UnescapeString(rawURL)) u, err := url.Parse(rawURL) if err != nil { return "", err } if u.User == nil && c.url.User != nil { u.User = c.url.User } return u.String(), nil } func (c *Client) GetName() (string, error) { b, err := c.DeviceRequest(DeviceGetDeviceInformation) if err != nil { return "", err } return FindTagValue(b, "Manufacturer") + " " + FindTagValue(b, "Model"), nil } func (c *Client) GetProfilesTokens() ([]string, error) { b, err := c.MediaRequest(MediaGetProfiles) if err != nil { return nil, err } var tokens []string re := regexp.MustCompile(`Profiles.+?token="([^"]+)`) for _, s := range re.FindAllStringSubmatch(string(b), 10) { tokens = append(tokens, s[1]) } return tokens, nil } func (c *Client) HasSnapshots() bool { b, err := c.GetServiceCapabilities() if err != nil { return false } return strings.Contains(string(b), `SnapshotUri="true"`) } func (c *Client) GetProfile(token string) ([]byte, error) { return c.Request( c.mediaURL, ``+token+``, ) } func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) { return c.Request(c.mediaURL, ` `+token+` `) } func (c *Client) GetStreamUri(token string) ([]byte, error) { return c.Request(c.mediaURL, ` RTP-Unicast RTSP `+token+` `) } func (c *Client) GetSnapshotUri(token string) ([]byte, error) { return c.Request( c.imaginURL, ``+token+``, ) } func (c *Client) GetServiceCapabilities() ([]byte, error) { // some cameras answer GetServiceCapabilities for media only for path = "/onvif/media" return c.Request( c.mediaURL, ``, ) } func (c *Client) DeviceRequest(operation string) ([]byte, error) { if operation == DeviceGetServices { operation = `true` } else { operation = `` } return c.Request(c.deviceURL, operation) } func (c *Client) MediaRequest(operation string) ([]byte, error) { operation = `` return c.Request(c.mediaURL, operation) } func (c *Client) Request(url, body string) ([]byte, error) { if url == "" { return nil, errors.New("onvif: unsupported service") } e := NewEnvelopeWithUser(c.url.User) e.Append(body) client := &http.Client{Timeout: time.Second * 5000} res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes())) if err != nil { return nil, err } // need to close body with eny response status b, err := io.ReadAll(res.Body) if err == nil && res.StatusCode != http.StatusOK { err = errors.New("onvif: " + res.Status + " for " + url) } return b, err }