111 lines
3.0 KiB
Go
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
|
|
}
|