Fix ONVIF library integration and improve stream discovery
- Fix ONVIF CallMethod response parsing (returns *http.Response, not structs) - Add proper XML SOAP envelope parsing for GetProfiles and GetStreamUri - Use correct types from xsd/onvif package (StreamType, TransportProtocol, ReferenceToken) - Add strix binary to .gitignore - Update configuration and API routes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,10 @@ func (l *Loader) ListBrands() ([]string, error) {
|
||||
var brands []string
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && strings.HasSuffix(file.Name(), ".json") {
|
||||
// Skip index files
|
||||
if file.Name() == "index.json" || file.Name() == "indexa.json" {
|
||||
continue
|
||||
}
|
||||
brandID := strings.TrimSuffix(file.Name(), ".json")
|
||||
brands = append(brands, brandID)
|
||||
}
|
||||
@@ -157,6 +161,11 @@ func (l *Loader) StreamingSearch(searchFunc func(*models.Camera) bool) ([]*model
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip index.json as it contains brand list, not camera data
|
||||
if file.Name() == "index.json" || file.Name() == "indexa.json" {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(l.brandsPath, file.Name())
|
||||
camera, err := l.loadCameraFromFile(filePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,10 +2,16 @@ package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/IOTechSystems/onvif"
|
||||
"github.com/IOTechSystems/onvif/media"
|
||||
xsdonvif "github.com/IOTechSystems/onvif/xsd/onvif"
|
||||
"github.com/strix-project/strix/internal/models"
|
||||
)
|
||||
|
||||
@@ -28,12 +34,159 @@ func (o *ONVIFDiscovery) DiscoverStreamsForIP(ctx context.Context, ip, username,
|
||||
ip = ip[:idx]
|
||||
}
|
||||
|
||||
// Return common RTSP streams as we can't use complex ONVIF due to API changes
|
||||
streams := o.getCommonRTSPStreams(ip, username, password)
|
||||
var allStreams []models.DiscoveredStream
|
||||
|
||||
o.logger.Debug("generated common RTSP streams", "count", len(streams))
|
||||
// Try real ONVIF discovery first
|
||||
onvifStreams := o.discoverViaONVIF(ctx, ip, username, password)
|
||||
allStreams = append(allStreams, onvifStreams...)
|
||||
|
||||
return streams, nil
|
||||
// Add common RTSP streams
|
||||
commonStreams := o.getCommonRTSPStreams(ip, username, password)
|
||||
allStreams = append(allStreams, commonStreams...)
|
||||
|
||||
o.logger.Debug("collected streams", "onvif", len(onvifStreams), "common", len(commonStreams), "total", len(allStreams))
|
||||
|
||||
return allStreams, nil
|
||||
}
|
||||
|
||||
// discoverViaONVIF performs real ONVIF discovery
|
||||
func (o *ONVIFDiscovery) discoverViaONVIF(ctx context.Context, ip, username, password string) []models.DiscoveredStream {
|
||||
var streams []models.DiscoveredStream
|
||||
|
||||
// Try standard ONVIF ports
|
||||
ports := []int{80, 8080, 8000}
|
||||
|
||||
for _, port := range ports {
|
||||
// Create timeout context for ONVIF connection
|
||||
onvifCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
xaddr := fmt.Sprintf("%s:%d", ip, port)
|
||||
|
||||
o.logger.Debug("trying ONVIF connection", "xaddr", xaddr)
|
||||
|
||||
// Create ONVIF device
|
||||
dev, err := onvif.NewDevice(onvif.DeviceParams{
|
||||
Xaddr: xaddr,
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
o.logger.Debug("ONVIF device creation failed", "xaddr", xaddr, "error", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to get profiles with context
|
||||
profileStreams := o.getProfileStreams(onvifCtx, dev, ip)
|
||||
if len(profileStreams) > 0 {
|
||||
streams = append(streams, profileStreams...)
|
||||
o.logger.Debug("ONVIF discovery successful", "xaddr", xaddr, "profiles", len(profileStreams))
|
||||
break // Found working port, stop trying
|
||||
}
|
||||
}
|
||||
|
||||
return streams
|
||||
}
|
||||
|
||||
// getProfileStreams gets stream URIs from media profiles
|
||||
func (o *ONVIFDiscovery) getProfileStreams(ctx context.Context, dev *onvif.Device, ip string) []models.DiscoveredStream {
|
||||
var streams []models.DiscoveredStream
|
||||
|
||||
// Get media profiles
|
||||
getProfilesReq := media.GetProfiles{}
|
||||
profilesResp, err := dev.CallMethod(getProfilesReq)
|
||||
if err != nil {
|
||||
o.logger.Debug("failed to get ONVIF profiles", "error", err.Error())
|
||||
return streams
|
||||
}
|
||||
defer profilesResp.Body.Close()
|
||||
|
||||
// Read and parse XML response
|
||||
body, err := io.ReadAll(profilesResp.Body)
|
||||
if err != nil {
|
||||
o.logger.Debug("failed to read profiles response", "error", err.Error())
|
||||
return streams
|
||||
}
|
||||
|
||||
// Parse SOAP envelope
|
||||
var envelope struct {
|
||||
XMLName xml.Name `xml:"Envelope"`
|
||||
Body struct {
|
||||
GetProfilesResponse media.GetProfilesResponse `xml:"GetProfilesResponse"`
|
||||
} `xml:"Body"`
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal(body, &envelope); err != nil {
|
||||
o.logger.Debug("failed to parse profiles response", "error", err.Error())
|
||||
return streams
|
||||
}
|
||||
|
||||
// Get stream URI for each profile
|
||||
for _, profile := range envelope.Body.GetProfilesResponse.Profiles {
|
||||
streamURI := o.getStreamURI(dev, string(profile.Token))
|
||||
if streamURI != "" {
|
||||
streams = append(streams, models.DiscoveredStream{
|
||||
URL: streamURI,
|
||||
Type: "FFMPEG",
|
||||
Protocol: "rtsp",
|
||||
Port: 554,
|
||||
Working: false, // Will be tested later
|
||||
Metadata: map[string]interface{}{
|
||||
"source": "onvif",
|
||||
"profile_token": string(profile.Token),
|
||||
"profile_name": string(profile.Name),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return streams
|
||||
}
|
||||
|
||||
// getStreamURI retrieves stream URI for a profile
|
||||
func (o *ONVIFDiscovery) getStreamURI(dev *onvif.Device, profileToken string) string {
|
||||
stream := xsdonvif.StreamType("RTP-Unicast")
|
||||
protocol := xsdonvif.TransportProtocol("RTSP")
|
||||
token := xsdonvif.ReferenceToken(profileToken)
|
||||
|
||||
getStreamURIReq := media.GetStreamUri{
|
||||
ProfileToken: &token,
|
||||
StreamSetup: &xsdonvif.StreamSetup{
|
||||
Stream: &stream,
|
||||
Transport: &xsdonvif.Transport{
|
||||
Protocol: &protocol,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := dev.CallMethod(getStreamURIReq)
|
||||
if err != nil {
|
||||
o.logger.Debug("failed to get stream URI", "profile", profileToken, "error", err.Error())
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read and parse XML response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
o.logger.Debug("failed to read stream URI response", "error", err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
// Parse SOAP envelope
|
||||
var envelope struct {
|
||||
XMLName xml.Name `xml:"Envelope"`
|
||||
Body struct {
|
||||
GetStreamUriResponse media.GetStreamUriResponse `xml:"GetStreamUriResponse"`
|
||||
} `xml:"Body"`
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal(body, &envelope); err != nil {
|
||||
o.logger.Debug("failed to parse stream URI response", "error", err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(envelope.Body.GetStreamUriResponse.MediaUri.Uri)
|
||||
}
|
||||
|
||||
// getCommonRTSPStreams returns common RTSP stream URLs
|
||||
|
||||
@@ -23,7 +23,7 @@ type Tester struct {
|
||||
func NewTester(ffprobeTimeout time.Duration, logger interface{ Debug(string, ...any); Error(string, error, ...any) }) *Tester {
|
||||
return &Tester{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
ffprobeTimeout: ffprobeTimeout,
|
||||
logger: logger,
|
||||
@@ -265,6 +265,11 @@ func (t *Tester) testHTTP(ctx context.Context, streamURL, username, password str
|
||||
// Try to probe with ffprobe for more details
|
||||
t.probeHTTPVideo(ctx, streamURL, username, password, result)
|
||||
|
||||
case strings.Contains(contentType, "text/html"), strings.Contains(contentType, "text/plain"):
|
||||
// Ignore web interfaces and plain text responses
|
||||
result.Working = false
|
||||
result.Error = "web interface, not a video stream"
|
||||
|
||||
default:
|
||||
result.Type = "HTTP_UNKNOWN"
|
||||
result.Working = true // Assume it works if we got 200 OK
|
||||
|
||||
Reference in New Issue
Block a user