Improve support ONVIF client
This commit is contained in:
+22
-6
@@ -11,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -123,17 +124,32 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
|||||||
var items []api.Stream
|
var items []api.Stream
|
||||||
|
|
||||||
if src == "" {
|
if src == "" {
|
||||||
hosts, err := onvif.DiscoveryStreamingHosts()
|
urls, err := onvif.DiscoveryStreamingURLs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, host := range hosts {
|
for _, rawURL := range urls {
|
||||||
items = append(items, api.Stream{
|
u, err := url.Parse(rawURL)
|
||||||
Name: host,
|
if err != nil {
|
||||||
URL: "onvif://user:pass@" + host,
|
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 {
|
} else {
|
||||||
client, err := onvif.NewClient(src)
|
client, err := onvif.NewClient(src)
|
||||||
|
|||||||
+43
-21
@@ -17,6 +17,10 @@ import (
|
|||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
url *url.URL
|
url *url.URL
|
||||||
|
|
||||||
|
deviceURL string
|
||||||
|
mediaURL string
|
||||||
|
imaginURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(rawURL string) (*Client, error) {
|
func NewClient(rawURL string) (*Client, error) {
|
||||||
@@ -24,7 +28,25 @@ func NewClient(rawURL string) (*Client, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (c *Client) GetURI() (string, error) {
|
||||||
@@ -39,7 +61,7 @@ func (c *Client) GetURI() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if i >= len(tokens) {
|
if i >= len(tokens) {
|
||||||
return "", errors.New("wrong subtype")
|
return "", errors.New("onvif: wrong subtype")
|
||||||
}
|
}
|
||||||
token = tokens[i]
|
token = tokens[i]
|
||||||
}
|
}
|
||||||
@@ -104,7 +126,7 @@ func (c *Client) HasSnapshots() bool {
|
|||||||
|
|
||||||
func (c *Client) GetCapabilities() ([]byte, error) {
|
func (c *Client) GetCapabilities() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathDevice,
|
c.deviceURL,
|
||||||
`<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
`<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||||
<tds:Category>All</tds:Category>
|
<tds:Category>All</tds:Category>
|
||||||
</tds:GetCapabilities>`,
|
</tds:GetCapabilities>`,
|
||||||
@@ -113,25 +135,25 @@ func (c *Client) GetCapabilities() ([]byte, error) {
|
|||||||
|
|
||||||
func (c *Client) GetNetworkInterfaces() ([]byte, error) {
|
func (c *Client) GetNetworkInterfaces() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathDevice, `<tds:GetNetworkInterfaces xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
c.deviceURL, `<tds:GetNetworkInterfaces xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetDeviceInformation() ([]byte, error) {
|
func (c *Client) GetDeviceInformation() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathDevice, `<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
c.deviceURL, `<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetProfiles() ([]byte, error) {
|
func (c *Client) GetProfiles() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathMedia, `<trt:GetProfiles xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
|
c.mediaURL, `<trt:GetProfiles xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetStreamUri(token string) ([]byte, error) {
|
func (c *Client) GetStreamUri(token string) ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathMedia,
|
c.mediaURL,
|
||||||
`<trt:GetStreamUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
|
`<trt:GetStreamUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||||
<trt:StreamSetup>
|
<trt:StreamSetup>
|
||||||
<tt:Stream>RTP-Unicast</tt:Stream>
|
<tt:Stream>RTP-Unicast</tt:Stream>
|
||||||
@@ -144,8 +166,8 @@ func (c *Client) GetStreamUri(token string) ([]byte, error) {
|
|||||||
|
|
||||||
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
|
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathMedia,
|
c.imaginURL,
|
||||||
`<trt:GetSnapshotUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
`<trt:GetSnapshotUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||||
<trt:ProfileToken>`+token+`</trt:ProfileToken>
|
<trt:ProfileToken>`+token+`</trt:ProfileToken>
|
||||||
</trt:GetSnapshotUri>`,
|
</trt:GetSnapshotUri>`,
|
||||||
)
|
)
|
||||||
@@ -153,26 +175,26 @@ func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
|
|||||||
|
|
||||||
func (c *Client) GetSystemDateAndTime() ([]byte, error) {
|
func (c *Client) GetSystemDateAndTime() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathDevice, `<tds:GetSystemDateAndTime xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
c.deviceURL, `<tds:GetSystemDateAndTime xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetServiceCapabilities() ([]byte, error) {
|
func (c *Client) GetServiceCapabilities() ([]byte, error) {
|
||||||
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
|
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathMedia, `<trt:GetServiceCapabilities xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
|
c.mediaURL, `<trt:GetServiceCapabilities xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SystemReboot() ([]byte, error) {
|
func (c *Client) SystemReboot() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathDevice, `<tds:SystemReboot xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
c.deviceURL, `<tds:SystemReboot xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetServices() ([]byte, error) {
|
func (c *Client) GetServices() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathDevice, `<tds:GetServices xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
c.deviceURL, `<tds:GetServices xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||||
<tds:IncludeCapability>true</tds:IncludeCapability>
|
<tds:IncludeCapability>true</tds:IncludeCapability>
|
||||||
</tds:GetServices>`,
|
</tds:GetServices>`,
|
||||||
)
|
)
|
||||||
@@ -180,11 +202,15 @@ func (c *Client) GetServices() ([]byte, error) {
|
|||||||
|
|
||||||
func (c *Client) GetScopes() ([]byte, error) {
|
func (c *Client) GetScopes() ([]byte, error) {
|
||||||
return c.Request(
|
return c.Request(
|
||||||
PathDevice, `<tds:GetScopes xmlns:tds="http://www.onvif.org/ver10/device/wsdl" />`,
|
c.deviceURL, `<tds:GetScopes xmlns:tds="http://www.onvif.org/ver10/device/wsdl" />`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 := bytes.NewBuffer(nil)
|
||||||
buf.WriteString(
|
buf.WriteString(
|
||||||
`<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">`,
|
`<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">`,
|
||||||
@@ -213,11 +239,7 @@ func (c *Client) Request(path, body string) ([]byte, error) {
|
|||||||
buf.WriteString(`<s:Body>` + body + `</s:Body></s:Envelope>`)
|
buf.WriteString(`<s:Body>` + body + `</s:Body></s:Envelope>`)
|
||||||
|
|
||||||
client := &http.Client{Timeout: time.Second * 5000}
|
client := &http.Client{Timeout: time.Second * 5000}
|
||||||
res, err := client.Post(
|
res, err := client.Post(url, `application/soap+xml;charset=utf-8`, buf)
|
||||||
"http://"+c.url.Host+path,
|
|
||||||
`application/soap+xml;charset=utf-8`,
|
|
||||||
buf,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -226,7 +248,7 @@ func (c *Client) Request(path, body string) ([]byte, error) {
|
|||||||
b, err := io.ReadAll(res.Body)
|
b, err := io.ReadAll(res.Body)
|
||||||
|
|
||||||
if err == nil && res.StatusCode != http.StatusOK {
|
if err == nil && res.StatusCode != http.StatusOK {
|
||||||
err = errors.New(res.Status)
|
err = errors.New("onvif: " + res.Status + " for " + url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, err
|
return b, err
|
||||||
|
|||||||
+11
-22
@@ -3,7 +3,6 @@ package onvif
|
|||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,11 +11,10 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
PathDevice = "/onvif/device_service"
|
PathDevice = "/onvif/device_service"
|
||||||
PathMedia = "/onvif/media_service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func FindTagValue(b []byte, tag string) string {
|
func FindTagValue(b []byte, tag string) string {
|
||||||
re := regexp.MustCompile(tag + `[^>]*>([^<]+)`)
|
re := regexp.MustCompile(`<[^/>]*` + tag + `[^>]*>([^<]+)`)
|
||||||
m := re.FindSubmatch(b)
|
m := re.FindSubmatch(b)
|
||||||
if len(m) != 2 {
|
if len(m) != 2 {
|
||||||
return ""
|
return ""
|
||||||
@@ -30,7 +28,7 @@ func UUID() string {
|
|||||||
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
|
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)
|
conn, err := net.ListenUDP("udp4", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,7 +64,7 @@ func DiscoveryStreamingHosts() ([]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var hosts []string
|
var urls []string
|
||||||
|
|
||||||
b := make([]byte, 8192)
|
b := make([]byte, 8192)
|
||||||
for {
|
for {
|
||||||
@@ -75,37 +73,28 @@ func DiscoveryStreamingHosts() ([]string, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//log.Printf("[onvif] discovery response addr=%s:\n%s", addr, b[:n])
|
||||||
|
|
||||||
// ignore printers, etc
|
// ignore printers, etc
|
||||||
if !strings.Contains(string(b[:n]), "onvif") {
|
if !strings.Contains(string(b[:n]), "onvif") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
//log.Printf("[onvif] discovery response:\n%s", b[:n])
|
url := FindTagValue(b[:n], "XAddrs")
|
||||||
|
if url == "" {
|
||||||
rawURL := FindTagValue(b[:n], "XAddrs")
|
|
||||||
if rawURL == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(rawURL)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Scheme != "http" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix some buggy cameras
|
// fix some buggy cameras
|
||||||
// <wsdd:XAddrs>http://0.0.0.0:8080/onvif/device_service</wsdd:XAddrs>
|
// <wsdd:XAddrs>http://0.0.0.0:8080/onvif/device_service</wsdd:XAddrs>
|
||||||
if strings.HasPrefix(u.Host, "0.0.0.0") {
|
if strings.HasPrefix(url, "http://0.0.0.0") {
|
||||||
u.Host = addr.IP.String() + u.Host[7:]
|
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 {
|
func atoi(s string) int {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<script>
|
<script>
|
||||||
async function getStreams(url, tableID) {
|
async function getStreams(url, tableID) {
|
||||||
const table = document.getElementById(tableID)
|
const table = document.getElementById(tableID)
|
||||||
|
table.innerText = 'loading...'
|
||||||
|
|
||||||
const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url
|
const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user