package server import ( "encoding/xml" "fmt" "sync" ) // Imaging service SOAP message types // 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. type GetImagingSettingsResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl GetImagingSettingsResponse"` ImagingSettings *ImagingSettings `xml:"ImagingSettings"` } // ImagingSettings represents imaging settings. type ImagingSettings struct { BacklightCompensation *BacklightCompensationSettings `xml:"BacklightCompensation,omitempty"` Brightness *float64 `xml:"Brightness,omitempty"` ColorSaturation *float64 `xml:"ColorSaturation,omitempty"` Contrast *float64 `xml:"Contrast,omitempty"` Exposure *ExposureSettings20 `xml:"Exposure,omitempty"` Focus *FocusConfiguration20 `xml:"Focus,omitempty"` IrCutFilter *string `xml:"IrCutFilter,omitempty"` Sharpness *float64 `xml:"Sharpness,omitempty"` WideDynamicRange *WideDynamicRangeSettings `xml:"WideDynamicRange,omitempty"` WhiteBalance *WhiteBalanceSettings20 `xml:"WhiteBalance,omitempty"` } // 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. type ExposureSettings20 struct { Mode string `xml:"Mode"` Priority *string `xml:"Priority,omitempty"` Window *Rectangle `xml:"Window,omitempty"` MinExposureTime *float64 `xml:"MinExposureTime,omitempty"` MaxExposureTime *float64 `xml:"MaxExposureTime,omitempty"` MinGain *float64 `xml:"MinGain,omitempty"` MaxGain *float64 `xml:"MaxGain,omitempty"` MinIris *float64 `xml:"MinIris,omitempty"` MaxIris *float64 `xml:"MaxIris,omitempty"` ExposureTime *float64 `xml:"ExposureTime,omitempty"` Gain *float64 `xml:"Gain,omitempty"` Iris *float64 `xml:"Iris,omitempty"` } // FocusConfiguration20 represents focus configuration for ONVIF 2.0. type FocusConfiguration20 struct { AutoFocusMode string `xml:"AutoFocusMode"` DefaultSpeed *float64 `xml:"DefaultSpeed,omitempty"` NearLimit *float64 `xml:"NearLimit,omitempty"` FarLimit *float64 `xml:"FarLimit,omitempty"` } // 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. type WhiteBalanceSettings20 struct { Mode string `xml:"Mode"` CrGain *float64 `xml:"CrGain,omitempty"` CbGain *float64 `xml:"CbGain,omitempty"` } // Rectangle represents a rectangle. type Rectangle struct { Bottom float64 `xml:"bottom,attr"` Top float64 `xml:"top,attr"` Right float64 `xml:"right,attr"` Left float64 `xml:"left,attr"` } // SetImagingSettingsRequest represents SetImagingSettings request. type SetImagingSettingsRequest struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl SetImagingSettings"` VideoSourceToken string `xml:"VideoSourceToken"` ImagingSettings *ImagingSettings `xml:"ImagingSettings"` ForcePersistence bool `xml:"ForcePersistence,omitempty"` } // SetImagingSettingsResponse represents SetImagingSettings response. type SetImagingSettingsResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl SetImagingSettingsResponse"` } // 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. type GetOptionsResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl GetOptionsResponse"` ImagingOptions *ImagingOptions `xml:"ImagingOptions"` } // ImagingOptions represents imaging options/capabilities. type ImagingOptions struct { BacklightCompensation *BacklightCompensationOptions `xml:"BacklightCompensation,omitempty"` Brightness *FloatRange `xml:"Brightness,omitempty"` ColorSaturation *FloatRange `xml:"ColorSaturation,omitempty"` Contrast *FloatRange `xml:"Contrast,omitempty"` Exposure *ExposureOptions `xml:"Exposure,omitempty"` Focus *FocusOptions `xml:"Focus,omitempty"` IrCutFilterModes []string `xml:"IrCutFilterModes,omitempty"` Sharpness *FloatRange `xml:"Sharpness,omitempty"` WideDynamicRange *WideDynamicRangeOptions `xml:"WideDynamicRange,omitempty"` WhiteBalance *WhiteBalanceOptions `xml:"WhiteBalance,omitempty"` } // BacklightCompensationOptions represents backlight compensation options. type BacklightCompensationOptions struct { Mode []string `xml:"Mode"` Level *FloatRange `xml:"Level,omitempty"` } // ExposureOptions represents exposure options. type ExposureOptions struct { Mode []string `xml:"Mode"` Priority []string `xml:"Priority,omitempty"` MinExposureTime *FloatRange `xml:"MinExposureTime,omitempty"` MaxExposureTime *FloatRange `xml:"MaxExposureTime,omitempty"` MinGain *FloatRange `xml:"MinGain,omitempty"` MaxGain *FloatRange `xml:"MaxGain,omitempty"` MinIris *FloatRange `xml:"MinIris,omitempty"` MaxIris *FloatRange `xml:"MaxIris,omitempty"` ExposureTime *FloatRange `xml:"ExposureTime,omitempty"` Gain *FloatRange `xml:"Gain,omitempty"` Iris *FloatRange `xml:"Iris,omitempty"` } // FocusOptions represents focus options. type FocusOptions struct { AutoFocusModes []string `xml:"AutoFocusModes"` DefaultSpeed *FloatRange `xml:"DefaultSpeed,omitempty"` NearLimit *FloatRange `xml:"NearLimit,omitempty"` FarLimit *FloatRange `xml:"FarLimit,omitempty"` } // WideDynamicRangeOptions represents WDR options. type WideDynamicRangeOptions struct { Mode []string `xml:"Mode"` Level *FloatRange `xml:"Level,omitempty"` } // 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. 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. type FocusMove struct { Absolute *AbsoluteFocus `xml:"Absolute,omitempty"` Relative *RelativeFocus `xml:"Relative,omitempty"` Continuous *ContinuousFocus `xml:"Continuous,omitempty"` } // AbsoluteFocus represents absolute focus. type AbsoluteFocus struct { Position float64 `xml:"Position"` Speed *float64 `xml:"Speed,omitempty"` } // RelativeFocus represents relative focus. type RelativeFocus struct { Distance float64 `xml:"Distance"` Speed *float64 `xml:"Speed,omitempty"` } // ContinuousFocus represents continuous focus. type ContinuousFocus struct { Speed float64 `xml:"Speed"` } // MoveResponse represents Move response. type MoveResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/imaging/wsdl MoveResponse"` } // Imaging service handlers var imagingMutex sync.RWMutex // HandleGetImagingSettings handles GetImagingSettings request. func (s *Server) HandleGetImagingSettings(body interface{}) (interface{}, error) { var req GetImagingSettingsRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get imaging state imagingMutex.RLock() defer imagingMutex.RUnlock() state, ok := s.imagingState[req.VideoSourceToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrVideoSourceNotFound, req.VideoSourceToken) } // Build imaging settings response settings := &ImagingSettings{ Brightness: &state.Brightness, ColorSaturation: &state.Saturation, Contrast: &state.Contrast, Sharpness: &state.Sharpness, IrCutFilter: &state.IrCutFilter, BacklightCompensation: &BacklightCompensationSettings{ Mode: state.BacklightComp.Mode, Level: &state.BacklightComp.Level, }, Exposure: &ExposureSettings20{ Mode: state.Exposure.Mode, Priority: &state.Exposure.Priority, MinExposureTime: &state.Exposure.MinExposure, MaxExposureTime: &state.Exposure.MaxExposure, MinGain: &state.Exposure.MinGain, MaxGain: &state.Exposure.MaxGain, ExposureTime: &state.Exposure.ExposureTime, Gain: &state.Exposure.Gain, }, Focus: &FocusConfiguration20{ AutoFocusMode: state.Focus.AutoFocusMode, DefaultSpeed: &state.Focus.DefaultSpeed, NearLimit: &state.Focus.NearLimit, FarLimit: &state.Focus.FarLimit, }, WideDynamicRange: &WideDynamicRangeSettings{ Mode: state.WideDynamicRange.Mode, Level: &state.WideDynamicRange.Level, }, WhiteBalance: &WhiteBalanceSettings20{ Mode: state.WhiteBalance.Mode, CrGain: &state.WhiteBalance.CrGain, CbGain: &state.WhiteBalance.CbGain, }, } return &GetImagingSettingsResponse{ ImagingSettings: settings, }, nil } // HandleSetImagingSettings handles SetImagingSettings request. // //nolint:gocyclo // SetImagingSettings has high complexity due to multiple validation and update paths func (s *Server) HandleSetImagingSettings(body interface{}) (interface{}, error) { var req SetImagingSettingsRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get imaging state imagingMutex.Lock() defer imagingMutex.Unlock() state, ok := s.imagingState[req.VideoSourceToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrVideoSourceNotFound, req.VideoSourceToken) } // Update settings settings := req.ImagingSettings if settings == nil { // Return success if no settings to update return &SetImagingSettingsResponse{}, nil } if settings.Brightness != nil { state.Brightness = *settings.Brightness } if settings.ColorSaturation != nil { state.Saturation = *settings.ColorSaturation } if settings.Contrast != nil { state.Contrast = *settings.Contrast } if settings.Sharpness != nil { state.Sharpness = *settings.Sharpness } if settings.IrCutFilter != nil { state.IrCutFilter = *settings.IrCutFilter } if settings.BacklightCompensation != nil { state.BacklightComp.Mode = settings.BacklightCompensation.Mode if settings.BacklightCompensation.Level != nil { state.BacklightComp.Level = *settings.BacklightCompensation.Level } } if settings.Exposure != nil { state.Exposure.Mode = settings.Exposure.Mode if settings.Exposure.Priority != nil { state.Exposure.Priority = *settings.Exposure.Priority } if settings.Exposure.ExposureTime != nil { state.Exposure.ExposureTime = *settings.Exposure.ExposureTime } if settings.Exposure.Gain != nil { state.Exposure.Gain = *settings.Exposure.Gain } } if settings.Focus != nil { state.Focus.AutoFocusMode = settings.Focus.AutoFocusMode } if settings.WideDynamicRange != nil { state.WideDynamicRange.Mode = settings.WideDynamicRange.Mode if settings.WideDynamicRange.Level != nil { state.WideDynamicRange.Level = *settings.WideDynamicRange.Level } } if settings.WhiteBalance != nil { state.WhiteBalance.Mode = settings.WhiteBalance.Mode if settings.WhiteBalance.CrGain != nil { state.WhiteBalance.CrGain = *settings.WhiteBalance.CrGain } if settings.WhiteBalance.CbGain != nil { state.WhiteBalance.CbGain = *settings.WhiteBalance.CbGain } } return &SetImagingSettingsResponse{}, nil } // HandleGetOptions handles GetOptions request. func (s *Server) HandleGetOptions(body interface{}) (interface{}, error) { // Return available imaging options/capabilities const maxImagingValue = 100 //nolint:mnd // Maximum imaging parameter value const maxExposureTime = 10000 //nolint:mnd // Maximum exposure time in microseconds options := &ImagingOptions{ Brightness: &FloatRange{Min: 0, Max: maxImagingValue}, ColorSaturation: &FloatRange{Min: 0, Max: maxImagingValue}, Contrast: &FloatRange{Min: 0, Max: maxImagingValue}, Sharpness: &FloatRange{Min: 0, Max: maxImagingValue}, IrCutFilterModes: []string{"ON", "OFF", "AUTO"}, BacklightCompensation: &BacklightCompensationOptions{ Mode: []string{"OFF", "ON"}, Level: &FloatRange{Min: 0, Max: maxImagingValue}, }, Exposure: &ExposureOptions{ Mode: []string{"AUTO", "MANUAL"}, Priority: []string{"LowNoise", "FrameRate"}, MinExposureTime: &FloatRange{Min: 1, Max: maxExposureTime}, MaxExposureTime: &FloatRange{Min: 1, Max: maxExposureTime}, MinGain: &FloatRange{Min: 0, Max: maxImagingValue}, MaxGain: &FloatRange{Min: 0, Max: maxImagingValue}, ExposureTime: &FloatRange{Min: 1, Max: maxExposureTime}, Gain: &FloatRange{Min: 0, Max: maxImagingValue}, }, Focus: &FocusOptions{ AutoFocusModes: []string{"AUTO", "MANUAL"}, DefaultSpeed: &FloatRange{Min: 0, Max: 1}, NearLimit: &FloatRange{Min: 0, Max: 1}, FarLimit: &FloatRange{Min: 0, Max: 1}, }, WideDynamicRange: &WideDynamicRangeOptions{ Mode: []string{"OFF", "ON"}, Level: &FloatRange{Min: 0, Max: 100}, //nolint:mnd // Imaging parameter range }, WhiteBalance: &WhiteBalanceOptions{ Mode: []string{"AUTO", "MANUAL"}, YrGain: &FloatRange{Min: 0, Max: 255}, //nolint:mnd // White balance gain range YbGain: &FloatRange{Min: 0, Max: 255}, //nolint:mnd // White balance gain range }, } return &GetOptionsResponse{ ImagingOptions: options, }, nil } // HandleMove handles Move (focus) request. func (s *Server) HandleMove(body interface{}) (interface{}, error) { var req MoveRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get imaging state imagingMutex.Lock() defer imagingMutex.Unlock() state, ok := s.imagingState[req.VideoSourceToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrVideoSourceNotFound, req.VideoSourceToken) } // Process focus move if req.Focus != nil { if req.Focus.Absolute != nil { state.Focus.CurrentPos = req.Focus.Absolute.Position } else if req.Focus.Relative != nil { state.Focus.CurrentPos += req.Focus.Relative.Distance // Clamp to valid range if state.Focus.CurrentPos < 0 { state.Focus.CurrentPos = 0 } else if state.Focus.CurrentPos > 1 { state.Focus.CurrentPos = 1 } } // Continuous focus would start a background task } return &MoveResponse{}, nil }