Improve ONVIF server

This commit is contained in:
Alex X
2024-12-30 22:34:08 +03:00
parent 2c3219ffcb
commit f601c47218
8 changed files with 506 additions and 346 deletions

View File

@@ -0,0 +1,72 @@
package main
import (
"log"
"net"
"net/url"
"os"
"github.com/AlexxIT/go2rtc/pkg/onvif"
)
func main() {
var rawURL = os.Args[1]
var operation = os.Args[2]
var token string
if len(os.Args) > 3 {
token = os.Args[3]
}
client, err := onvif.NewClient(rawURL)
if err != nil {
log.Panic(err)
}
var b []byte
switch operation {
case onvif.ServiceGetServiceCapabilities:
b, err = client.MediaRequest(operation)
case onvif.DeviceGetCapabilities,
onvif.DeviceGetDeviceInformation,
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkInterfaces,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes,
onvif.DeviceGetServices,
onvif.DeviceGetSystemDateAndTime,
onvif.DeviceSystemReboot:
b, err = client.DeviceRequest(operation)
case onvif.MediaGetProfiles, onvif.MediaGetVideoSources:
b, err = client.MediaRequest(operation)
case onvif.MediaGetProfile:
b, err = client.GetProfile(token)
case onvif.MediaGetVideoSourceConfiguration:
b, err = client.GetVideoSourceConfiguration(token)
case onvif.MediaGetStreamUri:
b, err = client.GetStreamUri(token)
case onvif.MediaGetSnapshotUri:
b, err = client.GetSnapshotUri(token)
default:
log.Printf("unknown action\n")
}
if err != nil {
log.Printf("%s\n", err)
}
u, err := url.Parse(rawURL)
if err != nil {
log.Fatal(err)
}
host, _, _ := net.SplitHostPort(u.Host)
if err = os.WriteFile(host+"_"+operation+".xml", b, 0644); err != nil {
log.Printf("%s\n", err)
}
}

25
internal/onvif/README.md Normal file
View File

@@ -0,0 +1,25 @@
# ONVIF
A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
Go2rtc has one video source and one profile per stream.
## Tested clients
Go2rtc works as ONVIF server:
- Happytime onvif client (windows)
- Home Assistant ONVIF integration (linux)
- Onvier (android)
- ONVIF Device Manager (windows)
PS. Support only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
## Tested cameras
Go2rtc works as ONVIF client:
- Dahua IPC-K42
- OpenIPC
- Reolink RLC-520A
- TP-Link Tapo TC60

View File

@@ -55,55 +55,65 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
return return
} }
action := onvif.GetRequestAction(b) operation := onvif.GetRequestAction(b)
if action == "" { if operation == "" {
http.Error(w, "malformed request body", http.StatusBadRequest) http.Error(w, "malformed request body", http.StatusBadRequest)
return return
} }
log.Trace().Msgf("[onvif] %s", action) log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
var res string switch operation {
case onvif.DeviceGetNetworkInterfaces, // important for Hass
onvif.DeviceGetSystemDateAndTime, // important for Hass
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes:
b = onvif.StaticResponse(operation)
switch action { case onvif.DeviceGetCapabilities:
case onvif.ActionGetCapabilities:
// important for Hass: Media section // important for Hass: Media section
res = onvif.GetCapabilitiesResponse(r.Host) b = onvif.GetCapabilitiesResponse(r.Host)
case onvif.ActionGetServices: case onvif.DeviceGetServices:
res = onvif.GetServicesResponse(r.Host) b = onvif.GetServicesResponse(r.Host)
case onvif.ActionGetSystemDateAndTime: case onvif.DeviceGetDeviceInformation:
// important for Hass
res = onvif.GetSystemDateAndTimeResponse()
case onvif.ActionGetNetworkInterfaces:
// important for Hass: none
res = onvif.GetNetworkInterfacesResponse()
case onvif.ActionGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID) // important for Hass: SerialNumber (unique server ID)
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host) b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.ActionGetServiceCapabilities: case onvif.ServiceGetServiceCapabilities:
// important for Hass // important for Hass
res = onvif.GetServiceCapabilitiesResponse() // TODO: check path links to media
b = onvif.GetMediaServiceCapabilitiesResponse()
case onvif.ActionSystemReboot: case onvif.DeviceSystemReboot:
res = onvif.SystemRebootResponse() b = onvif.StaticResponse(operation)
time.AfterFunc(time.Second, func() { time.AfterFunc(time.Second, func() {
os.Exit(0) os.Exit(0)
}) })
case onvif.ActionGetProfiles: case onvif.MediaGetVideoSources:
b = onvif.GetVideoSourcesResponse(streams.GetAll())
case onvif.MediaGetProfiles:
// important for Hass: H264 codec, width, height // important for Hass: H264 codec, width, height
res = onvif.GetProfilesResponse(streams.GetAll()) b = onvif.GetProfilesResponse(streams.GetAll())
case onvif.ActionGetVideoSources: case onvif.MediaGetProfile:
res = onvif.GetVideoSourcesResponse(streams.GetAll()) token := onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetProfileResponse(token)
case onvif.ActionGetStreamUri: case onvif.MediaGetVideoSourceConfiguration:
token := onvif.FindTagValue(b, "ConfigurationToken")
b = onvif.GetVideoSourceConfigurationResponse(token)
case onvif.MediaGetStreamUri:
host, _, err := net.SplitHostPort(r.Host) host, _, err := net.SplitHostPort(r.Host)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -111,20 +121,22 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
} }
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken") uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
res = onvif.GetStreamUriResponse(uri) b = onvif.GetStreamUriResponse(uri)
case onvif.ActionGetSnapshotUri: case onvif.MediaGetSnapshotUri:
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken") uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
res = onvif.GetSnapshotUriResponse(uri) b = onvif.GetSnapshotUriResponse(uri)
default: default:
http.Error(w, "unsupported action", http.StatusBadRequest) http.Error(w, "unsupported operation", http.StatusBadRequest)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b) log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return return
} }
log.Trace().Msgf("[onvif] server response:\n%s", b)
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8") w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
if _, err = w.Write([]byte(res)); err != nil { if _, err = w.Write(b); err != nil {
log.Error().Err(err).Caller().Send() log.Error().Err(err).Caller().Send()
} }
} }
@@ -170,7 +182,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
} }
if l := log.Trace(); l.Enabled() { if l := log.Trace(); l.Enabled() {
b, _ := client.GetProfiles() b, _ := client.MediaRequest(onvif.MediaGetProfiles)
l.Msgf("[onvif] src=%s profiles:\n%s", src, b) l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
} }

38
pkg/onvif/README.md Normal file
View File

@@ -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
-

View File

@@ -2,8 +2,6 @@ package onvif
import ( import (
"bytes" "bytes"
"crypto/sha1"
"encoding/base64"
"errors" "errors"
"html" "html"
"io" "io"
@@ -12,8 +10,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/AlexxIT/go2rtc/pkg/core"
) )
const PathDevice = "/onvif/device_service" const PathDevice = "/onvif/device_service"
@@ -41,7 +37,7 @@ func NewClient(rawURL string) (*Client, error) {
client.deviceURL = baseURL + u.Path client.deviceURL = baseURL + u.Path
} }
b, err := client.GetCapabilities() b, err := client.DeviceRequest(DeviceGetCapabilities)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -95,7 +91,7 @@ func (c *Client) GetURI() (string, error) {
} }
func (c *Client) GetName() (string, error) { func (c *Client) GetName() (string, error) {
b, err := c.GetDeviceInformation() b, err := c.DeviceRequest(DeviceGetDeviceInformation)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -104,7 +100,7 @@ func (c *Client) GetName() (string, error) {
} }
func (c *Client) GetProfilesTokens() ([]string, error) { func (c *Client) GetProfilesTokens() ([]string, error) {
b, err := c.GetProfiles() b, err := c.MediaRequest(MediaGetProfiles)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -127,86 +123,53 @@ func (c *Client) HasSnapshots() bool {
return strings.Contains(string(b), `SnapshotUri="true"`) return strings.Contains(string(b), `SnapshotUri="true"`)
} }
func (c *Client) GetCapabilities() ([]byte, error) { func (c *Client) GetProfile(token string) ([]byte, error) {
return c.Request( return c.Request(
c.deviceURL, c.mediaURL, `<trt:GetProfile><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetProfile>`,
`<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<tds:Category>All</tds:Category>
</tds:GetCapabilities>`,
) )
} }
func (c *Client) GetNetworkInterfaces() ([]byte, error) { func (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) {
return c.Request( return c.Request(c.mediaURL, `<trt:GetVideoSourceConfiguration>
c.deviceURL, `<tds:GetNetworkInterfaces xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`, <trt:ConfigurationToken>`+token+`</trt:ConfigurationToken>
) </trt:GetVideoSourceConfiguration>`)
}
func (c *Client) GetDeviceInformation() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`,
)
}
func (c *Client) GetProfiles() ([]byte, error) {
return c.Request(
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(c.mediaURL, `<trt:GetStreamUri>
c.mediaURL,
`<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>
<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport> <tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>
</trt:StreamSetup> </trt:StreamSetup>
<trt:ProfileToken>`+token+`</trt:ProfileToken> <trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetStreamUri>`, </trt:GetStreamUri>`)
)
} }
func (c *Client) GetSnapshotUri(token string) ([]byte, error) { func (c *Client) GetSnapshotUri(token string) ([]byte, error) {
return c.Request( return c.Request(
c.imaginURL, c.imaginURL, `<trt:GetSnapshotUri><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetSnapshotUri>`,
`<trt:GetSnapshotUri xmlns:trt="http://www.onvif.org/ver10/media/wsdl">
<trt:ProfileToken>`+token+`</trt:ProfileToken>
</trt:GetSnapshotUri>`,
)
}
func (c *Client) GetSystemDateAndTime() ([]byte, error) {
return c.Request(
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(
c.mediaURL, `<trt:GetServiceCapabilities xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`, c.mediaURL, `<trt:GetServiceCapabilities />`,
) )
} }
func (c *Client) SystemReboot() ([]byte, error) { func (c *Client) DeviceRequest(operation string) ([]byte, error) {
return c.Request( if operation == DeviceGetServices {
c.deviceURL, `<tds:SystemReboot xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>`, operation = `<tds:GetServices><tds:IncludeCapability>true</tds:IncludeCapability></tds:GetServices>`
) } else {
operation = `<tds:` + operation + `/>`
}
return c.Request(c.deviceURL, operation)
} }
func (c *Client) GetServices() ([]byte, error) { func (c *Client) MediaRequest(operation string) ([]byte, error) {
return c.Request( operation = `<trt:` + operation + `/>`
c.deviceURL, `<tds:GetServices xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> return c.Request(c.mediaURL, operation)
<tds:IncludeCapability>true</tds:IncludeCapability>
</tds:GetServices>`,
)
}
func (c *Client) GetScopes() ([]byte, error) {
return c.Request(
c.deviceURL, `<tds:GetScopes xmlns:tds="http://www.onvif.org/ver10/device/wsdl" />`,
)
} }
func (c *Client) Request(url, body string) ([]byte, error) { func (c *Client) Request(url, body string) ([]byte, error) {
@@ -214,35 +177,11 @@ func (c *Client) Request(url, body string) ([]byte, error) {
return nil, errors.New("onvif: unsupported service") return nil, errors.New("onvif: unsupported service")
} }
buf := bytes.NewBuffer(nil) e := NewEnvelopeWithUser(c.url.User)
buf.WriteString( e.Append(body)
`<?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">`,
)
if user := c.url.User; user != nil {
nonce := core.RandString(16, 36)
created := time.Now().UTC().Format(time.RFC3339Nano)
pass, _ := user.Password()
h := sha1.New()
h.Write([]byte(nonce + created + pass))
buf.WriteString(`<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>` + user.Username() + `</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">` + base64.StdEncoding.EncodeToString(h.Sum(nil)) + `</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">` + created + `</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>`)
}
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(url, `application/soap+xml;charset=utf-8`, buf) res, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes()))
if err != nil { if err != nil {
return nil, err return nil, err
} }

79
pkg/onvif/envelope.go Normal file
View File

@@ -0,0 +1,79 @@
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...)
}

View File

@@ -1,6 +1,7 @@
package onvif package onvif
import ( import (
"fmt"
"net" "net"
"regexp" "regexp"
"strconv" "strconv"
@@ -106,3 +107,25 @@ func atoi(s string) int {
} }
return i 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)
}

View File

@@ -2,31 +2,40 @@ package onvif
import ( import (
"bytes" "bytes"
"fmt"
"regexp" "regexp"
"strconv"
"time" "time"
) )
const ( const ServiceGetServiceCapabilities = "GetServiceCapabilities"
ActionGetCapabilities = "GetCapabilities"
ActionGetSystemDateAndTime = "GetSystemDateAndTime"
ActionGetNetworkInterfaces = "GetNetworkInterfaces"
ActionGetDeviceInformation = "GetDeviceInformation"
ActionGetServiceCapabilities = "GetServiceCapabilities"
ActionGetProfiles = "GetProfiles"
ActionGetStreamUri = "GetStreamUri"
ActionGetSnapshotUri = "GetSnapshotUri"
ActionSystemReboot = "SystemReboot"
ActionGetServices = "GetServices" const (
ActionGetScopes = "GetScopes" DeviceGetCapabilities = "GetCapabilities"
ActionGetVideoSources = "GetVideoSources" DeviceGetDeviceInformation = "GetDeviceInformation"
ActionGetAudioSources = "GetAudioSources" DeviceGetDiscoveryMode = "GetDiscoveryMode"
ActionGetVideoSourceConfigurations = "GetVideoSourceConfigurations" DeviceGetDNS = "GetDNS"
ActionGetAudioSourceConfigurations = "GetAudioSourceConfigurations" DeviceGetHostname = "GetHostname"
ActionGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations" DeviceGetNetworkDefaultGateway = "GetNetworkDefaultGateway"
ActionGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations" DeviceGetNetworkInterfaces = "GetNetworkInterfaces"
DeviceGetNetworkProtocols = "GetNetworkProtocols"
DeviceGetNTP = "GetNTP"
DeviceGetScopes = "GetScopes"
DeviceGetServices = "GetServices"
DeviceGetSystemDateAndTime = "GetSystemDateAndTime"
DeviceSystemReboot = "SystemReboot"
)
const (
MediaGetAudioEncoderConfigurations = "GetAudioEncoderConfigurations"
MediaGetAudioSources = "GetAudioSources"
MediaGetAudioSourceConfigurations = "GetAudioSourceConfigurations"
MediaGetProfile = "GetProfile"
MediaGetProfiles = "GetProfiles"
MediaGetSnapshotUri = "GetSnapshotUri"
MediaGetStreamUri = "GetStreamUri"
MediaGetVideoEncoderConfigurations = "GetVideoEncoderConfigurations"
MediaGetVideoSources = "GetVideoSources"
MediaGetVideoSourceConfiguration = "GetVideoSourceConfiguration"
MediaGetVideoSourceConfigurations = "GetVideoSourceConfigurations"
) )
func GetRequestAction(b []byte) string { func GetRequestAction(b []byte) string {
@@ -43,236 +52,199 @@ func GetRequestAction(b []byte) string {
return string(m[1]) return string(m[1])
} }
func GetCapabilitiesResponse(host string) string { func GetCapabilitiesResponse(host string) []byte {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> e := NewEnvelope()
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> e.Append(`<tds:GetCapabilitiesResponse>
<s:Body> <tds:Capabilities>
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> <tt:Device>
<tds:Capabilities xmlns:tt="http://www.onvif.org/ver10/schema"> <tt:XAddr>http://`, host, `/onvif/device_service</tt:XAddr>
<tt:Device> </tt:Device>
<tt:XAddr>http://` + host + `/onvif/device_service</tt:XAddr> <tt:Media>
</tt:Device> <tt:XAddr>http://`, host, `/onvif/media_service</tt:XAddr>
<tt:Media> <tt:StreamingCapabilities>
<tt:XAddr>http://` + host + `/onvif/media_service</tt:XAddr> <tt:RTPMulticast>false</tt:RTPMulticast>
<tt:StreamingCapabilities> <tt:RTP_TCP>false</tt:RTP_TCP>
<tt:RTPMulticast>false</tt:RTPMulticast> <tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>
<tt:RTP_TCP>false</tt:RTP_TCP> </tt:StreamingCapabilities>
<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP> </tt:Media>
</tt:StreamingCapabilities> </tds:Capabilities>
</tt:Media> </tds:GetCapabilitiesResponse>`)
</tds:Capabilities> return e.Bytes()
</tds:GetCapabilitiesResponse>
</s:Body>
</s:Envelope>`
} }
func GetServicesResponse(host string) string { func GetServicesResponse(host string) []byte {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> e := NewEnvelope()
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> e.Append(`<tds:GetServicesResponse>
<s:Body> <tds:Service>
<tds:GetServicesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> <tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>
<tds:Service> <tds:XAddr>http://`, host, `/onvif/device_service</tds:XAddr>
<tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace> <tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
<tds:XAddr>http://` + host + `/onvif/device_service</tds:XAddr> </tds:Service>
<tds:Version> <tds:Service>
<tds:Major>2</tds:Major> <tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>
<tds:Minor>5</tds:Minor> <tds:XAddr>http://`, host, `/onvif/media_service</tds:XAddr>
</tds:Version> <tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>
</tds:Service> </tds:Service>
<tds:Service> </tds:GetServicesResponse>`)
<tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace> return e.Bytes()
<tds:XAddr>http://` + host + `/onvif/media_service</tds:XAddr>
<tds:Version>
<tds:Major>2</tds:Major>
<tds:Minor>5</tds:Minor>
</tds:Version>
</tds:Service>
</tds:GetServicesResponse>
</s:Body>
</s:Envelope>`
} }
func GetSystemDateAndTimeResponse() string { func GetSystemDateAndTimeResponse() []byte {
loc := time.Now() loc := time.Now()
utc := loc.UTC() utc := loc.UTC()
return fmt.Sprintf(`<?xml version="1.0" encoding="utf-8" standalone="yes"?> e := NewEnvelope()
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> e.Appendf(`<tds:GetSystemDateAndTimeResponse>
<s:Body> <tds:SystemDateAndTime>
<tds:GetSystemDateAndTimeResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> <tt:DateTimeType>NTP</tt:DateTimeType>
<tds:SystemDateAndTime xmlns:tt="http://www.onvif.org/ver10/schema"> <tt:DaylightSavings>true</tt:DaylightSavings>
<tt:DateTimeType>NTP</tt:DateTimeType> <tt:TimeZone>
<tt:DaylightSavings>false</tt:DaylightSavings> <tt:TZ>%s</tt:TZ>
<tt:TimeZone> </tt:TimeZone>
<tt:TZ>GMT%s</tt:TZ> <tt:UTCDateTime>
</tt:TimeZone> <tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
<tt:UTCDateTime> <tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
<tt:Time> </tt:UTCDateTime>
<tt:Hour>%d</tt:Hour> <tt:LocalDateTime>
<tt:Minute>%d</tt:Minute> <tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>
<tt:Second>%d</tt:Second> <tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>
</tt:Time> </tt:LocalDateTime>
<tt:Date> </tds:SystemDateAndTime>
<tt:Year>%d</tt:Year> </tds:GetSystemDateAndTimeResponse>`,
<tt:Month>%d</tt:Month> GetPosixTZ(loc),
<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>
</s:Body>
</s:Envelope>`,
loc.Format("-07:00"),
utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(), utc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(),
loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(), loc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(),
) )
return e.Bytes()
} }
func GetNetworkInterfacesResponse() string { func GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> e := NewEnvelope()
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> e.Append(`<tds:GetDeviceInformationResponse>
<s:Body> <tds:Manufacturer>`, manuf, `</tds:Manufacturer>
<tds:GetNetworkInterfacesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/> <tds:Model>`, model, `</tds:Model>
</s:Body> <tds:FirmwareVersion>`, firmware, `</tds:FirmwareVersion>
</s:Envelope>` <tds:SerialNumber>`, serial, `</tds:SerialNumber>
<tds:HardwareId>1.00</tds:HardwareId>
</tds:GetDeviceInformationResponse>`)
return e.Bytes()
} }
func GetDeviceInformationResponse(manuf, model, firmware, serial string) string { func GetMediaServiceCapabilitiesResponse() []byte {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> e := NewEnvelope()
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> e.Append(`<trt:GetServiceCapabilitiesResponse>
<s:Body> <trt:Capabilities SnapshotUri="true" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false">
<tds:GetDeviceInformationResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> <trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" />
<tds:Manufacturer>` + manuf + `</tds:Manufacturer> </trt:Capabilities>
<tds:Model>` + model + `</tds:Model> </trt:GetServiceCapabilitiesResponse>`)
<tds:FirmwareVersion>` + firmware + `</tds:FirmwareVersion> return e.Bytes()
<tds:SerialNumber>` + serial + `</tds:SerialNumber>
<tds:HardwareId>1.00</tds:HardwareId>
</tds:GetDeviceInformationResponse>
</s:Body>
</s:Envelope>`
} }
func GetServiceCapabilitiesResponse() string { func GetProfilesResponse(names []string) []byte {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> e := NewEnvelope()
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> e.Append(`<trt:GetProfilesResponse>
<s:Body> `)
<trt:GetServiceCapabilitiesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl"> for _, name := range names {
<trt:Capabilities SnapshotUri="true" Rotation="false" VideoSourceMode="false" OSD="false" TemporaryOSDText="false" EXICompression="false"> appendProfile(e, "Profiles", name)
<trt:StreamingCapabilities RTPMulticast="false" RTP_TCP="false" RTP_RTSP_TCP="true" NonAggregateControl="false" NoRTSPStreaming="false" /> }
</trt:Capabilities> e.Append(`</trt:GetProfilesResponse>`)
</trt:GetServiceCapabilitiesResponse> return e.Bytes()
</s:Body>
</s:Envelope>`
} }
func SystemRebootResponse() string { func GetProfileResponse(name string) []byte {
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> e := NewEnvelope()
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> e.Append(`<trt:GetProfileResponse>
<s:Body> `)
<tds:SystemRebootResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> appendProfile(e, "Profile", name)
<tds:Message>system reboot in 1 second...</tds:Message> e.Append(`</trt:GetProfileResponse>`)
</tds:SystemRebootResponse> return e.Bytes()
</s:Body>
</s:Envelope>`
} }
func GetProfilesResponse(names []string) string { func appendProfile(e *Envelope, tag, name string) {
buf := bytes.NewBuffer(nil) e.Append(`<trt:`, tag, ` token="`, name, `" fixed="true">
buf.WriteString(`<?xml version="1.0" encoding="utf-8" standalone="yes"?> <tt:Name>`, name, `</tt:Name>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> <tt:VideoSourceConfiguration token="`, name, `">
<s:Body> <tt:Name>VSC</tt:Name>
<trt:GetProfilesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">`) <tt:SourceToken>`, name, `</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</tt:VideoSourceConfiguration>
<tt:VideoEncoderConfiguration token="vec">
<tt:Name>VEC</tt:Name>
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
</tt:VideoEncoderConfiguration>
</trt:`, tag, `>
`)
}
for i, name := range names { func GetVideoSourceConfigurationResponse(name string) []byte {
buf.WriteString(` e := NewEnvelope()
<trt:Profiles token="` + name + `" fixed="true"> e.Append(`<trt:GetVideoSourceConfigurationResponse>
<trt:Name>` + name + `</trt:Name> <trt:Configuration token="`, name, `">
<trt:VideoEncoderConfiguration token="` + strconv.Itoa(i) + `"> <tt:Name>VSC</tt:Name>
<trt:Name>` + name + `</trt:Name> <tt:SourceToken>`, name, `</tt:SourceToken>
<trt:Encoding>H264</trt:Encoding> <tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
<trt:Resolution> </trt:Configuration>
<trt:Width>1920</trt:Width> </trt:GetVideoSourceConfigurationResponse>`)
<trt:Height>1080</trt:Height> return e.Bytes()
</trt:Resolution> }
<trt:RateControl>
</trt:RateControl> func GetVideoSourcesResponse(names []string) []byte {
</trt:VideoEncoderConfiguration> e := NewEnvelope()
<trt:VideoSourceConfiguration token="` + strconv.Itoa(i) + `"> e.Append(`<trt:GetVideoSourcesResponse>
<trt:Name>` + name + `</trt:Name> `)
<trt:SourceToken>` + strconv.Itoa(i) + `</trt:SourceToken> for _, name := range names {
<trt:Bounds x="0" y="0" width="1920" height="1080"></trt:Bounds> e.Append(`<trt:VideoSources token="`, name, `">
</trt:VideoSourceConfiguration> <tt:Framerate>30.000000</tt:Framerate>
</trt:Profiles>`) <tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
</trt:VideoSources>
`)
}
e.Append(`</trt:GetVideoSourcesResponse>`)
return e.Bytes()
}
func GetStreamUriResponse(uri string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>`, uri, `</tt:Uri></trt:MediaUri></trt:GetStreamUriResponse>`)
return e.Bytes()
}
func GetSnapshotUriResponse(uri string) []byte {
e := NewEnvelope()
e.Append(`<trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>`, uri, `</tt:Uri></trt:MediaUri></trt:GetSnapshotUriResponse>`)
return e.Bytes()
}
func StaticResponse(operation string) []byte {
switch operation {
case DeviceGetSystemDateAndTime:
return GetSystemDateAndTimeResponse()
} }
buf.WriteString(` e := NewEnvelope()
</trt:GetProfilesResponse> e.Append(responses[operation])
</s:Body> b := e.Bytes()
</s:Envelope>`) if operation == DeviceGetNetworkInterfaces {
println()
return buf.String()
}
func GetVideoSourcesResponse(names []string) string {
buf := bytes.NewBuffer(nil)
buf.WriteString(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Body>
<trt:GetVideoSourcesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl">`)
for i, _ := range names {
buf.WriteString(`
<trt:VideoSources token="` + strconv.Itoa(i) + `">
<trt:Resolution>
<trt:Width>1920</trt:Width>
<trt:Height>1080</trt:Height>
</trt:Resolution>
</trt:VideoSources>`)
} }
return b
buf.WriteString(`
</trt:GetVideoSourcesResponse >
</s:Body>
</s:Envelope>`)
return buf.String()
} }
func GetStreamUriResponse(uri string) string { var responses = map[string]string{
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> DeviceGetDiscoveryMode: `<tds:GetDiscoveryModeResponse><tds:DiscoveryMode>Discoverable</tds:DiscoveryMode></tds:GetDiscoveryModeResponse>`,
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> DeviceGetDNS: `<tds:GetDNSResponse><tds:DNSInformation /></tds:GetDNSResponse>`,
<s:Body> DeviceGetHostname: `<tds:GetHostnameResponse><tds:HostnameInformation /></tds:GetHostnameResponse>`,
<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl"> DeviceGetNetworkDefaultGateway: `<tds:GetNetworkDefaultGatewayResponse><tds:NetworkGateway /></tds:GetNetworkDefaultGatewayResponse>`,
<trt:MediaUri> DeviceGetNTP: `<tds:GetNTPResponse><tds:NTPInformation /></tds:GetNTPResponse>`,
<trt:Uri>` + uri + `</trt:Uri> DeviceSystemReboot: `<tds:SystemRebootResponse><tds:Message>OK</tds:Message></tds:SystemRebootResponse>`,
</trt:MediaUri>
</trt:GetStreamUriResponse>
</s:Body>
</s:Envelope>`
}
func GetSnapshotUriResponse(uri string) string { DeviceGetNetworkInterfaces: `<tds:GetNetworkInterfacesResponse />`,
return `<?xml version="1.0" encoding="utf-8" standalone="yes"?> DeviceGetNetworkProtocols: `<tds:GetNetworkProtocolsResponse />`,
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> DeviceGetScopes: `<tds:GetScopesResponse>
<s:Body> <tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/name/go2rtc</tt:ScopeItem></tds:Scopes>
<trt:GetSnapshotUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl"> <tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/location/github</tt:ScopeItem></tds:Scopes>
<trt:MediaUri> <tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/Profile/Streaming</tt:ScopeItem></tds:Scopes>
<trt:Uri>` + uri + `</trt:Uri> <tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/type/Network_Video_Transmitter</tt:ScopeItem></tds:Scopes>
</trt:MediaUri> </tds:GetScopesResponse>`,
</trt:GetSnapshotUriResponse>
</s:Body>
</s:Envelope>`
} }