feat: add comprehensive ONVIF test reports and enhance documentation
- Introduced CAMERA_TEST_REPORT.md and COMPREHENSIVE_TEST_SUMMARY.md to document testing results for the Bosch FLEXIDOME indoor 5100i IR camera. - Added detailed analysis of ONVIF Media Service operations and implementation status in MEDIA_OPERATIONS_ANALYSIS.md and MEDIA_WSDL_OPERATIONS_ANALYSIS.md. - Updated implementation status documentation to reflect the completion of all 79 operations in the ONVIF Media Service. - Enhanced existing comments and documentation across various files for better clarity and consistency.
This commit is contained in:
+23
-23
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// Device service SOAP message types
|
||||
|
||||
// GetDeviceInformationResponse represents GetDeviceInformation response
|
||||
// GetDeviceInformationResponse represents GetDeviceInformation response.
|
||||
type GetDeviceInformationResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetDeviceInformationResponse"`
|
||||
Manufacturer string `xml:"Manufacturer"`
|
||||
@@ -20,13 +20,13 @@ type GetDeviceInformationResponse struct {
|
||||
HardwareId string `xml:"HardwareId"`
|
||||
}
|
||||
|
||||
// GetCapabilitiesResponse represents GetCapabilities response
|
||||
// GetCapabilitiesResponse represents GetCapabilities response.
|
||||
type GetCapabilitiesResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetCapabilitiesResponse"`
|
||||
Capabilities *Capabilities `xml:"Capabilities"`
|
||||
}
|
||||
|
||||
// Capabilities represents device capabilities
|
||||
// Capabilities represents device capabilities.
|
||||
type Capabilities struct {
|
||||
Analytics *AnalyticsCapabilities `xml:"Analytics,omitempty"`
|
||||
Device *DeviceCapabilities `xml:"Device"`
|
||||
@@ -36,14 +36,14 @@ type Capabilities struct {
|
||||
PTZ *PTZCapabilities `xml:"PTZ,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyticsCapabilities represents analytics service capabilities
|
||||
// AnalyticsCapabilities represents analytics service capabilities.
|
||||
type AnalyticsCapabilities struct {
|
||||
XAddr string `xml:"XAddr"`
|
||||
RuleSupport bool `xml:"RuleSupport,attr"`
|
||||
AnalyticsModuleSupport bool `xml:"AnalyticsModuleSupport,attr"`
|
||||
}
|
||||
|
||||
// DeviceCapabilities represents device service capabilities
|
||||
// DeviceCapabilities represents device service capabilities.
|
||||
type DeviceCapabilities struct {
|
||||
XAddr string `xml:"XAddr"`
|
||||
Network *NetworkCapabilities `xml:"Network,omitempty"`
|
||||
@@ -52,7 +52,7 @@ type DeviceCapabilities struct {
|
||||
Security *SecurityCapabilities `xml:"Security,omitempty"`
|
||||
}
|
||||
|
||||
// NetworkCapabilities represents network capabilities
|
||||
// NetworkCapabilities represents network capabilities.
|
||||
type NetworkCapabilities struct {
|
||||
IPFilter bool `xml:"IPFilter,attr"`
|
||||
ZeroConfiguration bool `xml:"ZeroConfiguration,attr"`
|
||||
@@ -60,7 +60,7 @@ type NetworkCapabilities struct {
|
||||
DynDNS bool `xml:"DynDNS,attr"`
|
||||
}
|
||||
|
||||
// SystemCapabilities represents system capabilities
|
||||
// SystemCapabilities represents system capabilities.
|
||||
type SystemCapabilities struct {
|
||||
DiscoveryResolve bool `xml:"DiscoveryResolve,attr"`
|
||||
DiscoveryBye bool `xml:"DiscoveryBye,attr"`
|
||||
@@ -70,13 +70,13 @@ type SystemCapabilities struct {
|
||||
FirmwareUpgrade bool `xml:"FirmwareUpgrade,attr"`
|
||||
}
|
||||
|
||||
// IOCapabilities represents I/O capabilities
|
||||
// IOCapabilities represents I/O capabilities.
|
||||
type IOCapabilities struct {
|
||||
InputConnectors int `xml:"InputConnectors,attr"`
|
||||
RelayOutputs int `xml:"RelayOutputs,attr"`
|
||||
}
|
||||
|
||||
// SecurityCapabilities represents security capabilities
|
||||
// SecurityCapabilities represents security capabilities.
|
||||
type SecurityCapabilities struct {
|
||||
TLS11 bool `xml:"TLS1.1,attr"`
|
||||
TLS12 bool `xml:"TLS1.2,attr"`
|
||||
@@ -88,7 +88,7 @@ type SecurityCapabilities struct {
|
||||
RELToken bool `xml:"RELToken,attr"`
|
||||
}
|
||||
|
||||
// EventCapabilities represents event service capabilities
|
||||
// EventCapabilities represents event service capabilities.
|
||||
type EventCapabilities struct {
|
||||
XAddr string `xml:"XAddr"`
|
||||
WSSubscriptionPolicySupport bool `xml:"WSSubscriptionPolicySupport,attr"`
|
||||
@@ -96,49 +96,49 @@ type EventCapabilities struct {
|
||||
WSPausableSubscriptionSupport bool `xml:"WSPausableSubscriptionManagerInterfaceSupport,attr"`
|
||||
}
|
||||
|
||||
// ImagingCapabilities represents imaging service capabilities
|
||||
// ImagingCapabilities represents imaging service capabilities.
|
||||
type ImagingCapabilities struct {
|
||||
XAddr string `xml:"XAddr"`
|
||||
}
|
||||
|
||||
// MediaCapabilities represents media service capabilities
|
||||
// MediaCapabilities represents media service capabilities.
|
||||
type MediaCapabilities struct {
|
||||
XAddr string `xml:"XAddr"`
|
||||
StreamingCapabilities *StreamingCapabilities `xml:"StreamingCapabilities"`
|
||||
}
|
||||
|
||||
// StreamingCapabilities represents streaming capabilities
|
||||
// StreamingCapabilities represents streaming capabilities.
|
||||
type StreamingCapabilities struct {
|
||||
RTPMulticast bool `xml:"RTPMulticast,attr"`
|
||||
RTP_TCP bool `xml:"RTP_TCP,attr"`
|
||||
RTP_RTSP_TCP bool `xml:"RTP_RTSP_TCP,attr"`
|
||||
}
|
||||
|
||||
// PTZCapabilities represents PTZ service capabilities
|
||||
// PTZCapabilities represents PTZ service capabilities.
|
||||
type PTZCapabilities struct {
|
||||
XAddr string `xml:"XAddr"`
|
||||
}
|
||||
|
||||
// GetServicesResponse represents GetServices response
|
||||
// GetServicesResponse represents GetServices response.
|
||||
type GetServicesResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetServicesResponse"`
|
||||
Service []Service `xml:"Service"`
|
||||
}
|
||||
|
||||
// Service represents a service
|
||||
// Service represents a service.
|
||||
type Service struct {
|
||||
Namespace string `xml:"Namespace"`
|
||||
XAddr string `xml:"XAddr"`
|
||||
Version Version `xml:"Version"`
|
||||
}
|
||||
|
||||
// Version represents service version
|
||||
// Version represents service version.
|
||||
type Version struct {
|
||||
Major int `xml:"Major"`
|
||||
Minor int `xml:"Minor"`
|
||||
}
|
||||
|
||||
// SystemRebootResponse represents SystemReboot response
|
||||
// SystemRebootResponse represents SystemReboot response.
|
||||
type SystemRebootResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl SystemRebootResponse"`
|
||||
Message string `xml:"Message"`
|
||||
@@ -146,7 +146,7 @@ type SystemRebootResponse struct {
|
||||
|
||||
// Device service handlers
|
||||
|
||||
// HandleGetDeviceInformation handles GetDeviceInformation request
|
||||
// HandleGetDeviceInformation handles GetDeviceInformation request.
|
||||
func (s *Server) HandleGetDeviceInformation(body interface{}) (interface{}, error) {
|
||||
return &GetDeviceInformationResponse{
|
||||
Manufacturer: s.config.DeviceInfo.Manufacturer,
|
||||
@@ -157,7 +157,7 @@ func (s *Server) HandleGetDeviceInformation(body interface{}) (interface{}, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGetCapabilities handles GetCapabilities request
|
||||
// HandleGetCapabilities handles GetCapabilities request.
|
||||
func (s *Server) HandleGetCapabilities(body interface{}) (interface{}, error) {
|
||||
// Get the host from the request (in a real implementation)
|
||||
// For now, use a placeholder
|
||||
@@ -236,7 +236,7 @@ func (s *Server) HandleGetCapabilities(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGetSystemDateAndTime handles GetSystemDateAndTime request
|
||||
// HandleGetSystemDateAndTime handles GetSystemDateAndTime request.
|
||||
func (s *Server) HandleGetSystemDateAndTime(body interface{}) (interface{}, error) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
@@ -253,7 +253,7 @@ func (s *Server) HandleGetSystemDateAndTime(body interface{}) (interface{}, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGetServices handles GetServices request
|
||||
// HandleGetServices handles GetServices request.
|
||||
func (s *Server) HandleGetServices(body interface{}) (interface{}, error) {
|
||||
host := s.config.Host
|
||||
if host == "0.0.0.0" || host == "" {
|
||||
@@ -296,7 +296,7 @@ func (s *Server) HandleGetServices(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleSystemReboot handles SystemReboot request
|
||||
// HandleSystemReboot handles SystemReboot request.
|
||||
func (s *Server) HandleSystemReboot(body interface{}) (interface{}, error) {
|
||||
return &SystemRebootResponse{
|
||||
Message: "Device rebooting",
|
||||
|
||||
@@ -54,6 +54,7 @@ func TestHandleGetCapabilities(t *testing.T) {
|
||||
|
||||
if capsResp.Capabilities == nil {
|
||||
t.Error("Capabilities is nil")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,6 +91,7 @@ func TestHandleGetSystemDateAndTime(t *testing.T) {
|
||||
// Response should be a map or interface
|
||||
if resp == nil {
|
||||
t.Error("Response is nil")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -110,6 +112,7 @@ func TestHandleGetServices(t *testing.T) {
|
||||
|
||||
if len(servicesResp.Service) == 0 {
|
||||
t.Error("No services returned")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -265,6 +268,7 @@ func TestHandleSnapshot(t *testing.T) {
|
||||
profiles := server.ListProfiles()
|
||||
if len(profiles) == 0 {
|
||||
t.Error("No profiles available for snapshot")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -289,6 +293,7 @@ func TestHandleGetCapabilitiesDetails(t *testing.T) {
|
||||
|
||||
if capsResp.Capabilities == nil {
|
||||
t.Error("Capabilities is nil")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -327,8 +332,9 @@ func TestHandleGetServicesDetails(t *testing.T) {
|
||||
t.Fatalf("Response is not GetServicesResponse: %T", resp)
|
||||
}
|
||||
|
||||
if servResp.Service == nil || len(servResp.Service) == 0 {
|
||||
if len(servResp.Service) == 0 {
|
||||
t.Error("No services returned")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -337,7 +343,7 @@ func TestHandleGetServicesDetails(t *testing.T) {
|
||||
if svc.Namespace == "" {
|
||||
t.Error("Service Namespace is empty")
|
||||
}
|
||||
if len(svc.XAddr) == 0 {
|
||||
if svc.XAddr == "" {
|
||||
t.Error("Service XAddr is empty")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package server
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrVideoSourceNotFound is returned when a video source is not found.
|
||||
ErrVideoSourceNotFound = errors.New("video source not found")
|
||||
|
||||
// ErrProfileNotFound is returned when a profile is not found.
|
||||
ErrProfileNotFound = errors.New("profile not found")
|
||||
|
||||
// ErrSnapshotNotSupported is returned when snapshot is not supported for a profile.
|
||||
ErrSnapshotNotSupported = errors.New("snapshot not supported for profile")
|
||||
|
||||
// ErrPTZNotSupported is returned when PTZ is not supported for a profile.
|
||||
ErrPTZNotSupported = errors.New("PTZ not supported for profile")
|
||||
|
||||
// ErrPresetNotFound is returned when a preset is not found.
|
||||
ErrPresetNotFound = errors.New("preset not found")
|
||||
)
|
||||
+32
-32
@@ -8,19 +8,19 @@ import (
|
||||
|
||||
// Imaging service SOAP message types
|
||||
|
||||
// GetImagingSettingsRequest represents GetImagingSettings request
|
||||
// GetImagingSettingsRequest represents GetImagingSettings request.
|
||||
type GetImagingSettingsRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl GetImagingSettings"`
|
||||
VideoSourceToken string `xml:"VideoSourceToken"`
|
||||
}
|
||||
|
||||
// GetImagingSettingsResponse represents GetImagingSettings response
|
||||
// GetImagingSettingsResponse represents GetImagingSettings response.
|
||||
type GetImagingSettingsResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl GetImagingSettingsResponse"`
|
||||
ImagingSettings *ImagingSettings `xml:"ImagingSettings"`
|
||||
}
|
||||
|
||||
// ImagingSettings represents imaging settings
|
||||
// ImagingSettings represents imaging settings.
|
||||
type ImagingSettings struct {
|
||||
BacklightCompensation *BacklightCompensationSettings `xml:"BacklightCompensation,omitempty"`
|
||||
Brightness *float64 `xml:"Brightness,omitempty"`
|
||||
@@ -34,13 +34,13 @@ type ImagingSettings struct {
|
||||
WhiteBalance *WhiteBalanceSettings20 `xml:"WhiteBalance,omitempty"`
|
||||
}
|
||||
|
||||
// BacklightCompensationSettings represents backlight compensation settings
|
||||
// BacklightCompensationSettings represents backlight compensation settings.
|
||||
type BacklightCompensationSettings struct {
|
||||
Mode string `xml:"Mode"`
|
||||
Level *float64 `xml:"Level,omitempty"`
|
||||
}
|
||||
|
||||
// ExposureSettings20 represents exposure settings for ONVIF 2.0
|
||||
// ExposureSettings20 represents exposure settings for ONVIF 2.0.
|
||||
type ExposureSettings20 struct {
|
||||
Mode string `xml:"Mode"`
|
||||
Priority *string `xml:"Priority,omitempty"`
|
||||
@@ -56,7 +56,7 @@ type ExposureSettings20 struct {
|
||||
Iris *float64 `xml:"Iris,omitempty"`
|
||||
}
|
||||
|
||||
// FocusConfiguration20 represents focus configuration for ONVIF 2.0
|
||||
// FocusConfiguration20 represents focus configuration for ONVIF 2.0.
|
||||
type FocusConfiguration20 struct {
|
||||
AutoFocusMode string `xml:"AutoFocusMode"`
|
||||
DefaultSpeed *float64 `xml:"DefaultSpeed,omitempty"`
|
||||
@@ -64,20 +64,20 @@ type FocusConfiguration20 struct {
|
||||
FarLimit *float64 `xml:"FarLimit,omitempty"`
|
||||
}
|
||||
|
||||
// WideDynamicRangeSettings represents WDR settings
|
||||
// WideDynamicRangeSettings represents WDR settings.
|
||||
type WideDynamicRangeSettings struct {
|
||||
Mode string `xml:"Mode"`
|
||||
Level *float64 `xml:"Level,omitempty"`
|
||||
}
|
||||
|
||||
// WhiteBalanceSettings20 represents white balance settings for ONVIF 2.0
|
||||
// WhiteBalanceSettings20 represents white balance settings for ONVIF 2.0.
|
||||
type WhiteBalanceSettings20 struct {
|
||||
Mode string `xml:"Mode"`
|
||||
CrGain *float64 `xml:"CrGain,omitempty"`
|
||||
CbGain *float64 `xml:"CbGain,omitempty"`
|
||||
}
|
||||
|
||||
// Rectangle represents a rectangle
|
||||
// Rectangle represents a rectangle.
|
||||
type Rectangle struct {
|
||||
Bottom float64 `xml:"bottom,attr"`
|
||||
Top float64 `xml:"top,attr"`
|
||||
@@ -85,7 +85,7 @@ type Rectangle struct {
|
||||
Left float64 `xml:"left,attr"`
|
||||
}
|
||||
|
||||
// SetImagingSettingsRequest represents SetImagingSettings request
|
||||
// SetImagingSettingsRequest represents SetImagingSettings request.
|
||||
type SetImagingSettingsRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl SetImagingSettings"`
|
||||
VideoSourceToken string `xml:"VideoSourceToken"`
|
||||
@@ -93,24 +93,24 @@ type SetImagingSettingsRequest struct {
|
||||
ForcePersistence bool `xml:"ForcePersistence,omitempty"`
|
||||
}
|
||||
|
||||
// SetImagingSettingsResponse represents SetImagingSettings response
|
||||
// SetImagingSettingsResponse represents SetImagingSettings response.
|
||||
type SetImagingSettingsResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl SetImagingSettingsResponse"`
|
||||
}
|
||||
|
||||
// GetOptionsRequest represents GetOptions request
|
||||
// GetOptionsRequest represents GetOptions request.
|
||||
type GetOptionsRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl GetOptions"`
|
||||
VideoSourceToken string `xml:"VideoSourceToken"`
|
||||
}
|
||||
|
||||
// GetOptionsResponse represents GetOptions response
|
||||
// GetOptionsResponse represents GetOptions response.
|
||||
type GetOptionsResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl GetOptionsResponse"`
|
||||
ImagingOptions *ImagingOptions `xml:"ImagingOptions"`
|
||||
}
|
||||
|
||||
// ImagingOptions represents imaging options/capabilities
|
||||
// ImagingOptions represents imaging options/capabilities.
|
||||
type ImagingOptions struct {
|
||||
BacklightCompensation *BacklightCompensationOptions `xml:"BacklightCompensation,omitempty"`
|
||||
Brightness *FloatRange `xml:"Brightness,omitempty"`
|
||||
@@ -124,13 +124,13 @@ type ImagingOptions struct {
|
||||
WhiteBalance *WhiteBalanceOptions `xml:"WhiteBalance,omitempty"`
|
||||
}
|
||||
|
||||
// BacklightCompensationOptions represents backlight compensation options
|
||||
// BacklightCompensationOptions represents backlight compensation options.
|
||||
type BacklightCompensationOptions struct {
|
||||
Mode []string `xml:"Mode"`
|
||||
Level *FloatRange `xml:"Level,omitempty"`
|
||||
}
|
||||
|
||||
// ExposureOptions represents exposure options
|
||||
// ExposureOptions represents exposure options.
|
||||
type ExposureOptions struct {
|
||||
Mode []string `xml:"Mode"`
|
||||
Priority []string `xml:"Priority,omitempty"`
|
||||
@@ -145,7 +145,7 @@ type ExposureOptions struct {
|
||||
Iris *FloatRange `xml:"Iris,omitempty"`
|
||||
}
|
||||
|
||||
// FocusOptions represents focus options
|
||||
// FocusOptions represents focus options.
|
||||
type FocusOptions struct {
|
||||
AutoFocusModes []string `xml:"AutoFocusModes"`
|
||||
DefaultSpeed *FloatRange `xml:"DefaultSpeed,omitempty"`
|
||||
@@ -153,51 +153,51 @@ type FocusOptions struct {
|
||||
FarLimit *FloatRange `xml:"FarLimit,omitempty"`
|
||||
}
|
||||
|
||||
// WideDynamicRangeOptions represents WDR options
|
||||
// WideDynamicRangeOptions represents WDR options.
|
||||
type WideDynamicRangeOptions struct {
|
||||
Mode []string `xml:"Mode"`
|
||||
Level *FloatRange `xml:"Level,omitempty"`
|
||||
}
|
||||
|
||||
// WhiteBalanceOptions represents white balance options
|
||||
// WhiteBalanceOptions represents white balance options.
|
||||
type WhiteBalanceOptions struct {
|
||||
Mode []string `xml:"Mode"`
|
||||
YrGain *FloatRange `xml:"YrGain,omitempty"`
|
||||
YbGain *FloatRange `xml:"YbGain,omitempty"`
|
||||
}
|
||||
|
||||
// MoveRequest represents Move (focus) request
|
||||
// MoveRequest represents Move (focus) request.
|
||||
type MoveRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl Move"`
|
||||
VideoSourceToken string `xml:"VideoSourceToken"`
|
||||
Focus *FocusMove `xml:"Focus"`
|
||||
}
|
||||
|
||||
// FocusMove represents focus move parameters
|
||||
// FocusMove represents focus move parameters.
|
||||
type FocusMove struct {
|
||||
Absolute *AbsoluteFocus `xml:"Absolute,omitempty"`
|
||||
Relative *RelativeFocus `xml:"Relative,omitempty"`
|
||||
Continuous *ContinuousFocus `xml:"Continuous,omitempty"`
|
||||
}
|
||||
|
||||
// AbsoluteFocus represents absolute focus
|
||||
// AbsoluteFocus represents absolute focus.
|
||||
type AbsoluteFocus struct {
|
||||
Position float64 `xml:"Position"`
|
||||
Speed *float64 `xml:"Speed,omitempty"`
|
||||
}
|
||||
|
||||
// RelativeFocus represents relative focus
|
||||
// RelativeFocus represents relative focus.
|
||||
type RelativeFocus struct {
|
||||
Distance float64 `xml:"Distance"`
|
||||
Speed *float64 `xml:"Speed,omitempty"`
|
||||
}
|
||||
|
||||
// ContinuousFocus represents continuous focus
|
||||
// ContinuousFocus represents continuous focus.
|
||||
type ContinuousFocus struct {
|
||||
Speed float64 `xml:"Speed"`
|
||||
}
|
||||
|
||||
// MoveResponse represents Move response
|
||||
// MoveResponse represents Move response.
|
||||
type MoveResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl MoveResponse"`
|
||||
}
|
||||
@@ -206,7 +206,7 @@ type MoveResponse struct {
|
||||
|
||||
var imagingMutex sync.RWMutex
|
||||
|
||||
// HandleGetImagingSettings handles GetImagingSettings request
|
||||
// HandleGetImagingSettings handles GetImagingSettings request.
|
||||
func (s *Server) HandleGetImagingSettings(body interface{}) (interface{}, error) {
|
||||
var req GetImagingSettingsRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -219,7 +219,7 @@ func (s *Server) HandleGetImagingSettings(body interface{}) (interface{}, error)
|
||||
|
||||
state, ok := s.imagingState[req.VideoSourceToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("video source not found: %s", req.VideoSourceToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrVideoSourceNotFound, req.VideoSourceToken)
|
||||
}
|
||||
|
||||
// Build imaging settings response
|
||||
@@ -265,7 +265,7 @@ func (s *Server) HandleGetImagingSettings(body interface{}) (interface{}, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleSetImagingSettings handles SetImagingSettings request
|
||||
// HandleSetImagingSettings handles SetImagingSettings request.
|
||||
func (s *Server) HandleSetImagingSettings(body interface{}) (interface{}, error) {
|
||||
var req SetImagingSettingsRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -278,7 +278,7 @@ func (s *Server) HandleSetImagingSettings(body interface{}) (interface{}, error)
|
||||
|
||||
state, ok := s.imagingState[req.VideoSourceToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("video source not found: %s", req.VideoSourceToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrVideoSourceNotFound, req.VideoSourceToken)
|
||||
}
|
||||
|
||||
// Update settings
|
||||
@@ -342,7 +342,7 @@ func (s *Server) HandleSetImagingSettings(body interface{}) (interface{}, error)
|
||||
return &SetImagingSettingsResponse{}, nil
|
||||
}
|
||||
|
||||
// HandleGetOptions handles GetOptions request
|
||||
// HandleGetOptions handles GetOptions request.
|
||||
func (s *Server) HandleGetOptions(body interface{}) (interface{}, error) {
|
||||
// Return available imaging options/capabilities
|
||||
options := &ImagingOptions{
|
||||
@@ -387,7 +387,7 @@ func (s *Server) HandleGetOptions(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleMove handles Move (focus) request
|
||||
// HandleMove handles Move (focus) request.
|
||||
func (s *Server) HandleMove(body interface{}) (interface{}, error) {
|
||||
var req MoveRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -400,7 +400,7 @@ func (s *Server) HandleMove(body interface{}) (interface{}, error) {
|
||||
|
||||
state, ok := s.imagingState[req.VideoSourceToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("video source not found: %s", req.VideoSourceToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrVideoSourceNotFound, req.VideoSourceToken)
|
||||
}
|
||||
|
||||
// Process focus move
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestHandleGetImagingSettings(t *testing.T) {
|
||||
|
||||
if settingsResp.ImagingSettings == nil {
|
||||
t.Error("ImagingSettings is nil")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,6 +108,7 @@ func TestHandleGetOptions(t *testing.T) {
|
||||
|
||||
if optionsResp.ImagingOptions == nil {
|
||||
t.Error("ImagingOptions is nil")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ func TestHandleGetOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleMove - DISABLED due to SOAP namespace requirements
|
||||
// TestHandleMove - DISABLED due to SOAP namespace requirements.
|
||||
func _DisabledTestHandleMove(t *testing.T) {
|
||||
config := createTestConfig()
|
||||
server, _ := New(config)
|
||||
|
||||
+38
-30
@@ -7,13 +7,13 @@ import (
|
||||
|
||||
// Media service SOAP message types
|
||||
|
||||
// GetProfilesResponse represents GetProfiles response
|
||||
// GetProfilesResponse represents GetProfiles response.
|
||||
type GetProfilesResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetProfilesResponse"`
|
||||
Profiles []MediaProfile `xml:"Profiles"`
|
||||
}
|
||||
|
||||
// MediaProfile represents a media profile
|
||||
// MediaProfile represents a media profile.
|
||||
type MediaProfile struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Fixed bool `xml:"fixed,attr"`
|
||||
@@ -27,7 +27,7 @@ type MediaProfile struct {
|
||||
MetadataConfiguration *MetadataConfiguration `xml:"MetadataConfiguration,omitempty"`
|
||||
}
|
||||
|
||||
// VideoSourceConfiguration represents video source configuration
|
||||
// VideoSourceConfiguration represents video source configuration.
|
||||
type VideoSourceConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
@@ -36,7 +36,7 @@ type VideoSourceConfiguration struct {
|
||||
Bounds IntRectangle `xml:"Bounds"`
|
||||
}
|
||||
|
||||
// AudioSourceConfiguration represents audio source configuration
|
||||
// AudioSourceConfiguration represents audio source configuration.
|
||||
type AudioSourceConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
@@ -44,7 +44,7 @@ type AudioSourceConfiguration struct {
|
||||
SourceToken string `xml:"SourceToken"`
|
||||
}
|
||||
|
||||
// VideoEncoderConfiguration represents video encoder configuration
|
||||
// VideoEncoderConfiguration represents video encoder configuration.
|
||||
type VideoEncoderConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
@@ -58,7 +58,7 @@ type VideoEncoderConfiguration struct {
|
||||
SessionTimeout string `xml:"SessionTimeout"`
|
||||
}
|
||||
|
||||
// AudioEncoderConfiguration represents audio encoder configuration
|
||||
// AudioEncoderConfiguration represents audio encoder configuration.
|
||||
type AudioEncoderConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
@@ -70,14 +70,14 @@ type AudioEncoderConfiguration struct {
|
||||
SessionTimeout string `xml:"SessionTimeout"`
|
||||
}
|
||||
|
||||
// VideoAnalyticsConfiguration represents video analytics configuration
|
||||
// VideoAnalyticsConfiguration represents video analytics configuration.
|
||||
type VideoAnalyticsConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
UseCount int `xml:"UseCount"`
|
||||
}
|
||||
|
||||
// PTZConfiguration represents PTZ configuration
|
||||
// PTZConfiguration represents PTZ configuration.
|
||||
type PTZConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
@@ -85,7 +85,7 @@ type PTZConfiguration struct {
|
||||
NodeToken string `xml:"NodeToken"`
|
||||
}
|
||||
|
||||
// MetadataConfiguration represents metadata configuration
|
||||
// MetadataConfiguration represents metadata configuration.
|
||||
type MetadataConfiguration struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
@@ -93,7 +93,7 @@ type MetadataConfiguration struct {
|
||||
SessionTimeout string `xml:"SessionTimeout"`
|
||||
}
|
||||
|
||||
// IntRectangle represents a rectangle with integer coordinates
|
||||
// IntRectangle represents a rectangle with integer coordinates.
|
||||
type IntRectangle struct {
|
||||
X int `xml:"x,attr"`
|
||||
Y int `xml:"y,attr"`
|
||||
@@ -101,26 +101,26 @@ type IntRectangle struct {
|
||||
Height int `xml:"height,attr"`
|
||||
}
|
||||
|
||||
// VideoResolution represents video resolution
|
||||
// VideoResolution represents video resolution.
|
||||
type VideoResolution struct {
|
||||
Width int `xml:"Width"`
|
||||
Height int `xml:"Height"`
|
||||
}
|
||||
|
||||
// VideoRateControl represents video rate control
|
||||
// VideoRateControl represents video rate control.
|
||||
type VideoRateControl struct {
|
||||
FrameRateLimit int `xml:"FrameRateLimit"`
|
||||
EncodingInterval int `xml:"EncodingInterval"`
|
||||
BitrateLimit int `xml:"BitrateLimit"`
|
||||
}
|
||||
|
||||
// H264Configuration represents H264 configuration
|
||||
// H264Configuration represents H264 configuration.
|
||||
type H264Configuration struct {
|
||||
GovLength int `xml:"GovLength"`
|
||||
H264Profile string `xml:"H264Profile"`
|
||||
}
|
||||
|
||||
// MulticastConfiguration represents multicast configuration
|
||||
// MulticastConfiguration represents multicast configuration.
|
||||
type MulticastConfiguration struct {
|
||||
Address IPAddress `xml:"Address"`
|
||||
Port int `xml:"Port"`
|
||||
@@ -128,20 +128,20 @@ type MulticastConfiguration struct {
|
||||
AutoStart bool `xml:"AutoStart"`
|
||||
}
|
||||
|
||||
// IPAddress represents an IP address
|
||||
// IPAddress represents an IP address.
|
||||
type IPAddress struct {
|
||||
Type string `xml:"Type"`
|
||||
IPv4Address string `xml:"IPv4Address,omitempty"`
|
||||
IPv6Address string `xml:"IPv6Address,omitempty"`
|
||||
}
|
||||
|
||||
// GetStreamURIResponse represents GetStreamURI response
|
||||
// GetStreamURIResponse represents GetStreamURI response.
|
||||
type GetStreamURIResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetStreamURIResponse"`
|
||||
MediaUri MediaUri `xml:"MediaUri"`
|
||||
}
|
||||
|
||||
// MediaUri represents a media URI
|
||||
// MediaUri represents a media URI.
|
||||
type MediaUri struct {
|
||||
Uri string `xml:"Uri"`
|
||||
InvalidAfterConnect bool `xml:"InvalidAfterConnect"`
|
||||
@@ -149,19 +149,19 @@ type MediaUri struct {
|
||||
Timeout string `xml:"Timeout"`
|
||||
}
|
||||
|
||||
// GetSnapshotURIResponse represents GetSnapshotURI response
|
||||
// GetSnapshotURIResponse represents GetSnapshotURI response.
|
||||
type GetSnapshotURIResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetSnapshotURIResponse"`
|
||||
MediaUri MediaUri `xml:"MediaUri"`
|
||||
}
|
||||
|
||||
// GetVideoSourcesResponse represents GetVideoSources response
|
||||
// GetVideoSourcesResponse represents GetVideoSources response.
|
||||
type GetVideoSourcesResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetVideoSourcesResponse"`
|
||||
VideoSources []VideoSource `xml:"VideoSources"`
|
||||
}
|
||||
|
||||
// VideoSource represents a video source
|
||||
// VideoSource represents a video source.
|
||||
type VideoSource struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Framerate float64 `xml:"Framerate"`
|
||||
@@ -170,10 +170,11 @@ type VideoSource struct {
|
||||
|
||||
// Media service handlers
|
||||
|
||||
// HandleGetProfiles handles GetProfiles request
|
||||
// HandleGetProfiles handles GetProfiles request.
|
||||
func (s *Server) HandleGetProfiles(body interface{}) (interface{}, error) {
|
||||
profiles := make([]MediaProfile, len(s.config.Profiles))
|
||||
|
||||
//nolint:gocritic // Range value copy is acceptable for small structs
|
||||
for i, profileCfg := range s.config.Profiles {
|
||||
profile := MediaProfile{
|
||||
Token: profileCfg.Token,
|
||||
@@ -258,7 +259,7 @@ func (s *Server) HandleGetProfiles(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGetStreamURI handles GetStreamURI request
|
||||
// HandleGetStreamURI handles GetStreamURI request.
|
||||
func (s *Server) HandleGetStreamURI(body interface{}) (interface{}, error) {
|
||||
var req struct {
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -271,7 +272,7 @@ func (s *Server) HandleGetStreamURI(body interface{}) (interface{}, error) {
|
||||
// Find the stream configuration for this profile
|
||||
streamCfg, ok := s.streams[req.ProfileToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("profile not found: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrProfileNotFound, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Build RTSP URI
|
||||
@@ -295,7 +296,7 @@ func (s *Server) HandleGetStreamURI(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGetSnapshotURI handles GetSnapshotURI request
|
||||
// HandleGetSnapshotURI handles GetSnapshotURI request.
|
||||
func (s *Server) HandleGetSnapshotURI(body interface{}) (interface{}, error) {
|
||||
var req struct {
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -310,16 +311,17 @@ func (s *Server) HandleGetSnapshotURI(body interface{}) (interface{}, error) {
|
||||
for i := range s.config.Profiles {
|
||||
if s.config.Profiles[i].Token == req.ProfileToken {
|
||||
profileCfg = &s.config.Profiles[i]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if profileCfg == nil {
|
||||
return nil, fmt.Errorf("profile not found: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrProfileNotFound, req.ProfileToken)
|
||||
}
|
||||
|
||||
if !profileCfg.Snapshot.Enabled {
|
||||
return nil, fmt.Errorf("snapshot not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrSnapshotNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Build snapshot URI
|
||||
@@ -340,12 +342,13 @@ func (s *Server) HandleGetSnapshotURI(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGetVideoSources handles GetVideoSources request
|
||||
// HandleGetVideoSources handles GetVideoSources request.
|
||||
func (s *Server) HandleGetVideoSources(body interface{}) (interface{}, error) {
|
||||
sources := make([]VideoSource, 0)
|
||||
|
||||
// Collect unique video sources from profiles
|
||||
seenSources := make(map[string]bool)
|
||||
//nolint:gocritic // Range value copy is acceptable for small structs
|
||||
for _, profileCfg := range s.config.Profiles {
|
||||
if !seenSources[profileCfg.VideoSource.Token] {
|
||||
sources = append(sources, VideoSource{
|
||||
@@ -365,8 +368,8 @@ func (s *Server) HandleGetVideoSources(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// unmarshalBody is a helper to unmarshal SOAP body content
|
||||
func unmarshalBody(body interface{}, target interface{}) error {
|
||||
// unmarshalBody is a helper to unmarshal SOAP body content.
|
||||
func unmarshalBody(body, target interface{}) error {
|
||||
var bodyXML []byte
|
||||
var err error
|
||||
|
||||
@@ -379,5 +382,10 @@ func unmarshalBody(body interface{}, target interface{}) error {
|
||||
return fmt.Errorf("failed to marshal XML: %w", err)
|
||||
}
|
||||
}
|
||||
return xml.Unmarshal(bodyXML, target)
|
||||
|
||||
if err := xml.Unmarshal(bodyXML, target); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal XML: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ func TestHandleGetStreamURI(t *testing.T) {
|
||||
|
||||
if streamResp.MediaUri.Uri == "" {
|
||||
t.Error("Stream URI is empty")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,6 +101,7 @@ func TestHandleGetVideoSources(t *testing.T) {
|
||||
|
||||
if len(sourcesResp.VideoSources) == 0 {
|
||||
t.Error("No video sources returned")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+54
-49
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// PTZ service SOAP message types
|
||||
|
||||
// ContinuousMoveRequest represents ContinuousMove request
|
||||
// ContinuousMoveRequest represents ContinuousMove request.
|
||||
type ContinuousMoveRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl ContinuousMove"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -17,12 +17,12 @@ type ContinuousMoveRequest struct {
|
||||
Timeout string `xml:"Timeout,omitempty"`
|
||||
}
|
||||
|
||||
// ContinuousMoveResponse represents ContinuousMove response
|
||||
// ContinuousMoveResponse represents ContinuousMove response.
|
||||
type ContinuousMoveResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl ContinuousMoveResponse"`
|
||||
}
|
||||
|
||||
// AbsoluteMoveRequest represents AbsoluteMove request
|
||||
// AbsoluteMoveRequest represents AbsoluteMove request.
|
||||
type AbsoluteMoveRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl AbsoluteMove"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -30,12 +30,12 @@ type AbsoluteMoveRequest struct {
|
||||
Speed PTZVector `xml:"Speed,omitempty"`
|
||||
}
|
||||
|
||||
// AbsoluteMoveResponse represents AbsoluteMove response
|
||||
// AbsoluteMoveResponse represents AbsoluteMove response.
|
||||
type AbsoluteMoveResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl AbsoluteMoveResponse"`
|
||||
}
|
||||
|
||||
// RelativeMoveRequest represents RelativeMove request
|
||||
// RelativeMoveRequest represents RelativeMove request.
|
||||
type RelativeMoveRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl RelativeMove"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -43,12 +43,12 @@ type RelativeMoveRequest struct {
|
||||
Speed PTZVector `xml:"Speed,omitempty"`
|
||||
}
|
||||
|
||||
// RelativeMoveResponse represents RelativeMove response
|
||||
// RelativeMoveResponse represents RelativeMove response.
|
||||
type RelativeMoveResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl RelativeMoveResponse"`
|
||||
}
|
||||
|
||||
// StopRequest represents Stop request
|
||||
// StopRequest represents Stop request.
|
||||
type StopRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl Stop"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -56,75 +56,75 @@ type StopRequest struct {
|
||||
Zoom bool `xml:"Zoom,omitempty"`
|
||||
}
|
||||
|
||||
// StopResponse represents Stop response
|
||||
// StopResponse represents Stop response.
|
||||
type StopResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl StopResponse"`
|
||||
}
|
||||
|
||||
// GetStatusRequest represents GetStatus request
|
||||
// GetStatusRequest represents GetStatus request.
|
||||
type GetStatusRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GetStatus"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
}
|
||||
|
||||
// GetStatusResponse represents GetStatus response
|
||||
// GetStatusResponse represents GetStatus response.
|
||||
type GetStatusResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GetStatusResponse"`
|
||||
PTZStatus *PTZStatus `xml:"PTZStatus"`
|
||||
}
|
||||
|
||||
// PTZStatus represents PTZ status
|
||||
// PTZStatus represents PTZ status.
|
||||
type PTZStatus struct {
|
||||
Position PTZVector `xml:"Position"`
|
||||
MoveStatus PTZMoveStatus `xml:"MoveStatus"`
|
||||
UTCTime string `xml:"UtcTime"`
|
||||
}
|
||||
|
||||
// PTZMoveStatus represents PTZ movement status
|
||||
// PTZMoveStatus represents PTZ movement status.
|
||||
type PTZMoveStatus struct {
|
||||
PanTilt string `xml:"PanTilt,omitempty"`
|
||||
Zoom string `xml:"Zoom,omitempty"`
|
||||
}
|
||||
|
||||
// PTZVector represents PTZ position/velocity
|
||||
// PTZVector represents PTZ position/velocity.
|
||||
type PTZVector struct {
|
||||
PanTilt *Vector2D `xml:"PanTilt,omitempty"`
|
||||
Zoom *Vector1D `xml:"Zoom,omitempty"`
|
||||
}
|
||||
|
||||
// Vector2D represents a 2D vector
|
||||
// Vector2D represents a 2D vector.
|
||||
type Vector2D struct {
|
||||
X float64 `xml:"x,attr"`
|
||||
Y float64 `xml:"y,attr"`
|
||||
Space string `xml:"space,attr,omitempty"`
|
||||
}
|
||||
|
||||
// Vector1D represents a 1D vector
|
||||
// Vector1D represents a 1D vector.
|
||||
type Vector1D struct {
|
||||
X float64 `xml:"x,attr"`
|
||||
Space string `xml:"space,attr,omitempty"`
|
||||
}
|
||||
|
||||
// GetPresetsRequest represents GetPresets request
|
||||
// GetPresetsRequest represents GetPresets request.
|
||||
type GetPresetsRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GetPresets"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
}
|
||||
|
||||
// GetPresetsResponse represents GetPresets response
|
||||
// GetPresetsResponse represents GetPresets response.
|
||||
type GetPresetsResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GetPresetsResponse"`
|
||||
Preset []PTZPreset `xml:"Preset"`
|
||||
}
|
||||
|
||||
// PTZPreset represents a PTZ preset
|
||||
// PTZPreset represents a PTZ preset.
|
||||
type PTZPreset struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
PTZPosition *PTZVector `xml:"PTZPosition,omitempty"`
|
||||
}
|
||||
|
||||
// GotoPresetRequest represents GotoPreset request
|
||||
// GotoPresetRequest represents GotoPreset request.
|
||||
type GotoPresetRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GotoPreset"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -132,12 +132,12 @@ type GotoPresetRequest struct {
|
||||
Speed PTZVector `xml:"Speed,omitempty"`
|
||||
}
|
||||
|
||||
// GotoPresetResponse represents GotoPreset response
|
||||
// GotoPresetResponse represents GotoPreset response.
|
||||
type GotoPresetResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GotoPresetResponse"`
|
||||
}
|
||||
|
||||
// SetPresetRequest represents SetPreset request
|
||||
// SetPresetRequest represents SetPreset request.
|
||||
type SetPresetRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl SetPreset"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
@@ -145,19 +145,19 @@ type SetPresetRequest struct {
|
||||
PresetToken string `xml:"PresetToken,omitempty"`
|
||||
}
|
||||
|
||||
// SetPresetResponse represents SetPreset response
|
||||
// SetPresetResponse represents SetPreset response.
|
||||
type SetPresetResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl SetPresetResponse"`
|
||||
PresetToken string `xml:"PresetToken"`
|
||||
}
|
||||
|
||||
// GetConfigurationsResponse represents GetConfigurations response
|
||||
// GetConfigurationsResponse represents GetConfigurations response.
|
||||
type GetConfigurationsResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GetConfigurationsResponse"`
|
||||
PTZConfiguration []PTZConfigurationExt `xml:"PTZConfiguration"`
|
||||
}
|
||||
|
||||
// PTZConfigurationExt represents PTZ configuration with extensions
|
||||
// PTZConfigurationExt represents PTZ configuration with extensions.
|
||||
type PTZConfigurationExt struct {
|
||||
Token string `xml:"token,attr"`
|
||||
Name string `xml:"Name"`
|
||||
@@ -167,30 +167,30 @@ type PTZConfigurationExt struct {
|
||||
ZoomLimits *ZoomLimits `xml:"ZoomLimits,omitempty"`
|
||||
}
|
||||
|
||||
// PanTiltLimits represents pan/tilt limits
|
||||
// PanTiltLimits represents pan/tilt limits.
|
||||
type PanTiltLimits struct {
|
||||
Range Space2DDescription `xml:"Range"`
|
||||
}
|
||||
|
||||
// ZoomLimits represents zoom limits
|
||||
// ZoomLimits represents zoom limits.
|
||||
type ZoomLimits struct {
|
||||
Range Space1DDescription `xml:"Range"`
|
||||
}
|
||||
|
||||
// Space2DDescription represents 2D space description
|
||||
// Space2DDescription represents 2D space description.
|
||||
type Space2DDescription struct {
|
||||
URI string `xml:"URI"`
|
||||
XRange FloatRange `xml:"XRange"`
|
||||
YRange FloatRange `xml:"YRange"`
|
||||
}
|
||||
|
||||
// Space1DDescription represents 1D space description
|
||||
// Space1DDescription represents 1D space description.
|
||||
type Space1DDescription struct {
|
||||
URI string `xml:"URI"`
|
||||
XRange FloatRange `xml:"XRange"`
|
||||
}
|
||||
|
||||
// FloatRange represents a float range
|
||||
// FloatRange represents a float range.
|
||||
type FloatRange struct {
|
||||
Min float64 `xml:"Min"`
|
||||
Max float64 `xml:"Max"`
|
||||
@@ -200,7 +200,7 @@ type FloatRange struct {
|
||||
|
||||
var ptzMutex sync.RWMutex
|
||||
|
||||
// HandleContinuousMove handles ContinuousMove request
|
||||
// HandleContinuousMove handles ContinuousMove request.
|
||||
func (s *Server) HandleContinuousMove(body interface{}) (interface{}, error) {
|
||||
var req ContinuousMoveRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -213,7 +213,7 @@ func (s *Server) HandleContinuousMove(body interface{}) (interface{}, error) {
|
||||
|
||||
state, ok := s.ptzState[req.ProfileToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("PTZ not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Set movement state
|
||||
@@ -233,7 +233,7 @@ func (s *Server) HandleContinuousMove(body interface{}) (interface{}, error) {
|
||||
return &ContinuousMoveResponse{}, nil
|
||||
}
|
||||
|
||||
// HandleAbsoluteMove handles AbsoluteMove request
|
||||
// HandleAbsoluteMove handles AbsoluteMove request.
|
||||
func (s *Server) HandleAbsoluteMove(body interface{}) (interface{}, error) {
|
||||
var req AbsoluteMoveRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -246,7 +246,7 @@ func (s *Server) HandleAbsoluteMove(body interface{}) (interface{}, error) {
|
||||
|
||||
state, ok := s.ptzState[req.ProfileToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("PTZ not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Update position
|
||||
@@ -280,7 +280,7 @@ func (s *Server) HandleAbsoluteMove(body interface{}) (interface{}, error) {
|
||||
return &AbsoluteMoveResponse{}, nil
|
||||
}
|
||||
|
||||
// HandleRelativeMove handles RelativeMove request
|
||||
// HandleRelativeMove handles RelativeMove request.
|
||||
func (s *Server) HandleRelativeMove(body interface{}) (interface{}, error) {
|
||||
var req RelativeMoveRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -293,7 +293,7 @@ func (s *Server) HandleRelativeMove(body interface{}) (interface{}, error) {
|
||||
|
||||
state, ok := s.ptzState[req.ProfileToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("PTZ not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Update position relatively
|
||||
@@ -327,7 +327,7 @@ func (s *Server) HandleRelativeMove(body interface{}) (interface{}, error) {
|
||||
return &RelativeMoveResponse{}, nil
|
||||
}
|
||||
|
||||
// HandleStop handles Stop request
|
||||
// HandleStop handles Stop request.
|
||||
func (s *Server) HandleStop(body interface{}) (interface{}, error) {
|
||||
var req StopRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -340,7 +340,7 @@ func (s *Server) HandleStop(body interface{}) (interface{}, error) {
|
||||
|
||||
state, ok := s.ptzState[req.ProfileToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("PTZ not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Stop movement
|
||||
@@ -363,7 +363,7 @@ func (s *Server) HandleStop(body interface{}) (interface{}, error) {
|
||||
return &StopResponse{}, nil
|
||||
}
|
||||
|
||||
// HandleGetStatus handles GetStatus request
|
||||
// HandleGetStatus handles GetStatus request.
|
||||
func (s *Server) HandleGetStatus(body interface{}) (interface{}, error) {
|
||||
var req GetStatusRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -376,7 +376,7 @@ func (s *Server) HandleGetStatus(body interface{}) (interface{}, error) {
|
||||
|
||||
state, ok := s.ptzState[req.ProfileToken]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("PTZ not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Build status response
|
||||
@@ -404,7 +404,7 @@ func (s *Server) HandleGetStatus(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGetPresets handles GetPresets request
|
||||
// HandleGetPresets handles GetPresets request.
|
||||
func (s *Server) HandleGetPresets(body interface{}) (interface{}, error) {
|
||||
var req GetPresetsRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -416,12 +416,13 @@ func (s *Server) HandleGetPresets(body interface{}) (interface{}, error) {
|
||||
for i := range s.config.Profiles {
|
||||
if s.config.Profiles[i].Token == req.ProfileToken {
|
||||
profileCfg = &s.config.Profiles[i]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if profileCfg == nil || profileCfg.PTZ == nil {
|
||||
return nil, fmt.Errorf("PTZ not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Build presets response
|
||||
@@ -447,7 +448,7 @@ func (s *Server) HandleGetPresets(body interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HandleGotoPreset handles GotoPreset request
|
||||
// HandleGotoPreset handles GotoPreset request.
|
||||
func (s *Server) HandleGotoPreset(body interface{}) (interface{}, error) {
|
||||
var req GotoPresetRequest
|
||||
if err := unmarshalBody(body, &req); err != nil {
|
||||
@@ -459,12 +460,13 @@ func (s *Server) HandleGotoPreset(body interface{}) (interface{}, error) {
|
||||
for i := range s.config.Profiles {
|
||||
if s.config.Profiles[i].Token == req.ProfileToken {
|
||||
profileCfg = &s.config.Profiles[i]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if profileCfg == nil || profileCfg.PTZ == nil {
|
||||
return nil, fmt.Errorf("PTZ not supported for profile: %s", req.ProfileToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken)
|
||||
}
|
||||
|
||||
// Find the preset
|
||||
@@ -472,12 +474,13 @@ func (s *Server) HandleGotoPreset(body interface{}) (interface{}, error) {
|
||||
for _, preset := range profileCfg.PTZ.Presets {
|
||||
if preset.Token == req.PresetToken {
|
||||
presetPos = &preset.Position
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if presetPos == nil {
|
||||
return nil, fmt.Errorf("preset not found: %s", req.PresetToken)
|
||||
return nil, fmt.Errorf("%w: %s", ErrPresetNotFound, req.PresetToken)
|
||||
}
|
||||
|
||||
// Get PTZ state and move to preset
|
||||
@@ -512,15 +515,17 @@ func getMoveStatusString(moving bool) string {
|
||||
if moving {
|
||||
return "MOVING"
|
||||
}
|
||||
|
||||
return "IDLE"
|
||||
}
|
||||
|
||||
func clamp(value, min, max float64) float64 {
|
||||
if value < min {
|
||||
return min
|
||||
func clamp(value, minVal, maxVal float64) float64 {
|
||||
if value < minVal {
|
||||
return minVal
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
if value > maxVal {
|
||||
return maxVal
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
+5
-4
@@ -6,8 +6,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestHandleGetPresets tests GetPresets handler - DISABLED due to SOAP namespace requirements
|
||||
// These handlers are better tested through the SOAP handler in integration tests
|
||||
// These handlers are better tested through the SOAP handler in integration tests.
|
||||
func _DisabledTestHandleGetPresets(t *testing.T) {
|
||||
config := createTestConfig()
|
||||
server, _ := New(config)
|
||||
@@ -75,7 +74,7 @@ func TestHandleGotoPreset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleGetStatus - DISABLED due to SOAP namespace requirements
|
||||
// TestHandleGetStatus - DISABLED due to SOAP namespace requirements.
|
||||
func _DisabledTestHandleGetStatus(t *testing.T) {
|
||||
config := createTestConfig()
|
||||
server, _ := New(config)
|
||||
@@ -100,6 +99,7 @@ func _DisabledTestHandleGetStatus(t *testing.T) {
|
||||
|
||||
if statusResp.PTZStatus == nil {
|
||||
t.Error("PTZStatus is nil")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ func _DisabledTestHandleContinuousMove(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleStop - DISABLED due to SOAP namespace requirements
|
||||
// TestHandleStop - DISABLED due to SOAP namespace requirements.
|
||||
func _DisabledTestHandleStop(t *testing.T) {
|
||||
config := createTestConfig()
|
||||
server, _ := New(config)
|
||||
@@ -468,6 +468,7 @@ func TestPTZPresetOperations(t *testing.T) {
|
||||
name: "GetStatus",
|
||||
testFunc: func() (interface{}, error) {
|
||||
reqXML := `<GetStatus><ProfileToken>` + config.Profiles[0].Token + `</ProfileToken></GetStatus>`
|
||||
|
||||
return server.HandleGetStatus([]byte(reqXML))
|
||||
},
|
||||
},
|
||||
|
||||
+35
-17
@@ -1,7 +1,9 @@
|
||||
// Package server provides ONVIF server implementation for testing and simulation.
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -9,7 +11,7 @@ import (
|
||||
"github.com/0x524a/onvif-go/server/soap"
|
||||
)
|
||||
|
||||
// New creates a new ONVIF server with the given configuration
|
||||
// New creates a new ONVIF server with the given configuration.
|
||||
func New(config *Config) (*Server, error) {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
@@ -96,7 +98,7 @@ func New(config *Config) (*Server, error) {
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// Start starts the ONVIF server
|
||||
// Start starts the ONVIF server.
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
// Create HTTP server
|
||||
mux := http.NewServeMux()
|
||||
@@ -138,6 +140,7 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
fmt.Printf("📷 Imaging Service: http://%s%s/imaging_service\n", addr, s.config.BasePath)
|
||||
}
|
||||
fmt.Printf("\n🌐 Virtual Camera Profiles:\n")
|
||||
//nolint:gocritic // Range value copy is acceptable for small structs
|
||||
for i, profile := range s.config.Profiles {
|
||||
stream := s.streams[profile.Token]
|
||||
fmt.Printf(" [%d] %s - %s (%dx%d @ %dfps)\n",
|
||||
@@ -148,7 +151,7 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
}
|
||||
fmt.Printf("\n✅ Server is ready!\n\n")
|
||||
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
@@ -159,13 +162,18 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
fmt.Println("\n🛑 Shutting down server...")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return httpServer.Shutdown(shutdownCtx)
|
||||
|
||||
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("server shutdown failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
case err := <-errChan:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// registerDeviceService registers the device service handler
|
||||
// registerDeviceService registers the device service handler.
|
||||
func (s *Server) registerDeviceService(mux *http.ServeMux) {
|
||||
handler := soap.NewHandler(s.config.Username, s.config.Password)
|
||||
|
||||
@@ -179,7 +187,7 @@ func (s *Server) registerDeviceService(mux *http.ServeMux) {
|
||||
mux.Handle(s.config.BasePath+"/device_service", handler)
|
||||
}
|
||||
|
||||
// registerMediaService registers the media service handler
|
||||
// registerMediaService registers the media service handler.
|
||||
func (s *Server) registerMediaService(mux *http.ServeMux) {
|
||||
handler := soap.NewHandler(s.config.Username, s.config.Password)
|
||||
|
||||
@@ -192,7 +200,7 @@ func (s *Server) registerMediaService(mux *http.ServeMux) {
|
||||
mux.Handle(s.config.BasePath+"/media_service", handler)
|
||||
}
|
||||
|
||||
// registerPTZService registers the PTZ service handler
|
||||
// registerPTZService registers the PTZ service handler.
|
||||
func (s *Server) registerPTZService(mux *http.ServeMux) {
|
||||
handler := soap.NewHandler(s.config.Username, s.config.Password)
|
||||
|
||||
@@ -208,7 +216,7 @@ func (s *Server) registerPTZService(mux *http.ServeMux) {
|
||||
mux.Handle(s.config.BasePath+"/ptz_service", handler)
|
||||
}
|
||||
|
||||
// registerImagingService registers the imaging service handler
|
||||
// registerImagingService registers the imaging service handler.
|
||||
func (s *Server) registerImagingService(mux *http.ServeMux) {
|
||||
handler := soap.NewHandler(s.config.Username, s.config.Password)
|
||||
|
||||
@@ -221,12 +229,13 @@ func (s *Server) registerImagingService(mux *http.ServeMux) {
|
||||
mux.Handle(s.config.BasePath+"/imaging_service", handler)
|
||||
}
|
||||
|
||||
// handleSnapshot handles HTTP snapshot requests
|
||||
// handleSnapshot handles HTTP snapshot requests.
|
||||
func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
// Get profile token from query parameter
|
||||
profileToken := r.URL.Query().Get("profile")
|
||||
if profileToken == "" {
|
||||
http.Error(w, "Missing profile parameter", http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,17 +244,20 @@ func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
for i := range s.config.Profiles {
|
||||
if s.config.Profiles[i].Token == profileToken {
|
||||
profileCfg = &s.config.Profiles[i]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if profileCfg == nil {
|
||||
http.Error(w, "Profile not found", http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !profileCfg.Snapshot.Enabled {
|
||||
http.Error(w, "Snapshot not supported", http.StatusNotImplemented)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -258,49 +270,53 @@ func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Generate or capture actual JPEG snapshot
|
||||
}
|
||||
|
||||
// GetConfig returns the server configuration
|
||||
// GetConfig returns the server configuration.
|
||||
func (s *Server) GetConfig() *Config {
|
||||
return s.config
|
||||
}
|
||||
|
||||
// GetStreamConfig returns the stream configuration for a profile
|
||||
// GetStreamConfig returns the stream configuration for a profile.
|
||||
func (s *Server) GetStreamConfig(profileToken string) (*StreamConfig, bool) {
|
||||
stream, ok := s.streams[profileToken]
|
||||
|
||||
return stream, ok
|
||||
}
|
||||
|
||||
// UpdateStreamURI updates the RTSP URI for a profile
|
||||
// UpdateStreamURI updates the RTSP URI for a profile.
|
||||
func (s *Server) UpdateStreamURI(profileToken, uri string) error {
|
||||
stream, ok := s.streams[profileToken]
|
||||
if !ok {
|
||||
return fmt.Errorf("profile not found: %s", profileToken)
|
||||
return fmt.Errorf("%w: %s", ErrProfileNotFound, profileToken)
|
||||
}
|
||||
stream.StreamURI = uri
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListProfiles returns all configured profiles
|
||||
// ListProfiles returns all configured profiles.
|
||||
func (s *Server) ListProfiles() []ProfileConfig {
|
||||
return s.config.Profiles
|
||||
}
|
||||
|
||||
// GetPTZState returns the current PTZ state for a profile
|
||||
// GetPTZState returns the current PTZ state for a profile.
|
||||
func (s *Server) GetPTZState(profileToken string) (*PTZState, bool) {
|
||||
ptzMutex.RLock()
|
||||
defer ptzMutex.RUnlock()
|
||||
state, ok := s.ptzState[profileToken]
|
||||
|
||||
return state, ok
|
||||
}
|
||||
|
||||
// GetImagingState returns the current imaging state for a video source
|
||||
// GetImagingState returns the current imaging state for a video source.
|
||||
func (s *Server) GetImagingState(videoSourceToken string) (*ImagingState, bool) {
|
||||
imagingMutex.RLock()
|
||||
defer imagingMutex.RUnlock()
|
||||
state, ok := s.imagingState[videoSourceToken]
|
||||
|
||||
return state, ok
|
||||
}
|
||||
|
||||
// ServerInfo returns human-readable server information
|
||||
// ServerInfo returns human-readable server information.
|
||||
func (s *Server) ServerInfo() string {
|
||||
var info string
|
||||
info += "ONVIF Server Configuration\n"
|
||||
@@ -311,6 +327,7 @@ func (s *Server) ServerInfo() string {
|
||||
info += fmt.Sprintf("\nServer Address: %s:%d\n", s.config.Host, s.config.Port)
|
||||
info += fmt.Sprintf("Base Path: %s\n", s.config.BasePath)
|
||||
info += fmt.Sprintf("\nProfiles (%d):\n", len(s.config.Profiles))
|
||||
//nolint:gocritic // Range value copy is acceptable for small structs
|
||||
for i, profile := range s.config.Profiles {
|
||||
info += fmt.Sprintf(" [%d] %s (%s)\n", i+1, profile.Name, profile.Token)
|
||||
info += fmt.Sprintf(" Video: %dx%d @ %dfps (%s)\n",
|
||||
@@ -329,5 +346,6 @@ func (s *Server) ServerInfo() string {
|
||||
info += fmt.Sprintf(" PTZ: %v\n", s.config.SupportPTZ)
|
||||
info += fmt.Sprintf(" Imaging: %v\n", s.config.SupportImaging)
|
||||
info += fmt.Sprintf(" Events: %v\n", s.config.SupportEvents)
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -31,10 +31,12 @@ func TestNew(t *testing.T) {
|
||||
server, err := New(tt.config)
|
||||
if (err != nil) != tt.expectError {
|
||||
t.Errorf("New() error = %v, expectError %v", err, tt.expectError)
|
||||
|
||||
return
|
||||
}
|
||||
if server == nil && !tt.expectError {
|
||||
t.Error("New() returned nil server")
|
||||
|
||||
return
|
||||
}
|
||||
if server != nil && server.config == nil {
|
||||
@@ -61,6 +63,7 @@ func TestNewInitializesStreamsAndState(t *testing.T) {
|
||||
stream, ok := server.streams[profile.Token]
|
||||
if !ok {
|
||||
t.Errorf("Stream not found for profile %s", profile.Token)
|
||||
|
||||
continue
|
||||
}
|
||||
if stream.ProfileToken != profile.Token {
|
||||
@@ -120,6 +123,7 @@ func TestGetStreamConfig(t *testing.T) {
|
||||
if sc.StreamURI == "" {
|
||||
return errorf("StreamURI is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -135,6 +139,7 @@ func TestGetStreamConfig(t *testing.T) {
|
||||
stream, ok := server.GetStreamConfig(tt.token)
|
||||
if ok != tt.expectOk {
|
||||
t.Errorf("GetStreamConfig() ok = %v, expectOk %v", ok, tt.expectOk)
|
||||
|
||||
return
|
||||
}
|
||||
if ok && tt.checkFunc != nil {
|
||||
@@ -176,6 +181,7 @@ func TestUpdateStreamURI(t *testing.T) {
|
||||
err := server.UpdateStreamURI(tt.token, tt.newURI)
|
||||
if (err != nil) != tt.expectError {
|
||||
t.Errorf("UpdateStreamURI() error = %v, expectError %v", err, tt.expectError)
|
||||
|
||||
return
|
||||
}
|
||||
if !tt.expectError {
|
||||
@@ -217,6 +223,7 @@ func TestGetPTZState(t *testing.T) {
|
||||
for _, profile := range config.Profiles {
|
||||
if profile.PTZ != nil {
|
||||
profileWithPTZ = profile.Token
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -255,6 +262,7 @@ func TestGetPTZState(t *testing.T) {
|
||||
state, ok := server.GetPTZState(tt.token)
|
||||
if ok != tt.expectOk {
|
||||
t.Errorf("GetPTZState() ok = %v, expectOk %v", ok, tt.expectOk)
|
||||
|
||||
return
|
||||
}
|
||||
if ok && state == nil {
|
||||
@@ -287,6 +295,7 @@ func TestGetImagingState(t *testing.T) {
|
||||
if state.Contrast < 0 || state.Contrast > 100 {
|
||||
return errorf("contrast out of range: %f", state.Contrast)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -302,6 +311,7 @@ func TestGetImagingState(t *testing.T) {
|
||||
state, ok := server.GetImagingState(tt.token)
|
||||
if ok != tt.expectOk {
|
||||
t.Errorf("GetImagingState() ok = %v, expectOk %v", ok, tt.expectOk)
|
||||
|
||||
return
|
||||
}
|
||||
if ok && tt.checkFunc != nil {
|
||||
@@ -416,6 +426,7 @@ func contains(s, substr string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+43
-29
@@ -1,3 +1,4 @@
|
||||
// Package soap provides SOAP request handling for the ONVIF server.
|
||||
package soap
|
||||
|
||||
import (
|
||||
@@ -14,17 +15,17 @@ import (
|
||||
originsoap "github.com/0x524a/onvif-go/internal/soap"
|
||||
)
|
||||
|
||||
// Handler handles incoming SOAP requests
|
||||
// Handler handles incoming SOAP requests.
|
||||
type Handler struct {
|
||||
username string
|
||||
password string
|
||||
handlers map[string]MessageHandler
|
||||
}
|
||||
|
||||
// MessageHandler is a function that handles a specific SOAP message
|
||||
// MessageHandler is a function that handles a specific SOAP message.
|
||||
type MessageHandler func(body interface{}) (interface{}, error)
|
||||
|
||||
// NewHandler creates a new SOAP handler
|
||||
// NewHandler creates a new SOAP handler.
|
||||
func NewHandler(username, password string) *Handler {
|
||||
return &Handler{
|
||||
username: username,
|
||||
@@ -33,16 +34,17 @@ func NewHandler(username, password string) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHandler registers a handler for a specific action/message type
|
||||
// RegisterHandler registers a handler for a specific action/message type.
|
||||
func (h *Handler) RegisterHandler(action string, handler MessageHandler) {
|
||||
h.handlers[action] = handler
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler interface
|
||||
// ServeHTTP implements http.Handler interface.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Only accept POST requests
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,14 +52,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.sendFault(w, "Receiver", "Failed to read request body", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
//nolint:errcheck // Close error is not critical for cleanup
|
||||
_ = r.Body.Close()
|
||||
|
||||
// Extract action from raw XML first (before parsing)
|
||||
action := h.extractAction(body)
|
||||
if action == "" {
|
||||
h.sendFault(w, "Sender", "Unknown action", "Could not determine request action")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,6 +70,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var envelope originsoap.Envelope
|
||||
if err := xml.Unmarshal(body, &envelope); err != nil {
|
||||
h.sendFault(w, "Sender", "Invalid SOAP envelope", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -72,6 +78,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.username != "" && h.password != "" {
|
||||
if !h.authenticate(&envelope) {
|
||||
h.sendFault(w, "Sender", "Authentication failed", "Invalid username or password")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -80,6 +87,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
handler, ok := h.handlers[action]
|
||||
if !ok {
|
||||
h.sendFault(w, "Receiver", "Action not supported", fmt.Sprintf("No handler for action: %s", action))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,6 +95,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
response, err := handler(envelope.Body.Content)
|
||||
if err != nil {
|
||||
h.sendFault(w, "Receiver", "Handler error", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,7 +103,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.sendResponse(w, response)
|
||||
}
|
||||
|
||||
// authenticate verifies the WS-Security credentials
|
||||
// authenticate verifies the WS-Security credentials.
|
||||
func (h *Handler) authenticate(envelope *originsoap.Envelope) bool {
|
||||
if envelope.Header == nil || envelope.Header.Security == nil || envelope.Header.Security.UsernameToken == nil {
|
||||
return false
|
||||
@@ -124,7 +133,7 @@ func (h *Handler) authenticate(envelope *originsoap.Envelope) bool {
|
||||
return token.Password.Password == expectedDigest
|
||||
}
|
||||
|
||||
// extractAction extracts the action/message type from the SOAP body
|
||||
// extractAction extracts the action/message type from the SOAP body.
|
||||
func (h *Handler) extractAction(bodyXML []byte) string {
|
||||
// Parse XML to find the first element inside the Body element
|
||||
decoder := xml.NewDecoder(bytes.NewReader(bodyXML))
|
||||
@@ -156,7 +165,7 @@ func (h *Handler) extractAction(bodyXML []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// sendResponse sends a SOAP response
|
||||
// sendResponse sends a SOAP response.
|
||||
func (h *Handler) sendResponse(w http.ResponseWriter, response interface{}) {
|
||||
envelope := &originsoap.Envelope{
|
||||
Body: originsoap.Body{
|
||||
@@ -168,6 +177,7 @@ func (h *Handler) sendResponse(w http.ResponseWriter, response interface{}) {
|
||||
body, err := xml.MarshalIndent(envelope, "", " ")
|
||||
if err != nil {
|
||||
h.sendFault(w, "Receiver", "Failed to marshal response", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,10 +187,11 @@ func (h *Handler) sendResponse(w http.ResponseWriter, response interface{}) {
|
||||
// Send response
|
||||
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
//nolint:errcheck // Write error is not critical after WriteHeader
|
||||
_, _ = w.Write(xmlBody)
|
||||
}
|
||||
|
||||
// sendFault sends a SOAP fault response
|
||||
// sendFault sends a SOAP fault response.
|
||||
func (h *Handler) sendFault(w http.ResponseWriter, code, reason, detail string) {
|
||||
fault := &originsoap.Fault{
|
||||
Code: code,
|
||||
@@ -198,6 +209,7 @@ func (h *Handler) sendFault(w http.ResponseWriter, code, reason, detail string)
|
||||
body, err := xml.MarshalIndent(envelope, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,17 +223,18 @@ func (h *Handler) sendFault(w http.ResponseWriter, code, reason, detail string)
|
||||
statusCode = http.StatusBadRequest
|
||||
}
|
||||
w.WriteHeader(statusCode)
|
||||
//nolint:errcheck // Write error is not critical after WriteHeader
|
||||
_, _ = w.Write(xmlBody)
|
||||
}
|
||||
|
||||
// RequestWrapper wraps incoming SOAP request structures
|
||||
// RequestWrapper wraps incoming SOAP request structures.
|
||||
type RequestWrapper struct {
|
||||
XMLName xml.Name
|
||||
Content []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// ParseRequest parses a SOAP request into a specific structure
|
||||
func ParseRequest(bodyContent interface{}, target interface{}) error {
|
||||
// ParseRequest parses a SOAP request into a specific structure.
|
||||
func ParseRequest(bodyContent, target interface{}) error {
|
||||
// Marshal the body content back to XML
|
||||
bodyXML, err := xml.Marshal(bodyContent)
|
||||
if err != nil {
|
||||
@@ -238,18 +251,18 @@ func ParseRequest(bodyContent interface{}, target interface{}) error {
|
||||
|
||||
// Common SOAP request/response structures for ONVIF
|
||||
|
||||
// GetSystemDateAndTimeRequest represents GetSystemDateAndTime request
|
||||
// GetSystemDateAndTimeRequest represents GetSystemDateAndTime request.
|
||||
type GetSystemDateAndTimeRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetSystemDateAndTime"`
|
||||
}
|
||||
|
||||
// GetSystemDateAndTimeResponse represents GetSystemDateAndTime response
|
||||
// GetSystemDateAndTimeResponse represents GetSystemDateAndTime response.
|
||||
type GetSystemDateAndTimeResponse struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetSystemDateAndTimeResponse"`
|
||||
SystemDateAndTime SystemDateAndTime `xml:"SystemDateAndTime"`
|
||||
}
|
||||
|
||||
// SystemDateAndTime represents system date and time
|
||||
// SystemDateAndTime represents system date and time.
|
||||
type SystemDateAndTime struct {
|
||||
DateTimeType string `xml:"DateTimeType"`
|
||||
DaylightSavings bool `xml:"DaylightSavings"`
|
||||
@@ -258,32 +271,32 @@ type SystemDateAndTime struct {
|
||||
LocalDateTime DateTime `xml:"LocalDateTime,omitempty"`
|
||||
}
|
||||
|
||||
// TimeZone represents timezone information
|
||||
// TimeZone represents timezone information.
|
||||
type TimeZone struct {
|
||||
TZ string `xml:"TZ"`
|
||||
}
|
||||
|
||||
// DateTime represents date and time
|
||||
// DateTime represents date and time.
|
||||
type DateTime struct {
|
||||
Time Time `xml:"Time"`
|
||||
Date Date `xml:"Date"`
|
||||
}
|
||||
|
||||
// Time represents time components
|
||||
// Time represents time components.
|
||||
type Time struct {
|
||||
Hour int `xml:"Hour"`
|
||||
Minute int `xml:"Minute"`
|
||||
Second int `xml:"Second"`
|
||||
}
|
||||
|
||||
// Date represents date components
|
||||
// Date represents date components.
|
||||
type Date struct {
|
||||
Year int `xml:"Year"`
|
||||
Month int `xml:"Month"`
|
||||
Day int `xml:"Day"`
|
||||
}
|
||||
|
||||
// ToDateTime converts time.Time to DateTime structure
|
||||
// ToDateTime converts time.Time to DateTime structure.
|
||||
func ToDateTime(t time.Time) DateTime {
|
||||
return DateTime{
|
||||
Date: Date{
|
||||
@@ -299,57 +312,58 @@ func ToDateTime(t time.Time) DateTime {
|
||||
}
|
||||
}
|
||||
|
||||
// GetCapabilitiesRequest represents GetCapabilities request
|
||||
// GetCapabilitiesRequest represents GetCapabilities request.
|
||||
type GetCapabilitiesRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetCapabilities"`
|
||||
Category []string `xml:"Category,omitempty"`
|
||||
}
|
||||
|
||||
// GetDeviceInformationRequest represents GetDeviceInformation request
|
||||
// GetDeviceInformationRequest represents GetDeviceInformation request.
|
||||
type GetDeviceInformationRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetDeviceInformation"`
|
||||
}
|
||||
|
||||
// GetServicesRequest represents GetServices request
|
||||
// GetServicesRequest represents GetServices request.
|
||||
type GetServicesRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetServices"`
|
||||
IncludeCapability bool `xml:"IncludeCapability"`
|
||||
}
|
||||
|
||||
// GetProfilesRequest represents GetProfiles request
|
||||
// GetProfilesRequest represents GetProfiles request.
|
||||
type GetProfilesRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetProfiles"`
|
||||
}
|
||||
|
||||
// GetStreamURIRequest represents GetStreamURI request
|
||||
// GetStreamURIRequest represents GetStreamURI request.
|
||||
type GetStreamURIRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetStreamURI"`
|
||||
StreamSetup StreamSetup `xml:"StreamSetup"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
}
|
||||
|
||||
// StreamSetup represents stream setup parameters
|
||||
// StreamSetup represents stream setup parameters.
|
||||
type StreamSetup struct {
|
||||
Stream string `xml:"Stream"`
|
||||
Transport Transport `xml:"Transport"`
|
||||
}
|
||||
|
||||
// Transport represents transport parameters
|
||||
// Transport represents transport parameters.
|
||||
type Transport struct {
|
||||
Protocol string `xml:"Protocol"`
|
||||
}
|
||||
|
||||
// GetSnapshotURIRequest represents GetSnapshotURI request
|
||||
// GetSnapshotURIRequest represents GetSnapshotURI request.
|
||||
type GetSnapshotURIRequest struct {
|
||||
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetSnapshotURI"`
|
||||
ProfileToken string `xml:"ProfileToken"`
|
||||
}
|
||||
|
||||
// NormalizeAction normalizes SOAP action names
|
||||
// NormalizeAction normalizes SOAP action names.
|
||||
func NormalizeAction(action string) string {
|
||||
// Remove namespace prefixes
|
||||
if idx := strings.LastIndex(action, ":"); idx != -1 {
|
||||
action = action[idx+1:]
|
||||
}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ func TestNewHandler(t *testing.T) {
|
||||
|
||||
if handler == nil {
|
||||
t.Error("NewHandler returned nil")
|
||||
|
||||
return
|
||||
}
|
||||
if handler.username != "admin" {
|
||||
t.Errorf("Username mismatch: got %s, want admin", handler.username)
|
||||
@@ -46,7 +48,7 @@ func TestRegisterHandler(t *testing.T) {
|
||||
func TestServeHTTPMethodNotAllowed(t *testing.T) {
|
||||
handler := NewHandler("admin", "password")
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req := httptest.NewRequest("GET", "/", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
+28
-28
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/0x524a/onvif-go"
|
||||
)
|
||||
|
||||
// Config represents the ONVIF server configuration
|
||||
// Config represents the ONVIF server configuration.
|
||||
type Config struct {
|
||||
// Server settings
|
||||
Host string // Bind address (e.g., "0.0.0.0")
|
||||
@@ -31,7 +31,7 @@ type Config struct {
|
||||
SupportEvents bool
|
||||
}
|
||||
|
||||
// DeviceInfo contains device identification information
|
||||
// DeviceInfo contains device identification information.
|
||||
type DeviceInfo struct {
|
||||
Manufacturer string
|
||||
Model string
|
||||
@@ -40,7 +40,7 @@ type DeviceInfo struct {
|
||||
HardwareID string
|
||||
}
|
||||
|
||||
// ProfileConfig represents a camera profile configuration
|
||||
// ProfileConfig represents a camera profile configuration.
|
||||
type ProfileConfig struct {
|
||||
Token string // Profile token (unique identifier)
|
||||
Name string // Profile name
|
||||
@@ -52,7 +52,7 @@ type ProfileConfig struct {
|
||||
Snapshot SnapshotConfig // Snapshot configuration
|
||||
}
|
||||
|
||||
// VideoSourceConfig represents video source configuration
|
||||
// VideoSourceConfig represents video source configuration.
|
||||
type VideoSourceConfig struct {
|
||||
Token string // Video source token
|
||||
Name string // Video source name
|
||||
@@ -61,7 +61,7 @@ type VideoSourceConfig struct {
|
||||
Bounds Bounds
|
||||
}
|
||||
|
||||
// AudioSourceConfig represents audio source configuration
|
||||
// AudioSourceConfig represents audio source configuration.
|
||||
type AudioSourceConfig struct {
|
||||
Token string // Audio source token
|
||||
Name string // Audio source name
|
||||
@@ -69,7 +69,7 @@ type AudioSourceConfig struct {
|
||||
Bitrate int // Bitrate in kbps
|
||||
}
|
||||
|
||||
// VideoEncoderConfig represents video encoder configuration
|
||||
// VideoEncoderConfig represents video encoder configuration.
|
||||
type VideoEncoderConfig struct {
|
||||
Encoding string // JPEG, H264, H265, MPEG4
|
||||
Resolution Resolution // Video resolution
|
||||
@@ -79,14 +79,14 @@ type VideoEncoderConfig struct {
|
||||
GovLength int // GOP length
|
||||
}
|
||||
|
||||
// AudioEncoderConfig represents audio encoder configuration
|
||||
// AudioEncoderConfig represents audio encoder configuration.
|
||||
type AudioEncoderConfig struct {
|
||||
Encoding string // G711, G726, AAC
|
||||
Bitrate int // Bitrate in kbps
|
||||
SampleRate int // Sample rate in Hz
|
||||
}
|
||||
|
||||
// PTZConfig represents PTZ configuration
|
||||
// PTZConfig represents PTZ configuration.
|
||||
type PTZConfig struct {
|
||||
NodeToken string // PTZ node token
|
||||
PanRange Range // Pan range in degrees
|
||||
@@ -99,20 +99,20 @@ type PTZConfig struct {
|
||||
Presets []Preset // Predefined presets
|
||||
}
|
||||
|
||||
// SnapshotConfig represents snapshot configuration
|
||||
// SnapshotConfig represents snapshot configuration.
|
||||
type SnapshotConfig struct {
|
||||
Enabled bool // Whether snapshots are supported
|
||||
Resolution Resolution // Snapshot resolution
|
||||
Quality float64 // JPEG quality (0-100)
|
||||
}
|
||||
|
||||
// Resolution represents video resolution
|
||||
// Resolution represents video resolution.
|
||||
type Resolution struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// Bounds represents video bounds
|
||||
// Bounds represents video bounds.
|
||||
type Bounds struct {
|
||||
X int
|
||||
Y int
|
||||
@@ -120,41 +120,41 @@ type Bounds struct {
|
||||
Height int
|
||||
}
|
||||
|
||||
// Range represents a numeric range
|
||||
// Range represents a numeric range.
|
||||
type Range struct {
|
||||
Min float64
|
||||
Max float64
|
||||
}
|
||||
|
||||
// PTZSpeed represents PTZ movement speed
|
||||
// PTZSpeed represents PTZ movement speed.
|
||||
type PTZSpeed struct {
|
||||
Pan float64 // Pan speed (-1.0 to 1.0)
|
||||
Tilt float64 // Tilt speed (-1.0 to 1.0)
|
||||
Zoom float64 // Zoom speed (-1.0 to 1.0)
|
||||
}
|
||||
|
||||
// Preset represents a PTZ preset position
|
||||
// Preset represents a PTZ preset position.
|
||||
type Preset struct {
|
||||
Token string // Preset token
|
||||
Name string // Preset name
|
||||
Position PTZPosition // Position
|
||||
}
|
||||
|
||||
// PTZPosition represents PTZ position
|
||||
// PTZPosition represents PTZ position.
|
||||
type PTZPosition struct {
|
||||
Pan float64 // Pan position
|
||||
Tilt float64 // Tilt position
|
||||
Zoom float64 // Zoom position
|
||||
}
|
||||
|
||||
// StreamConfig represents an RTSP stream configuration
|
||||
// StreamConfig represents an RTSP stream configuration.
|
||||
type StreamConfig struct {
|
||||
ProfileToken string // Associated profile token
|
||||
RTSPPath string // RTSP path (e.g., "/stream1")
|
||||
StreamURI string // Full RTSP URI
|
||||
}
|
||||
|
||||
// Server represents the ONVIF server
|
||||
// Server represents the ONVIF server.
|
||||
type Server struct {
|
||||
config *Config
|
||||
streams map[string]*StreamConfig // Profile token -> stream config
|
||||
@@ -163,7 +163,7 @@ type Server struct {
|
||||
systemTime time.Time
|
||||
}
|
||||
|
||||
// PTZState represents the current PTZ state
|
||||
// PTZState represents the current PTZ state.
|
||||
type PTZState struct {
|
||||
Position PTZPosition
|
||||
Moving bool
|
||||
@@ -173,7 +173,7 @@ type PTZState struct {
|
||||
LastUpdate time.Time
|
||||
}
|
||||
|
||||
// ImagingState represents the current imaging settings state
|
||||
// ImagingState represents the current imaging settings state.
|
||||
type ImagingState struct {
|
||||
Brightness float64
|
||||
Contrast float64
|
||||
@@ -187,13 +187,13 @@ type ImagingState struct {
|
||||
IrCutFilter string // ON, OFF, AUTO
|
||||
}
|
||||
|
||||
// BacklightCompensation represents backlight compensation settings
|
||||
// BacklightCompensation represents backlight compensation settings.
|
||||
type BacklightCompensation struct {
|
||||
Mode string // OFF, ON
|
||||
Level float64 // 0-100
|
||||
}
|
||||
|
||||
// ExposureSettings represents exposure settings
|
||||
// ExposureSettings represents exposure settings.
|
||||
type ExposureSettings struct {
|
||||
Mode string // AUTO, MANUAL
|
||||
Priority string // LowNoise, FrameRate
|
||||
@@ -205,7 +205,7 @@ type ExposureSettings struct {
|
||||
Gain float64
|
||||
}
|
||||
|
||||
// FocusSettings represents focus settings
|
||||
// FocusSettings represents focus settings.
|
||||
type FocusSettings struct {
|
||||
AutoFocusMode string // AUTO, MANUAL
|
||||
DefaultSpeed float64
|
||||
@@ -214,20 +214,20 @@ type FocusSettings struct {
|
||||
CurrentPos float64
|
||||
}
|
||||
|
||||
// WhiteBalanceSettings represents white balance settings
|
||||
// WhiteBalanceSettings represents white balance settings.
|
||||
type WhiteBalanceSettings struct {
|
||||
Mode string // AUTO, MANUAL
|
||||
CrGain float64
|
||||
CbGain float64
|
||||
}
|
||||
|
||||
// WDRSettings represents wide dynamic range settings
|
||||
// WDRSettings represents wide dynamic range settings.
|
||||
type WDRSettings struct {
|
||||
Mode string // OFF, ON
|
||||
Level float64 // 0-100
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default server configuration with a multi-lens camera setup
|
||||
// DefaultConfig returns a default server configuration with a multi-lens camera setup.
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Host: "0.0.0.0",
|
||||
@@ -351,7 +351,7 @@ func DefaultConfig() *Config {
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceEndpoints returns the service endpoint URLs
|
||||
// ServiceEndpoints returns the service endpoint URLs.
|
||||
func (c *Config) ServiceEndpoints(host string) map[string]string {
|
||||
if host == "" {
|
||||
host = c.Host
|
||||
@@ -360,7 +360,7 @@ func (c *Config) ServiceEndpoints(host string) map[string]string {
|
||||
}
|
||||
}
|
||||
|
||||
baseURL := ""
|
||||
var baseURL string
|
||||
if c.Port == 80 {
|
||||
baseURL = "http://" + host + c.BasePath
|
||||
} else {
|
||||
@@ -385,7 +385,7 @@ func (c *Config) ServiceEndpoints(host string) map[string]string {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// ToONVIFProfile converts a ProfileConfig to an ONVIF Profile
|
||||
// ToONVIFProfile converts a ProfileConfig to an ONVIF Profile.
|
||||
func (p *ProfileConfig) ToONVIFProfile() *onvif.Profile {
|
||||
profile := &onvif.Profile{
|
||||
Token: p.Token,
|
||||
|
||||
@@ -19,6 +19,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.Host == "" {
|
||||
return errorf("Host is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -28,6 +29,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.Port <= 0 || c.Port > 65535 {
|
||||
return errorf("Port is invalid: %d", c.Port)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -37,6 +39,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.BasePath == "" {
|
||||
return errorf("BasePath is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -46,6 +49,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.Timeout <= 0 {
|
||||
return errorf("Timeout is not positive: %v", c.Timeout)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -61,6 +65,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.DeviceInfo.FirmwareVersion == "" {
|
||||
return errorf("FirmwareVersion is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -70,6 +75,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if len(c.Profiles) == 0 {
|
||||
return errorf("No profiles configured")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -79,6 +85,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.Profiles[0].Token == "" {
|
||||
return errorf("Profile token is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -88,6 +95,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.Profiles[0].Name == "" {
|
||||
return errorf("Profile name is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -103,6 +111,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.Profiles[0].VideoSource.Resolution.Height == 0 {
|
||||
return errorf("Video resolution height is 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -115,6 +124,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
if c.Profiles[0].VideoEncoder.Framerate == 0 {
|
||||
return errorf("Video framerate is 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user