install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,42 @@
# ONVIF
## ONVIF Client
[`new in v1.5.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
**WebUI > Add** webpage supports ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use Docker, you must use "network host".
```yaml
streams:
dahua1: onvif://admin:password@192.168.1.123
reolink1: onvif://admin:password@192.168.1.123:8000
tapo1: onvif://admin:password@192.168.1.123:2020
```
## ONVIF Server
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. Supports 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
@@ -0,0 +1,238 @@
package onvif
import (
"io"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/rtsp"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/onvif"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("onvif")
streams.HandleFunc("onvif", streamOnvif)
// ONVIF server on all suburls
api.HandleFunc("/onvif/", onvifDeviceService)
// ONVIF client autodiscovery
api.HandleFunc("api/onvif", apiOnvif)
}
var log zerolog.Logger
func streamOnvif(rawURL string) (core.Producer, error) {
client, err := onvif.NewClient(rawURL)
if err != nil {
return nil, err
}
uri, err := client.GetURI()
if err != nil {
return nil, err
}
// Append hash-based arguments to the retrieved URI
if i := strings.IndexByte(rawURL, '#'); i > 0 {
uri += rawURL[i:]
}
log.Debug().Msgf("[onvif] new uri=%s", uri)
if err = streams.Validate(uri); err != nil {
return nil, err
}
return streams.GetProducer(uri)
}
func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
operation := onvif.GetRequestAction(b)
if operation == "" {
http.Error(w, "malformed request body", http.StatusBadRequest)
return
}
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
switch operation {
case onvif.ServiceGetServiceCapabilities, // important for Hass
onvif.DeviceGetNetworkInterfaces, // important for Hass
onvif.DeviceGetSystemDateAndTime, // important for Hass
onvif.DeviceSetSystemDateAndTime, // return just OK
onvif.DeviceGetDiscoveryMode,
onvif.DeviceGetDNS,
onvif.DeviceGetHostname,
onvif.DeviceGetNetworkDefaultGateway,
onvif.DeviceGetNetworkProtocols,
onvif.DeviceGetNTP,
onvif.DeviceGetScopes,
onvif.MediaGetVideoEncoderConfiguration,
onvif.MediaGetVideoEncoderConfigurations,
onvif.MediaGetAudioEncoderConfigurations,
onvif.MediaGetVideoEncoderConfigurationOptions,
onvif.MediaGetAudioSources,
onvif.MediaGetAudioSourceConfigurations:
b = onvif.StaticResponse(operation)
case onvif.DeviceGetCapabilities:
// important for Hass: Media section
b = onvif.GetCapabilitiesResponse(r.Host)
case onvif.DeviceGetServices:
b = onvif.GetServicesResponse(r.Host)
case onvif.DeviceGetDeviceInformation:
// important for Hass: SerialNumber (unique server ID)
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
case onvif.DeviceSystemReboot:
b = onvif.StaticResponse(operation)
time.AfterFunc(time.Second, func() {
os.Exit(0)
})
case onvif.MediaGetVideoSources:
b = onvif.GetVideoSourcesResponse(streams.GetAllNames())
case onvif.MediaGetProfiles:
// important for Hass: H264 codec, width, height
b = onvif.GetProfilesResponse(streams.GetAllNames())
case onvif.MediaGetProfile:
token := onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetProfileResponse(token)
case onvif.MediaGetVideoSourceConfigurations:
// important for Happytime Onvif Client
b = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames())
case onvif.MediaGetVideoSourceConfiguration:
token := onvif.FindTagValue(b, "ConfigurationToken")
b = onvif.GetVideoSourceConfigurationResponse(token)
case onvif.MediaGetStreamUri:
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host // in case of Host without port
}
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetStreamUriResponse(uri)
case onvif.MediaGetSnapshotUri:
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
b = onvif.GetSnapshotUriResponse(uri)
default:
http.Error(w, "unsupported operation", http.StatusBadRequest)
log.Warn().Msgf("[onvif] unsupported operation: %s", operation)
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
return
}
log.Trace().Msgf("[onvif] server response:\n%s", b)
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
if _, err = w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func apiOnvif(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
var items []*api.Source
if src == "" {
devices, err := onvif.DiscoveryStreamingDevices()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, device := range devices {
u, err := url.Parse(device.URL)
if err != nil {
log.Warn().Str("url", device.URL).Msg("[onvif] broken")
continue
}
if u.Scheme != "http" {
log.Warn().Str("url", device.URL).Msg("[onvif] unsupported")
continue
}
u.Scheme = "onvif"
u.User = url.UserPassword("user", "pass")
if u.Path == onvif.PathDevice {
u.Path = ""
}
items = append(items, &api.Source{
Name: u.Host,
URL: u.String(),
Info: device.Name + " " + device.Hardware,
})
}
} else {
client, err := onvif.NewClient(src)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if l := log.Trace(); l.Enabled() {
b, _ := client.MediaRequest(onvif.MediaGetProfiles)
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
}
name, err := client.GetName()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tokens, err := client.GetProfilesTokens()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for i, token := range tokens {
items = append(items, &api.Source{
Name: name + " stream" + strconv.Itoa(i),
URL: src + "?subtype=" + token,
})
}
if len(tokens) > 0 && client.HasSnapshots() {
items = append(items, &api.Source{
Name: name + " snapshot",
URL: src + "?subtype=" + tokens[0] + "&snapshot",
})
}
}
api.ResponseSources(w, items)
}