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 }