install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
## Profiles
|
||||
|
||||
- Profile A - For access control configuration
|
||||
- Profile C - For door control and event management
|
||||
- Profile S - For basic video streaming
|
||||
- Video streaming and configuration
|
||||
- Profile T - For advanced video streaming
|
||||
- H.264 / H.265 video compression
|
||||
- Imaging settings
|
||||
- Motion alarm and tampering events
|
||||
- Metadata streaming
|
||||
- Bi-directional audio
|
||||
|
||||
## Services
|
||||
|
||||
https://www.onvif.org/profiles/specifications/
|
||||
|
||||
- https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl
|
||||
- https://www.onvif.org/ver20/imaging/wsdl/imaging.wsdl
|
||||
- https://www.onvif.org/ver10/media/wsdl/media.wsdl
|
||||
|
||||
## TMP
|
||||
|
||||
| | Dahua | Reolink | TP-Link |
|
||||
|------------------------|---------|---------|---------|
|
||||
| GetCapabilities | no auth | no auth | no auth |
|
||||
| GetServices | no auth | no auth | no auth |
|
||||
| GetServiceCapabilities | no auth | no auth | auth |
|
||||
| GetSystemDateAndTime | no auth | no auth | no auth |
|
||||
| GetNetworkInterfaces | auth | auth | auth |
|
||||
| GetDeviceInformation | auth | auth | auth |
|
||||
| GetProfiles | auth | auth | auth |
|
||||
| GetScopes | auth | auth | auth |
|
||||
|
||||
- Dahua - onvif://192.168.10.90:80
|
||||
- Reolink - onvif://192.168.10.92:8000
|
||||
- TP-Link - onvif://192.168.10.91:2020/onvif/device_service
|
||||
-
|
||||
@@ -0,0 +1,197 @@
|
||||
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}
|
||||
client.deviceURL = baseURL + GetPath(u.Path, PathDevice)
|
||||
|
||||
b, err := client.DeviceRequest(DeviceGetCapabilities)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := FindTagValue(b, "Media.+?XAddr")
|
||||
client.mediaURL = baseURL + GetPath(s, "/onvif/media_service")
|
||||
|
||||
s = FindTagValue(b, "Imaging.+?XAddr")
|
||||
client.imaginURL = baseURL + GetPath(s, "/onvif/imaging_service")
|
||||
|
||||
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, `<trt:GetProfile><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetProfile>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) {
|
||||
return c.Request(c.mediaURL, `<trt:GetVideoSourceConfiguration>
|
||||
<trt:ConfigurationToken>`+token+`</trt:ConfigurationToken>
|
||||
</trt:GetVideoSourceConfiguration>`)
|
||||
}
|
||||
|
||||
func (c *Client) GetStreamUri(token string) ([]byte, error) {
|
||||
return c.Request(c.mediaURL, `<trt:GetStreamUri>
|
||||
<trt:StreamSetup>
|
||||
<tt:Stream>RTP-Unicast</tt:Stream>
|
||||
<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>
|
||||
</trt:StreamSetup>
|
||||
<trt:ProfileToken>`+token+`</trt:ProfileToken>
|
||||
</trt:GetStreamUri>`)
|
||||
}
|
||||
|
||||
func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
|
||||
return c.Request(
|
||||
c.imaginURL, `<trt:GetSnapshotUri><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetSnapshotUri>`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GetServiceCapabilities() ([]byte, error) {
|
||||
// some cameras answer GetServiceCapabilities for media only for path = "/onvif/media"
|
||||
return c.Request(
|
||||
c.mediaURL, `<trt:GetServiceCapabilities />`,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) DeviceRequest(operation string) ([]byte, error) {
|
||||
switch operation {
|
||||
case DeviceGetServices:
|
||||
operation = `<tds:GetServices><tds:IncludeCapability>true</tds:IncludeCapability></tds:GetServices>`
|
||||
case DeviceGetCapabilities:
|
||||
operation = `<tds:GetCapabilities><tds:Category>All</tds:Category></tds:GetCapabilities>`
|
||||
default:
|
||||
operation = `<tds:` + operation + `/>`
|
||||
}
|
||||
return c.Request(c.deviceURL, operation)
|
||||
}
|
||||
|
||||
func (c *Client) MediaRequest(operation string) ([]byte, error) {
|
||||
operation = `<trt:` + 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
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("onvif: wrong response " + res.Status)
|
||||
}
|
||||
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type Envelope struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
const (
|
||||
prefix1 = `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl">`
|
||||
prefix2 = `<s:Body>`
|
||||
suffix = `</s:Body></s:Envelope>`
|
||||
)
|
||||
|
||||
func NewEnvelope() *Envelope {
|
||||
e := &Envelope{buf: make([]byte, 0, 1024)}
|
||||
e.Append(prefix1, prefix2)
|
||||
return e
|
||||
}
|
||||
|
||||
func NewEnvelopeWithUser(user *url.Userinfo) *Envelope {
|
||||
if user == nil {
|
||||
return NewEnvelope()
|
||||
}
|
||||
|
||||
nonce := core.RandString(16, 36)
|
||||
created := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
pass, _ := user.Password()
|
||||
|
||||
h := sha1.New()
|
||||
h.Write([]byte(nonce + created + pass))
|
||||
|
||||
e := &Envelope{buf: make([]byte, 0, 1024)}
|
||||
e.Append(prefix1)
|
||||
e.Appendf(`<s:Header>
|
||||
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
|
||||
<wsse:UsernameToken>
|
||||
<wsse:Username>%s</wsse:Username>
|
||||
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</wsse:Password>
|
||||
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</wsse:Nonce>
|
||||
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</wsu:Created>
|
||||
</wsse:UsernameToken>
|
||||
</wsse:Security>
|
||||
</s:Header>`,
|
||||
user.Username(),
|
||||
base64.StdEncoding.EncodeToString(h.Sum(nil)),
|
||||
base64.StdEncoding.EncodeToString([]byte(nonce)),
|
||||
created)
|
||||
e.Append(prefix2)
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Envelope) Append(args ...string) {
|
||||
for _, s := range args {
|
||||
e.buf = append(e.buf, s...)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Envelope) Appendf(format string, args ...any) {
|
||||
e.buf = fmt.Appendf(e.buf, format, args...)
|
||||
}
|
||||
|
||||
func (e *Envelope) Bytes() []byte {
|
||||
return append(e.buf, suffix...)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
type DiscoveryDevice struct {
|
||||
URL string
|
||||
Name string
|
||||
Hardware string
|
||||
}
|
||||
|
||||
func FindTagValue(b []byte, tag string) string {
|
||||
re := regexp.MustCompile(`(?s)<(?:\w+:)?` + tag + `\b[^>]*>([^<]+)`)
|
||||
m := re.FindSubmatch(b)
|
||||
if len(m) != 2 {
|
||||
return ""
|
||||
}
|
||||
return string(m[1])
|
||||
}
|
||||
|
||||
// UUID - generate something like 44302cbf-0d18-4feb-79b3-33b575263da3
|
||||
func UUID() string {
|
||||
s := core.RandString(32, 16)
|
||||
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
|
||||
}
|
||||
|
||||
// DiscoveryStreamingDevices return list of tuple (onvif_url, name, hardware)
|
||||
func DiscoveryStreamingDevices() ([]DiscoveryDevice, error) {
|
||||
conn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
// https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_Feature_Discovery_Specification_16.07.pdf
|
||||
// 5.3 Discovery Procedure:
|
||||
msg := `<?xml version="1.0" ?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Header xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
|
||||
<a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>
|
||||
<a:MessageID>urn:uuid:` + UUID() + `</a:MessageID>
|
||||
<a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<d:Probe xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery">
|
||||
<d:Types />
|
||||
<d:Scopes />
|
||||
</d:Probe>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
|
||||
addr := &net.UDPAddr{
|
||||
IP: net.IP{239, 255, 255, 250},
|
||||
Port: 3702,
|
||||
}
|
||||
|
||||
if _, err = conn.WriteTo([]byte(msg), addr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
|
||||
var devices []DiscoveryDevice
|
||||
|
||||
b := make([]byte, 8192)
|
||||
for {
|
||||
n, addr, err := conn.ReadFromUDP(b)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
//log.Printf("[onvif] discovery response addr=%s:\n%s", addr, b[:n])
|
||||
|
||||
// ignore printers, etc
|
||||
if !strings.Contains(string(b[:n]), "onvif") {
|
||||
continue
|
||||
}
|
||||
|
||||
device := DiscoveryDevice{
|
||||
URL: FindTagValue(b[:n], "XAddrs"),
|
||||
}
|
||||
|
||||
if device.URL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// fix some buggy cameras
|
||||
// <wsdd:XAddrs>http://0.0.0.0:8080/onvif/device_service</wsdd:XAddrs>
|
||||
if s, ok := strings.CutPrefix(device.URL, "http://0.0.0.0"); ok {
|
||||
device.URL = "http://" + addr.IP.String() + s
|
||||
}
|
||||
|
||||
// try to find the camera name and model (hardware)
|
||||
scopes := FindTagValue(b[:n], "Scopes")
|
||||
device.Name = findScope(scopes, "onvif://www.onvif.org/name/")
|
||||
device.Hardware = findScope(scopes, "onvif://www.onvif.org/hardware/")
|
||||
|
||||
devices = append(devices, device)
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func findScope(s, prefix string) string {
|
||||
s = core.Between(s, prefix, " ")
|
||||
s, _ = url.QueryUnescape(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func atoi(s string) int {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func GetPosixTZ(current time.Time) string {
|
||||
// Thanks to https://github.com/Path-Variable/go-posix-time
|
||||
_, offset := current.Zone()
|
||||
|
||||
if current.IsDST() {
|
||||
_, end := current.ZoneBounds()
|
||||
endPlus1 := end.Add(time.Hour * 25)
|
||||
_, offset = endPlus1.Zone()
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if offset < 0 {
|
||||
prefix = "GMT+"
|
||||
offset = -offset / 60
|
||||
} else {
|
||||
prefix = "GMT-"
|
||||
offset = offset / 60
|
||||
}
|
||||
|
||||
return prefix + fmt.Sprintf("%02d:%02d", offset/60, offset%60)
|
||||
}
|
||||
|
||||
func GetPath(urlOrPath, defPath string) string {
|
||||
if urlOrPath == "" || urlOrPath[0] == '/' {
|
||||
return defPath
|
||||
}
|
||||
u, err := url.Parse(urlOrPath)
|
||||
if err != nil {
|
||||
return defPath
|
||||
}
|
||||
return GetPath(u.Path, defPath)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"html"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetStreamUri(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
xml string
|
||||
url string
|
||||
}{
|
||||
{
|
||||
name: "Dahua stream default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif</tt:Uri><tt:InvalidAfterConnect>true</tt:InvalidAfterConnect><tt:InvalidAfterReboot>true</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetStreamUriResponse></s:Body></s:Envelope>`,
|
||||
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
|
||||
},
|
||||
{
|
||||
name: "Dahua snapshot default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"><s:Header/><s:Body><trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1</tt:Uri><tt:InvalidAfterConnect>false</tt:InvalidAfterConnect><tt:InvalidAfterReboot>false</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetSnapshotUriResponse></s:Body></s:Envelope>`,
|
||||
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
|
||||
},
|
||||
{
|
||||
name: "Dahua stream formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<trt:GetStreamUriResponse>
|
||||
<trt:MediaUri>
|
||||
<tt:Uri>
|
||||
rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif</tt:Uri>
|
||||
<tt:InvalidAfterConnect>true</tt:InvalidAfterConnect>
|
||||
<tt:InvalidAfterReboot>true</tt:InvalidAfterReboot>
|
||||
<tt:Timeout>PT0S</tt:Timeout>
|
||||
</trt:MediaUri>
|
||||
</trt:GetStreamUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
url: "rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif",
|
||||
},
|
||||
{
|
||||
name: "Dahua snapshot formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<trt:GetSnapshotUriResponse>
|
||||
<trt:MediaUri>
|
||||
<tt:Uri>
|
||||
http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1</tt:Uri>
|
||||
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
|
||||
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
|
||||
<tt:Timeout>PT0S</tt:Timeout>
|
||||
</trt:MediaUri>
|
||||
</trt:GetSnapshotUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
url: "http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1",
|
||||
},
|
||||
{
|
||||
name: "Unknown",
|
||||
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SOAP-ENV:Envelope ...>
|
||||
<SOAP-ENV:Header></SOAP-ENV:Header>
|
||||
<SOAP-ENV:Body>
|
||||
<MC1:GetStreamUriResponse>
|
||||
<MC1:MediaUri>
|
||||
<MC2:Uri>
|
||||
rtsp://192.168.5.53:8090/profile1=r
|
||||
</MC2:Uri>
|
||||
</MC1:MediaUri>
|
||||
</MC1:GetStreamUriResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`,
|
||||
url: "rtsp://192.168.5.53:8090/profile1=r",
|
||||
},
|
||||
{
|
||||
name: "go2rtc 1.9.4",
|
||||
xml: `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
|
||||
<trt:MediaUri>
|
||||
<tt:Uri xmlns:tt="http://www.onvif.org/ver10/schema">rtsp://192.168.1.123:8554/rtsp-dahua1</tt:Uri>
|
||||
</trt:MediaUri>
|
||||
</trt:GetStreamUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
url: "rtsp://192.168.1.123:8554/rtsp-dahua1",
|
||||
},
|
||||
{
|
||||
name: "go2rtc 1.9.8",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<s:Body>
|
||||
<trt:GetStreamUriResponse>
|
||||
<trt:MediaUri>
|
||||
<tt:Uri>rtsp://192.168.1.123:8554/rtsp-dahua2</tt:Uri>
|
||||
</trt:MediaUri>
|
||||
</trt:GetStreamUriResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`,
|
||||
url: "rtsp://192.168.1.123:8554/rtsp-dahua2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
uri := FindTagValue([]byte(test.xml), "Uri")
|
||||
uri = strings.TrimSpace(html.UnescapeString(uri))
|
||||
u, err := url.Parse(uri)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, test.url, u.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
xml string
|
||||
}{
|
||||
{
|
||||
name: "Dahua default",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes" ?><s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl"><s:Header/><s:Body><tds:GetCapabilitiesResponse><tds:Capabilities><tt:Analytics><tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr><tt:RuleSupport>true</tt:RuleSupport><tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport></tt:Analytics><tt:Device><tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr><tt:Network><tt:IPFilter>false</tt:IPFilter><tt:ZeroConfiguration>false</tt:ZeroConfiguration><tt:IPVersion6>false</tt:IPVersion6><tt:DynDNS>false</tt:DynDNS><tt:Extension><tt:Dot11Configuration>false</tt:Dot11Configuration></tt:Extension></tt:Network><tt:System><tt:DiscoveryResolve>false</tt:DiscoveryResolve><tt:DiscoveryBye>true</tt:DiscoveryBye><tt:RemoteDiscovery>false</tt:RemoteDiscovery><tt:SystemBackup>false</tt:SystemBackup><tt:SystemLogging>true</tt:SystemLogging><tt:FirmwareUpgrade>true</tt:FirmwareUpgrade><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>00</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>10</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>20</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>30</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>40</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>16</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>20</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:Extension><tt:HttpFirmwareUpgrade>true</tt:HttpFirmwareUpgrade><tt:HttpSystemBackup>false</tt:HttpSystemBackup><tt:HttpSystemLogging>false</tt:HttpSystemLogging><tt:HttpSupportInformation>false</tt:HttpSupportInformation></tt:Extension></tt:System><tt:IO><tt:InputConnectors>2</tt:InputConnectors><tt:RelayOutputs>1</tt:RelayOutputs><tt:Extension><tt:Auxiliary>false</tt:Auxiliary><tt:AuxiliaryCommands></tt:AuxiliaryCommands><tt:Extension></tt:Extension></tt:Extension></tt:IO><tt:Security><tt:TLS1.1>false</tt:TLS1.1><tt:TLS1.2>false</tt:TLS1.2><tt:OnboardKeyGeneration>false</tt:OnboardKeyGeneration><tt:AccessPolicyConfig>false</tt:AccessPolicyConfig><tt:X.509Token>false</tt:X.509Token><tt:SAMLToken>false</tt:SAMLToken><tt:KerberosToken>false</tt:KerberosToken><tt:RELToken>false</tt:RELToken><tt:Extension><tt:TLS1.0>false</tt:TLS1.0><tt:Extension><tt:Dot1X>false</tt:Dot1X><tt:SupportedEAPMethod>0</tt:SupportedEAPMethod><tt:RemoteUserHandling>false</tt:RemoteUserHandling></tt:Extension></tt:Extension></tt:Security></tt:Device><tt:Events><tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr><tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport><tt:WSPullPointSupport>true</tt:WSPullPointSupport><tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport></tt:Events><tt:Imaging><tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr></tt:Imaging><tt:Media><tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr><tt:StreamingCapabilities><tt:RTPMulticast>true</tt:RTPMulticast><tt:RTP_TCP>true</tt:RTP_TCP><tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP></tt:StreamingCapabilities><tt:Extension><tt:ProfileCapabilities><tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles></tt:ProfileCapabilities></tt:Extension></tt:Media><tt:Extension><tt:DeviceIO><tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr><tt:VideoSources>1</tt:VideoSources><tt:VideoOutputs>0</tt:VideoOutputs><tt:AudioSources>1</tt:AudioSources><tt:AudioOutputs>1</tt:AudioOutputs><tt:RelayOutputs>1</tt:RelayOutputs></tt:DeviceIO></tt:Extension></tds:Capabilities></tds:GetCapabilitiesResponse></s:Body></s:Envelope>`,
|
||||
},
|
||||
{
|
||||
name: "Dahua formatted",
|
||||
xml: `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<s:Envelope xmlns:sc="http://www.w3.org/2003/05/soap-encoding"
|
||||
xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:tt="http://www.onvif.org/ver10/schema"
|
||||
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<s:Header />
|
||||
<s:Body>
|
||||
<tds:GetCapabilitiesResponse>
|
||||
<tds:Capabilities>
|
||||
<tt:Analytics>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr>
|
||||
<tt:RuleSupport>true</tt:RuleSupport>
|
||||
<tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport>
|
||||
</tt:Analytics>
|
||||
<tt:Device>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr>
|
||||
<tt:Network>
|
||||
<tt:IPFilter>false</tt:IPFilter>
|
||||
<tt:ZeroConfiguration>false</tt:ZeroConfiguration>
|
||||
<tt:IPVersion6>false</tt:IPVersion6>
|
||||
<tt:DynDNS>false</tt:DynDNS>
|
||||
<tt:Extension>
|
||||
<tt:Dot11Configuration>false</tt:Dot11Configuration>
|
||||
</tt:Extension>
|
||||
</tt:Network>
|
||||
<tt:System>
|
||||
...
|
||||
</tt:System>
|
||||
<tt:IO>
|
||||
<tt:InputConnectors>2</tt:InputConnectors>
|
||||
<tt:RelayOutputs>1</tt:RelayOutputs>
|
||||
<tt:Extension>
|
||||
<tt:Auxiliary>false</tt:Auxiliary>
|
||||
<tt:AuxiliaryCommands></tt:AuxiliaryCommands>
|
||||
<tt:Extension></tt:Extension>
|
||||
</tt:Extension>
|
||||
</tt:IO>
|
||||
<tt:Security>
|
||||
...
|
||||
</tt:Security>
|
||||
</tt:Device>
|
||||
<tt:Events>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr>
|
||||
<tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport>
|
||||
<tt:WSPullPointSupport>true</tt:WSPullPointSupport>
|
||||
<tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport>
|
||||
</tt:Events>
|
||||
<tt:Imaging>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr>
|
||||
</tt:Imaging>
|
||||
<tt:Media>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr>
|
||||
<tt:StreamingCapabilities>
|
||||
<tt:RTPMulticast>true</tt:RTPMulticast>
|
||||
<tt:RTP_TCP>true</tt:RTP_TCP>
|
||||
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
|
||||
</tt:StreamingCapabilities>
|
||||
<tt:Extension>
|
||||
<tt:ProfileCapabilities>
|
||||
<tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles>
|
||||
</tt:ProfileCapabilities>
|
||||
</tt:Extension>
|
||||
</tt:Media>
|
||||
<tt:Extension>
|
||||
<tt:DeviceIO>
|
||||
<tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr>
|
||||
<tt:VideoSources>1</tt:VideoSources>
|
||||
<tt:VideoOutputs>0</tt:VideoOutputs>
|
||||
<tt:AudioSources>1</tt:AudioSources>
|
||||
<tt:AudioOutputs>1</tt:AudioOutputs>
|
||||
<tt:RelayOutputs>1</tt:RelayOutputs>
|
||||
</tt:DeviceIO>
|
||||
</tt:Extension>
|
||||
</tds:Capabilities>
|
||||
</tds:GetCapabilitiesResponse>
|
||||
</s:Body>
|
||||
</s:Envelope>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rawURL := FindTagValue([]byte(test.xml), "Media.+?XAddr")
|
||||
require.Equal(t, "http://192.168.1.123/onvif/media_service", rawURL)
|
||||
|
||||
rawURL = FindTagValue([]byte(test.xml), "Imaging.+?XAddr")
|
||||
require.Equal(t, "http://192.168.1.123/onvif/imaging_service", rawURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
const ServiceGetServiceCapabilities = "GetServiceCapabilities"
|
||||
|
||||
const (
|
||||
DeviceGetCapabilities = "GetCapabilities"
|
||||
DeviceGetDeviceInformation = "GetDeviceInformation"
|
||||
DeviceGetDiscoveryMode = "GetDiscoveryMode"
|
||||
DeviceGetDNS = "GetDNS"
|
||||
DeviceGetHostname = "GetHostname"
|
||||
DeviceGetNetworkDefaultGateway = "GetNetworkDefaultGateway"
|
||||
DeviceGetNetworkInterfaces = "GetNetworkInterfaces"
|
||||
DeviceGetNetworkProtocols = "GetNetworkProtocols"
|
||||
DeviceGetNTP = "GetNTP"
|
||||
DeviceGetScopes = "GetScopes"
|
||||
DeviceGetServices = "GetServices"
|
||||
DeviceGetSystemDateAndTime = "GetSystemDateAndTime"
|
||||
DeviceSetSystemDateAndTime = "SetSystemDateAndTime"
|
||||
DeviceSystemReboot = "SystemReboot"
|
||||
)
|
||||
|
||||
const (
|
||||
MediaGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations"
|
||||
MediaGetAudioSources = "GetAudioSources"
|
||||
MediaGetAudioSourceConfigurations = "GetAudioSourceConfigurations"
|
||||
MediaGetProfile = "GetProfile"
|
||||
MediaGetProfiles = "GetProfiles"
|
||||
MediaGetSnapshotUri = "GetSnapshotUri"
|
||||
MediaGetStreamUri = "GetStreamUri"
|
||||
MediaGetVideoEncoderConfiguration = "GetVideoEncoderConfiguration"
|
||||
MediaGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations"
|
||||
MediaGetVideoEncoderConfigurationOptions = "GetVideoEncoderConfigurationOptions"
|
||||
MediaGetVideoSources = "GetVideoSources"
|
||||
MediaGetVideoSourceConfiguration = "GetVideoSourceConfiguration"
|
||||
MediaGetVideoSourceConfigurations = "GetVideoSourceConfigurations"
|
||||
)
|
||||
|
||||
func GetRequestAction(b []byte) string {
|
||||
// <soap-env:Body><ns0:GetCapabilities xmlns:ns0="http://www.onvif.org/ver10/device/wsdl">
|
||||
// <v:Body><GetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl" /></v:Body>
|
||||
re := regexp.MustCompile(`Body[^<]+<([^ />]+)`)
|
||||
m := re.FindSubmatch(b)
|
||||
if len(m) != 2 {
|
||||
return ""
|
||||
}
|
||||
if i := bytes.IndexByte(m[1], ':'); i > 0 {
|
||||
return string(m[1][i+1:])
|
||||
}
|
||||
return string(m[1])
|
||||
}
|
||||
|
||||
func GetCapabilitiesResponse(host string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Appendf(`<tds:GetCapabilitiesResponse>
|
||||
<tds:Capabilities>
|
||||
<tt:Device>
|
||||
<tt:XAddr>http://%s/onvif/device_service</tt:XAddr>
|
||||
</tt:Device>
|
||||
<tt:Media>
|
||||
<tt:XAddr>http://%s/onvif/media_service</tt:XAddr>
|
||||
<tt:StreamingCapabilities>
|
||||
<tt:RTPMulticast>false</tt:RTPMulticast>
|
||||
<tt:RTP_TCP>false</tt:RTP_TCP>
|
||||
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
|
||||
</tt:StreamingCapabilities>
|
||||
</tt:Media>
|
||||
</tds:Capabilities>
|
||||
</tds:GetCapabilitiesResponse>`, host, host)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetServicesResponse(host string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Appendf(`<tds:GetServicesResponse>
|
||||
<tds:Service>
|
||||
<tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>
|
||||
<tds:XAddr>http://%s/onvif/device_service</tds:XAddr>
|
||||
<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
|
||||
</tds:Service>
|
||||
<tds:Service>
|
||||
<tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>
|
||||
<tds:XAddr>http://%s/onvif/media_service</tds:XAddr>
|
||||
<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
|
||||
</tds:Service>
|
||||
</tds:GetServicesResponse>`, host, host)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetSystemDateAndTimeResponse() []byte {
|
||||
loc := time.Now()
|
||||
utc := loc.UTC()
|
||||
|
||||
e := NewEnvelope()
|
||||
e.Appendf(`<tds:GetSystemDateAndTimeResponse>
|
||||
<tds:SystemDateAndTime>
|
||||
<tt:DateTimeType>NTP</tt:DateTimeType>
|
||||
<tt:DaylightSavings>true</tt:DaylightSavings>
|
||||
<tt:TimeZone>
|
||||
<tt:TZ>%s</tt:TZ>
|
||||
</tt:TimeZone>
|
||||
<tt:UTCDateTime>
|
||||
<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
|
||||
<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
|
||||
</tt:UTCDateTime>
|
||||
<tt:LocalDateTime>
|
||||
<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
|
||||
<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
|
||||
</tt:LocalDateTime>
|
||||
</tds:SystemDateAndTime>
|
||||
</tds:GetSystemDateAndTimeResponse>`,
|
||||
GetPosixTZ(loc),
|
||||
utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(),
|
||||
loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(),
|
||||
)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Appendf(`<tds:GetDeviceInformationResponse>
|
||||
<tds:Manufacturer>%s</tds:Manufacturer>
|
||||
<tds:Model>%s</tds:Model>
|
||||
<tds:FirmwareVersion>%s</tds:FirmwareVersion>
|
||||
<tds:SerialNumber>%s</tds:SerialNumber>
|
||||
<tds:HardwareId>1.00</tds:HardwareId>
|
||||
</tds:GetDeviceInformationResponse>`, manuf, model, firmware, serial)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetProfilesResponse(names []string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Append(`<trt:GetProfilesResponse>`)
|
||||
for _, name := range names {
|
||||
appendProfile(e, "Profiles", name)
|
||||
}
|
||||
e.Append(`</trt:GetProfilesResponse>`)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetProfileResponse(name string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Append(`<trt:GetProfileResponse>`)
|
||||
appendProfile(e, "Profile", name)
|
||||
e.Append(`</trt:GetProfileResponse>`)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func appendProfile(e *Envelope, tag, name string) {
|
||||
// go2rtc name = ONVIF Profile Name = ONVIF Profile token
|
||||
e.Appendf(`<trt:%s token="%s" fixed="true">`, tag, name)
|
||||
e.Appendf(`<tt:Name>%s</tt:Name>`, name)
|
||||
appendVideoSourceConfiguration(e, "VideoSourceConfiguration", name)
|
||||
appendVideoEncoderConfiguration(e, "VideoEncoderConfiguration")
|
||||
e.Appendf(`</trt:%s>`, tag)
|
||||
}
|
||||
|
||||
func GetVideoSourcesResponse(names []string) []byte {
|
||||
// go2rtc name = ONVIF VideoSource token
|
||||
e := NewEnvelope()
|
||||
e.Append(`<trt:GetVideoSourcesResponse>`)
|
||||
for _, name := range names {
|
||||
e.Appendf(`<trt:VideoSources token="%s">
|
||||
<tt:Framerate>30.000000</tt:Framerate>
|
||||
<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
|
||||
</trt:VideoSources>`, name)
|
||||
}
|
||||
e.Append(`</trt:GetVideoSourcesResponse>`)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetVideoSourceConfigurationsResponse(names []string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Append(`<trt:GetVideoSourceConfigurationsResponse>`)
|
||||
for _, name := range names {
|
||||
appendVideoSourceConfiguration(e, "Configurations", name)
|
||||
}
|
||||
e.Append(`</trt:GetVideoSourceConfigurationsResponse>`)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetVideoSourceConfigurationResponse(name string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Append(`<trt:GetVideoSourceConfigurationResponse>`)
|
||||
appendVideoSourceConfiguration(e, "Configuration", name)
|
||||
e.Append(`</trt:GetVideoSourceConfigurationResponse>`)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func appendVideoSourceConfiguration(e *Envelope, tag, name string) {
|
||||
// go2rtc name = ONVIF VideoSourceConfiguration token
|
||||
e.Appendf(`<tt:%s token="%s" fixed="true">
|
||||
<tt:Name>VSC</tt:Name>
|
||||
<tt:SourceToken>%s</tt:SourceToken>
|
||||
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
|
||||
</tt:%s>`, tag, name, name, tag)
|
||||
}
|
||||
|
||||
func GetVideoEncoderConfigurationsResponse() []byte {
|
||||
e := NewEnvelope()
|
||||
e.Append(`<trt:GetVideoEncoderConfigurationsResponse>`)
|
||||
appendVideoEncoderConfiguration(e, "VideoEncoderConfigurations")
|
||||
e.Append(`</trt:GetVideoEncoderConfigurationsResponse>`)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetVideoEncoderConfigurationResponse() []byte {
|
||||
e := NewEnvelope()
|
||||
e.Append(`<trt:GetVideoEncoderConfigurationResponse>`)
|
||||
appendVideoEncoderConfiguration(e, "VideoEncoderConfiguration")
|
||||
e.Append(`</trt:GetVideoEncoderConfigurationResponse>`)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func appendVideoEncoderConfiguration(e *Envelope, tag string) {
|
||||
// empty `RateControl` important for UniFi Protect
|
||||
e.Appendf(`<tt:%s token="vec">
|
||||
<tt:Name>VEC</tt:Name>
|
||||
<tt:UseCount>1</tt:UseCount>
|
||||
<tt:Encoding>H264</tt:Encoding>
|
||||
<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
|
||||
<tt:Quality>0</tt:Quality>
|
||||
<tt:RateControl><tt:FrameRateLimit>30</tt:FrameRateLimit><tt:EncodingInterval>1</tt:EncodingInterval><tt:BitrateLimit>8192</tt:BitrateLimit></tt:RateControl>
|
||||
<tt:H264><tt:GovLength>10</tt:GovLength><tt:H264Profile>Main</tt:H264Profile></tt:H264>
|
||||
<tt:SessionTimeout>PT10S</tt:SessionTimeout>
|
||||
</tt:%s>`, tag, tag)
|
||||
}
|
||||
|
||||
func GetStreamUriResponse(uri string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Appendf(`<trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>%s</tt:Uri></trt:MediaUri></trt:GetStreamUriResponse>`, uri)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func GetSnapshotUriResponse(uri string) []byte {
|
||||
e := NewEnvelope()
|
||||
e.Appendf(`<trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>%s</tt:Uri></trt:MediaUri></trt:GetSnapshotUriResponse>`, uri)
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
func StaticResponse(operation string) []byte {
|
||||
switch operation {
|
||||
case DeviceGetSystemDateAndTime:
|
||||
return GetSystemDateAndTimeResponse()
|
||||
case MediaGetVideoEncoderConfiguration:
|
||||
return GetVideoEncoderConfigurationResponse()
|
||||
case MediaGetVideoEncoderConfigurations:
|
||||
return GetVideoEncoderConfigurationsResponse()
|
||||
}
|
||||
|
||||
e := NewEnvelope()
|
||||
e.Append(responses[operation])
|
||||
return e.Bytes()
|
||||
}
|
||||
|
||||
var responses = map[string]string{
|
||||
ServiceGetServiceCapabilities: `<trt:GetServiceCapabilitiesResponse>
|
||||
<trt:Capabilities SnapshotUri="true" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false">
|
||||
<trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" />
|
||||
</trt:Capabilities>
|
||||
</trt:GetServiceCapabilitiesResponse>`,
|
||||
|
||||
DeviceGetDiscoveryMode: `<tds:GetDiscoveryModeResponse><tds:DiscoveryMode>Discoverable</tds:DiscoveryMode></tds:GetDiscoveryModeResponse>`,
|
||||
DeviceGetDNS: `<tds:GetDNSResponse><tds:DNSInformation /></tds:GetDNSResponse>`,
|
||||
DeviceGetHostname: `<tds:GetHostnameResponse><tds:HostnameInformation /></tds:GetHostnameResponse>`,
|
||||
DeviceGetNetworkDefaultGateway: `<tds:GetNetworkDefaultGatewayResponse><tds:NetworkGateway /></tds:GetNetworkDefaultGatewayResponse>`,
|
||||
DeviceGetNTP: `<tds:GetNTPResponse><tds:NTPInformation /></tds:GetNTPResponse>`,
|
||||
DeviceSetSystemDateAndTime: `<tds:SetSystemDateAndTimeResponse />`,
|
||||
DeviceSystemReboot: `<tds:SystemRebootResponse><tds:Message>OK</tds:Message></tds:SystemRebootResponse>`,
|
||||
|
||||
DeviceGetNetworkInterfaces: `<tds:GetNetworkInterfacesResponse />`,
|
||||
DeviceGetNetworkProtocols: `<tds:GetNetworkProtocolsResponse />`,
|
||||
DeviceGetScopes: `<tds:GetScopesResponse>
|
||||
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/name/go2rtc</tt:ScopeItem></tds:Scopes>
|
||||
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/location/github</tt:ScopeItem></tds:Scopes>
|
||||
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/Profile/Streaming</tt:ScopeItem></tds:Scopes>
|
||||
<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/type/Network_Video_Transmitter</tt:ScopeItem></tds:Scopes>
|
||||
</tds:GetScopesResponse>`,
|
||||
|
||||
MediaGetAudioEncoderConfigurations: `<trt:GetAudioEncoderConfigurationsResponse />`,
|
||||
MediaGetAudioSources: `<trt:GetAudioSourcesResponse />`,
|
||||
MediaGetAudioSourceConfigurations: `<trt:GetAudioSourceConfigurationsResponse />`,
|
||||
|
||||
MediaGetVideoEncoderConfigurationOptions: `<trt:GetVideoEncoderConfigurationOptionsResponse>
|
||||
<trt:Options>
|
||||
<tt:QualityRange><tt:Min>1</tt:Min><tt:Max>6</tt:Max></tt:QualityRange>
|
||||
<tt:H264>
|
||||
<tt:ResolutionsAvailable><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:ResolutionsAvailable>
|
||||
<tt:GovLengthRange><tt:Min>0</tt:Min><tt:Max>100</tt:Max></tt:GovLengthRange>
|
||||
<tt:FrameRateRange><tt:Min>1</tt:Min><tt:Max>30</tt:Max></tt:FrameRateRange>
|
||||
<tt:EncodingIntervalRange><tt:Min>1</tt:Min><tt:Max>100</tt:Max></tt:EncodingIntervalRange>
|
||||
<tt:H264ProfilesSupported>Main</tt:H264ProfilesSupported>
|
||||
</tt:H264>
|
||||
</trt:Options>
|
||||
</trt:GetVideoEncoderConfigurationOptionsResponse>`,
|
||||
}
|
||||
Reference in New Issue
Block a user