Files
cctv/server/ingest/discovery.go
2026-04-22 23:35:59 +01:00

111 lines
3.0 KiB
Go

package ingest
import (
"context"
"fmt"
"log/slog"
"net/url"
"github.com/gowvp/onvif"
"github.com/gowvp/onvif/media"
mediaSdk "github.com/gowvp/onvif/sdk/media"
onvifXsd "github.com/gowvp/onvif/xsd/onvif"
)
type OnvifProfile struct {
Name onvifXsd.Name
Token onvifXsd.ReferenceToken
URI *url.URL
}
// GetOnvifProfiles retrieves all available ONVIF stream profiles for a camera.
//
// Note that the result is very limited since many cameras provide extremely incomplete data,
// so it's safest to assume that we only have the bare minimum.
func GetOnvifProfiles(ctx context.Context, host, username, password string) ([]OnvifProfile, error) {
dev, err := onvif.NewDevice(onvif.DeviceParams{
Xaddr: host,
Username: username,
Password: password,
})
if err != nil {
return nil, fmt.Errorf("failed to create ONVIF device: %w", err)
}
// Get available profiles
profiles, err := mediaSdk.Call_GetProfiles(ctx, dev, media.GetProfiles{})
if err != nil {
return nil, fmt.Errorf("failed to get ONVIF profiles: %w", err)
}
// Get available URIs for each profile
onvifProfiles := make([]OnvifProfile, 0, len(profiles.Profiles))
for _, p := range profiles.Profiles {
uri, err := mediaSdk.Call_GetStreamUri(ctx, dev, media.GetStreamUri{
StreamSetup: onvifXsd.StreamSetup{
Stream: onvifXsd.StreamType("RTP-Unicast"),
Transport: onvifXsd.Transport{
Protocol: onvifXsd.TransportProtocol("TCP"),
},
},
ProfileToken: p.Token,
})
if err != nil {
return nil, fmt.Errorf("failed to get stream URI for profile %s: %w", p.Token, err)
}
if uri.MediaUri.Uri != "" {
u, err := url.Parse(string(uri.MediaUri.Uri))
if err != nil {
return nil, fmt.Errorf("failed to parse stream URI for profile %s: %w", p.Token, err)
}
onvifProfiles = append(onvifProfiles, OnvifProfile{
Name: p.Name,
Token: p.Token,
URI: u,
})
}
}
return onvifProfiles, nil
}
type StreamStats struct {
FPS float64
Width int
Height int
}
// GetStreamStats starts a temporary pipeline to retrieve some basic (pre-decoding) information about the RTSP stream.
func GetStreamStats(ctx context.Context, log *slog.Logger, rtspUrl *url.URL, profileToken string) (stats StreamStats, err error) {
p, err := CreatePipeline(log)
if err != nil {
return stats, fmt.Errorf("failed to create pipeline: %w", err)
}
if err := p.addRtspSource(rtspUrl); err != nil {
return stats, fmt.Errorf("failed to add RTSP source: %w", err)
}
statsChan, err := p.addStatsBranch()
if err != nil {
return stats, fmt.Errorf("failed to add stats branch: %w", err)
}
statsFound := false
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
select {
case stats = <-statsChan:
statsFound = true
cancel()
case <-runCtx.Done():
}
}()
if err := p.Run(runCtx); err != nil {
return stats, fmt.Errorf("failed to run pipeline: %w", err)
}
if !statsFound {
return stats, fmt.Errorf("failed to retrieve stream stats: no stats received")
}
return stats, nil
}