package onvif import ( "io" "net" "net/http" "net/url" "os" "strconv" "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 } log.Debug().Msgf("[onvif] new uri=%s", uri) 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.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) 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.ServiceGetServiceCapabilities: // important for Hass // TODO: check path links to media b = onvif.GetMediaServiceCapabilitiesResponse() case onvif.DeviceSystemReboot: b = onvif.StaticResponse(operation) time.AfterFunc(time.Second, func() { os.Exit(0) }) case onvif.MediaGetVideoSources: b = onvif.GetVideoSourcesResponse(streams.GetAll()) case onvif.MediaGetProfiles: // important for Hass: H264 codec, width, height b = onvif.GetProfilesResponse(streams.GetAll()) case onvif.MediaGetProfile: token := onvif.FindTagValue(b, "ProfileToken") b = onvif.GetProfileResponse(token) case onvif.MediaGetVideoSourceConfiguration: token := onvif.FindTagValue(b, "ConfigurationToken") b = onvif.GetVideoSourceConfigurationResponse(token) case onvif.MediaGetStreamUri: host, _, err := net.SplitHostPort(r.Host) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } 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.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 == "" { urls, err := onvif.DiscoveryStreamingURLs() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } for _, rawURL := range urls { u, err := url.Parse(rawURL) if err != nil { 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.Source{Name: u.Host, URL: u.String()}) } } 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) }