package server import ( "encoding/xml" "fmt" "sync" "time" ) // PTZ service SOAP message types // ContinuousMoveRequest represents ContinuousMove request. type ContinuousMoveRequest struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl ContinuousMove"` ProfileToken string `xml:"ProfileToken"` Velocity PTZVector `xml:"Velocity"` Timeout string `xml:"Timeout,omitempty"` } // ContinuousMoveResponse represents ContinuousMove response. type ContinuousMoveResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl ContinuousMoveResponse"` } // AbsoluteMoveRequest represents AbsoluteMove request. type AbsoluteMoveRequest struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl AbsoluteMove"` ProfileToken string `xml:"ProfileToken"` Position PTZVector `xml:"Position"` Speed PTZVector `xml:"Speed,omitempty"` } // AbsoluteMoveResponse represents AbsoluteMove response. type AbsoluteMoveResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl AbsoluteMoveResponse"` } // RelativeMoveRequest represents RelativeMove request. type RelativeMoveRequest struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl RelativeMove"` ProfileToken string `xml:"ProfileToken"` Translation PTZVector `xml:"Translation"` Speed PTZVector `xml:"Speed,omitempty"` } // RelativeMoveResponse represents RelativeMove response. type RelativeMoveResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl RelativeMoveResponse"` } // StopRequest represents Stop request. type StopRequest struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl Stop"` ProfileToken string `xml:"ProfileToken"` PanTilt bool `xml:"PanTilt,omitempty"` Zoom bool `xml:"Zoom,omitempty"` } // StopResponse represents Stop response. type StopResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl StopResponse"` } // 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. type GetStatusResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GetStatusResponse"` PTZStatus *PTZStatus `xml:"PTZStatus"` } // PTZStatus represents PTZ status. type PTZStatus struct { Position PTZVector `xml:"Position"` MoveStatus PTZMoveStatus `xml:"MoveStatus"` UTCTime string `xml:"UtcTime"` } // PTZMoveStatus represents PTZ movement status. type PTZMoveStatus struct { PanTilt string `xml:"PanTilt,omitempty"` Zoom string `xml:"Zoom,omitempty"` } // PTZVector represents PTZ position/velocity. type PTZVector struct { PanTilt *Vector2D `xml:"PanTilt,omitempty"` Zoom *Vector1D `xml:"Zoom,omitempty"` } // 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. type Vector1D struct { X float64 `xml:"x,attr"` Space string `xml:"space,attr,omitempty"` } // 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. type GetPresetsResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GetPresetsResponse"` Preset []PTZPreset `xml:"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. type GotoPresetRequest struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GotoPreset"` ProfileToken string `xml:"ProfileToken"` PresetToken string `xml:"PresetToken"` Speed PTZVector `xml:"Speed,omitempty"` } // GotoPresetResponse represents GotoPreset response. type GotoPresetResponse struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl GotoPresetResponse"` } // SetPresetRequest represents SetPreset request. type SetPresetRequest struct { XMLName xml.Name `xml:"http://www.onvif.org/ver20/ptz/wsdl SetPreset"` ProfileToken string `xml:"ProfileToken"` PresetName string `xml:"PresetName,omitempty"` PresetToken string `xml:"PresetToken,omitempty"` } // 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. 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. type PTZConfigurationExt struct { Token string `xml:"token,attr"` Name string `xml:"Name"` UseCount int `xml:"UseCount"` NodeToken string `xml:"NodeToken"` PanTiltLimits *PanTiltLimits `xml:"PanTiltLimits,omitempty"` ZoomLimits *ZoomLimits `xml:"ZoomLimits,omitempty"` } // PanTiltLimits represents pan/tilt limits. type PanTiltLimits struct { Range Space2DDescription `xml:"Range"` } // ZoomLimits represents zoom limits. type ZoomLimits struct { Range Space1DDescription `xml:"Range"` } // 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. type Space1DDescription struct { URI string `xml:"URI"` XRange FloatRange `xml:"XRange"` } // FloatRange represents a float range. type FloatRange struct { Min float64 `xml:"Min"` Max float64 `xml:"Max"` } // PTZ service handlers var ptzMutex sync.RWMutex // HandleContinuousMove handles ContinuousMove request. func (s *Server) HandleContinuousMove(body interface{}) (interface{}, error) { var req ContinuousMoveRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get PTZ state ptzMutex.Lock() defer ptzMutex.Unlock() state, ok := s.ptzState[req.ProfileToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken) } // Set movement state state.Moving = true if req.Velocity.PanTilt != nil { state.PanMoving = req.Velocity.PanTilt.X != 0 || req.Velocity.PanTilt.Y != 0 state.TiltMoving = state.PanMoving } if req.Velocity.Zoom != nil { state.ZoomMoving = req.Velocity.Zoom.X != 0 } state.LastUpdate = time.Now() // In a real implementation, this would start a background task to // simulate movement and update position over time return &ContinuousMoveResponse{}, nil } // HandleAbsoluteMove handles AbsoluteMove request. func (s *Server) HandleAbsoluteMove(body interface{}) (interface{}, error) { var req AbsoluteMoveRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get PTZ state ptzMutex.Lock() defer ptzMutex.Unlock() state, ok := s.ptzState[req.ProfileToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken) } // Update position if req.Position.PanTilt != nil { state.Position.Pan = req.Position.PanTilt.X state.Position.Tilt = req.Position.PanTilt.Y } if req.Position.Zoom != nil { state.Position.Zoom = req.Position.Zoom.X } // Set moving state temporarily state.Moving = true state.PanMoving = req.Position.PanTilt != nil state.TiltMoving = req.Position.PanTilt != nil state.ZoomMoving = req.Position.Zoom != nil state.LastUpdate = time.Now() // In a real implementation, simulate movement over time // For now, we'll stop immediately go func() { time.Sleep(500 * time.Millisecond) //nolint:mnd // PTZ movement delay ptzMutex.Lock() state.Moving = false state.PanMoving = false state.TiltMoving = false state.ZoomMoving = false ptzMutex.Unlock() }() return &AbsoluteMoveResponse{}, nil } // HandleRelativeMove handles RelativeMove request. func (s *Server) HandleRelativeMove(body interface{}) (interface{}, error) { var req RelativeMoveRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get PTZ state ptzMutex.Lock() defer ptzMutex.Unlock() state, ok := s.ptzState[req.ProfileToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken) } // Update position relatively if req.Translation.PanTilt != nil { state.Position.Pan += req.Translation.PanTilt.X state.Position.Tilt += req.Translation.PanTilt.Y } if req.Translation.Zoom != nil { state.Position.Zoom += req.Translation.Zoom.X } // Clamp values to valid ranges (simplified) const maxPan = 180 // PTZ pan range const maxTilt = 90 // PTZ tilt range state.Position.Pan = clamp(state.Position.Pan, -maxPan, maxPan) state.Position.Tilt = clamp(state.Position.Tilt, -maxTilt, maxTilt) state.Position.Zoom = clamp(state.Position.Zoom, 0, 1) state.Moving = true state.LastUpdate = time.Now() // Simulate movement completion go func() { time.Sleep(500 * time.Millisecond) //nolint:mnd // PTZ movement delay ptzMutex.Lock() state.Moving = false state.PanMoving = false state.TiltMoving = false state.ZoomMoving = false ptzMutex.Unlock() }() return &RelativeMoveResponse{}, nil } // HandleStop handles Stop request. func (s *Server) HandleStop(body interface{}) (interface{}, error) { var req StopRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get PTZ state ptzMutex.Lock() defer ptzMutex.Unlock() state, ok := s.ptzState[req.ProfileToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken) } // Stop movement if req.PanTilt { state.PanMoving = false state.TiltMoving = false } if req.Zoom { state.ZoomMoving = false } if !req.PanTilt && !req.Zoom { // Stop all if neither specified state.PanMoving = false state.TiltMoving = false state.ZoomMoving = false } state.Moving = state.PanMoving || state.TiltMoving || state.ZoomMoving state.LastUpdate = time.Now() return &StopResponse{}, nil } // HandleGetStatus handles GetStatus request. func (s *Server) HandleGetStatus(body interface{}) (interface{}, error) { var req GetStatusRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Get PTZ state ptzMutex.RLock() defer ptzMutex.RUnlock() state, ok := s.ptzState[req.ProfileToken] if !ok { return nil, fmt.Errorf("%w: %s", ErrPTZNotSupported, req.ProfileToken) } // Build status response status := &PTZStatus{ Position: PTZVector{ PanTilt: &Vector2D{ X: state.Position.Pan, Y: state.Position.Tilt, Space: "http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace", }, Zoom: &Vector1D{ X: state.Position.Zoom, Space: "http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace", }, }, MoveStatus: PTZMoveStatus{ PanTilt: getMoveStatusString(state.PanMoving || state.TiltMoving), Zoom: getMoveStatusString(state.ZoomMoving), }, UTCTime: time.Now().UTC().Format(time.RFC3339), } return &GetStatusResponse{ PTZStatus: status, }, nil } // HandleGetPresets handles GetPresets request. func (s *Server) HandleGetPresets(body interface{}) (interface{}, error) { var req GetPresetsRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Find the profile configuration var profileCfg *ProfileConfig 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("%w: %s", ErrPTZNotSupported, req.ProfileToken) } // Build presets response presets := make([]PTZPreset, len(profileCfg.PTZ.Presets)) for i, preset := range profileCfg.PTZ.Presets { presets[i] = PTZPreset{ Token: preset.Token, Name: preset.Name, PTZPosition: &PTZVector{ PanTilt: &Vector2D{ X: preset.Position.Pan, Y: preset.Position.Tilt, }, Zoom: &Vector1D{ X: preset.Position.Zoom, }, }, } } return &GetPresetsResponse{ Preset: presets, }, nil } // HandleGotoPreset handles GotoPreset request. func (s *Server) HandleGotoPreset(body interface{}) (interface{}, error) { var req GotoPresetRequest if err := unmarshalBody(body, &req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Find the profile configuration var profileCfg *ProfileConfig 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("%w: %s", ErrPTZNotSupported, req.ProfileToken) } // Find the preset var presetPos *PTZPosition for _, preset := range profileCfg.PTZ.Presets { if preset.Token == req.PresetToken { presetPos = &preset.Position break } } if presetPos == nil { return nil, fmt.Errorf("%w: %s", ErrPresetNotFound, req.PresetToken) } // Get PTZ state and move to preset ptzMutex.Lock() defer ptzMutex.Unlock() state := s.ptzState[req.ProfileToken] state.Position = *presetPos state.Moving = true state.PanMoving = true state.TiltMoving = true state.ZoomMoving = true state.LastUpdate = time.Now() // Simulate movement completion go func() { time.Sleep(1 * time.Second) ptzMutex.Lock() state.Moving = false state.PanMoving = false state.TiltMoving = false state.ZoomMoving = false ptzMutex.Unlock() }() return &GotoPresetResponse{}, nil } // Helper functions func getMoveStatusString(moving bool) string { if moving { return "MOVING" } return "IDLE" } func clamp(value, minVal, maxVal float64) float64 { if value < minVal { return minVal } if value > maxVal { return maxVal } return value }