Files
onvif-go/server/media.go
T
ProtoTess b4e4982876 Refactor XML response handling in device extended and security tests
- Adjusted formatting in XML response strings for consistency in device_extended_test.go and device_security_test.go.
- Improved readability by aligning XML declaration and body content.
- Updated mock server responses to ensure proper handling of various ONVIF operations.

Enhance device security and storage handling

- Refactored struct field declarations in device_security.go and device_storage_test.go for improved clarity.
- Ensured consistent formatting across struct definitions and XML tags.

Standardize whitespace and formatting across multiple files

- Removed unnecessary blank lines and adjusted indentation in discovery, imaging, media, and PTZ server files.
- Improved overall code readability and maintainability by ensuring consistent formatting.

Update example applications for better readability

- Cleaned up whitespace in example applications to enhance code clarity.
- Ensured consistent formatting in main.go files across various examples.

Refactor server and SOAP handler code for consistency

- Standardized struct field declarations and XML tag formatting in server and SOAP handler files.
- Improved readability by aligning struct fields and ensuring consistent use of whitespace.

General code cleanup and formatting adjustments

- Applied consistent formatting across various files, including types.go and test files.
- Enhanced readability by aligning struct fields and removing unnecessary blank lines.
2025-12-01 00:49:36 +00:00

376 lines
12 KiB
Go

package server
import (
"encoding/xml"
"fmt"
)
// Media service SOAP message types
// 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
type MediaProfile struct {
Token string `xml:"token,attr"`
Fixed bool `xml:"fixed,attr"`
Name string `xml:"Name"`
VideoSourceConfiguration *VideoSourceConfiguration `xml:"VideoSourceConfiguration"`
AudioSourceConfiguration *AudioSourceConfiguration `xml:"AudioSourceConfiguration,omitempty"`
VideoEncoderConfiguration *VideoEncoderConfiguration `xml:"VideoEncoderConfiguration"`
AudioEncoderConfiguration *AudioEncoderConfiguration `xml:"AudioEncoderConfiguration,omitempty"`
VideoAnalyticsConfiguration *VideoAnalyticsConfiguration `xml:"VideoAnalyticsConfiguration,omitempty"`
PTZConfiguration *PTZConfiguration `xml:"PTZConfiguration,omitempty"`
MetadataConfiguration *MetadataConfiguration `xml:"MetadataConfiguration,omitempty"`
}
// VideoSourceConfiguration represents video source configuration
type VideoSourceConfiguration struct {
Token string `xml:"token,attr"`
Name string `xml:"Name"`
UseCount int `xml:"UseCount"`
SourceToken string `xml:"SourceToken"`
Bounds IntRectangle `xml:"Bounds"`
}
// AudioSourceConfiguration represents audio source configuration
type AudioSourceConfiguration struct {
Token string `xml:"token,attr"`
Name string `xml:"Name"`
UseCount int `xml:"UseCount"`
SourceToken string `xml:"SourceToken"`
}
// VideoEncoderConfiguration represents video encoder configuration
type VideoEncoderConfiguration struct {
Token string `xml:"token,attr"`
Name string `xml:"Name"`
UseCount int `xml:"UseCount"`
Encoding string `xml:"Encoding"`
Resolution VideoResolution `xml:"Resolution"`
Quality float64 `xml:"Quality"`
RateControl *VideoRateControl `xml:"RateControl,omitempty"`
H264 *H264Configuration `xml:"H264,omitempty"`
Multicast *MulticastConfiguration `xml:"Multicast,omitempty"`
SessionTimeout string `xml:"SessionTimeout"`
}
// AudioEncoderConfiguration represents audio encoder configuration
type AudioEncoderConfiguration struct {
Token string `xml:"token,attr"`
Name string `xml:"Name"`
UseCount int `xml:"UseCount"`
Encoding string `xml:"Encoding"`
Bitrate int `xml:"Bitrate"`
SampleRate int `xml:"SampleRate"`
Multicast *MulticastConfiguration `xml:"Multicast,omitempty"`
SessionTimeout string `xml:"SessionTimeout"`
}
// 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
type PTZConfiguration struct {
Token string `xml:"token,attr"`
Name string `xml:"Name"`
UseCount int `xml:"UseCount"`
NodeToken string `xml:"NodeToken"`
}
// MetadataConfiguration represents metadata configuration
type MetadataConfiguration struct {
Token string `xml:"token,attr"`
Name string `xml:"Name"`
UseCount int `xml:"UseCount"`
SessionTimeout string `xml:"SessionTimeout"`
}
// IntRectangle represents a rectangle with integer coordinates
type IntRectangle struct {
X int `xml:"x,attr"`
Y int `xml:"y,attr"`
Width int `xml:"width,attr"`
Height int `xml:"height,attr"`
}
// VideoResolution represents video resolution
type VideoResolution struct {
Width int `xml:"Width"`
Height int `xml:"Height"`
}
// VideoRateControl represents video rate control
type VideoRateControl struct {
FrameRateLimit int `xml:"FrameRateLimit"`
EncodingInterval int `xml:"EncodingInterval"`
BitrateLimit int `xml:"BitrateLimit"`
}
// H264Configuration represents H264 configuration
type H264Configuration struct {
GovLength int `xml:"GovLength"`
H264Profile string `xml:"H264Profile"`
}
// MulticastConfiguration represents multicast configuration
type MulticastConfiguration struct {
Address IPAddress `xml:"Address"`
Port int `xml:"Port"`
TTL int `xml:"TTL"`
AutoStart bool `xml:"AutoStart"`
}
// 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
type GetStreamURIResponse struct {
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetStreamURIResponse"`
MediaUri MediaUri `xml:"MediaUri"`
}
// MediaUri represents a media URI
type MediaUri struct {
Uri string `xml:"Uri"`
InvalidAfterConnect bool `xml:"InvalidAfterConnect"`
InvalidAfterReboot bool `xml:"InvalidAfterReboot"`
Timeout string `xml:"Timeout"`
}
// 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
type GetVideoSourcesResponse struct {
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetVideoSourcesResponse"`
VideoSources []VideoSource `xml:"VideoSources"`
}
// VideoSource represents a video source
type VideoSource struct {
Token string `xml:"token,attr"`
Framerate float64 `xml:"Framerate"`
Resolution VideoResolution `xml:"Resolution"`
}
// Media service handlers
// HandleGetProfiles handles GetProfiles request
func (s *Server) HandleGetProfiles(body interface{}) (interface{}, error) {
profiles := make([]MediaProfile, len(s.config.Profiles))
for i, profileCfg := range s.config.Profiles {
profile := MediaProfile{
Token: profileCfg.Token,
Fixed: true,
Name: profileCfg.Name,
VideoSourceConfiguration: &VideoSourceConfiguration{
Token: profileCfg.VideoSource.Token,
Name: profileCfg.VideoSource.Name,
UseCount: 1,
SourceToken: profileCfg.VideoSource.Token,
Bounds: IntRectangle{
X: profileCfg.VideoSource.Bounds.X,
Y: profileCfg.VideoSource.Bounds.Y,
Width: profileCfg.VideoSource.Bounds.Width,
Height: profileCfg.VideoSource.Bounds.Height,
},
},
VideoEncoderConfiguration: &VideoEncoderConfiguration{
Token: profileCfg.Token + "_encoder",
Name: profileCfg.Name + " Encoder",
UseCount: 1,
Encoding: profileCfg.VideoEncoder.Encoding,
Resolution: VideoResolution{
Width: profileCfg.VideoEncoder.Resolution.Width,
Height: profileCfg.VideoEncoder.Resolution.Height,
},
Quality: profileCfg.VideoEncoder.Quality,
RateControl: &VideoRateControl{
FrameRateLimit: profileCfg.VideoEncoder.Framerate,
EncodingInterval: 1,
BitrateLimit: profileCfg.VideoEncoder.Bitrate,
},
SessionTimeout: "PT60S",
},
}
// Add H264 configuration if encoding is H264
if profileCfg.VideoEncoder.Encoding == "H264" {
profile.VideoEncoderConfiguration.H264 = &H264Configuration{
GovLength: profileCfg.VideoEncoder.GovLength,
H264Profile: "Main",
}
}
// Add audio configuration if present
if profileCfg.AudioSource != nil {
profile.AudioSourceConfiguration = &AudioSourceConfiguration{
Token: profileCfg.AudioSource.Token,
Name: profileCfg.AudioSource.Name,
UseCount: 1,
SourceToken: profileCfg.AudioSource.Token,
}
}
if profileCfg.AudioEncoder != nil {
profile.AudioEncoderConfiguration = &AudioEncoderConfiguration{
Token: profileCfg.Token + "_audio_encoder",
Name: profileCfg.Name + " Audio Encoder",
UseCount: 1,
Encoding: profileCfg.AudioEncoder.Encoding,
Bitrate: profileCfg.AudioEncoder.Bitrate,
SampleRate: profileCfg.AudioEncoder.SampleRate,
SessionTimeout: "PT60S",
}
}
// Add PTZ configuration if present
if profileCfg.PTZ != nil {
profile.PTZConfiguration = &PTZConfiguration{
Token: profileCfg.PTZ.NodeToken,
Name: profileCfg.Name + " PTZ",
UseCount: 1,
NodeToken: profileCfg.PTZ.NodeToken,
}
}
profiles[i] = profile
}
return &GetProfilesResponse{
Profiles: profiles,
}, nil
}
// HandleGetStreamURI handles GetStreamURI request
func (s *Server) HandleGetStreamURI(body interface{}) (interface{}, error) {
var req struct {
ProfileToken string `xml:"ProfileToken"`
}
if err := unmarshalBody(body, &req); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
// 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)
}
// Build RTSP URI
uri := streamCfg.StreamURI
if uri == "" {
// Default URI construction
host := s.config.Host
if host == "0.0.0.0" || host == "" {
host = "localhost"
}
uri = fmt.Sprintf("rtsp://%s:8554%s", host, streamCfg.RTSPPath)
}
return &GetStreamURIResponse{
MediaUri: MediaUri{
Uri: uri,
InvalidAfterConnect: false,
InvalidAfterReboot: true,
Timeout: "PT60S",
},
}, nil
}
// HandleGetSnapshotURI handles GetSnapshotURI request
func (s *Server) HandleGetSnapshotURI(body interface{}) (interface{}, error) {
var req struct {
ProfileToken string `xml:"ProfileToken"`
}
if err := unmarshalBody(body, &req); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
// Find the profile
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 {
return nil, fmt.Errorf("profile not found: %s", req.ProfileToken)
}
if !profileCfg.Snapshot.Enabled {
return nil, fmt.Errorf("snapshot not supported for profile: %s", req.ProfileToken)
}
// Build snapshot URI
host := s.config.Host
if host == "0.0.0.0" || host == "" {
host = "localhost"
}
uri := fmt.Sprintf("http://%s:%d%s/snapshot?profile=%s",
host, s.config.Port, s.config.BasePath, req.ProfileToken)
return &GetSnapshotURIResponse{
MediaUri: MediaUri{
Uri: uri,
InvalidAfterConnect: false,
InvalidAfterReboot: true,
Timeout: "PT5S",
},
}, nil
}
// 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)
for _, profileCfg := range s.config.Profiles {
if !seenSources[profileCfg.VideoSource.Token] {
sources = append(sources, VideoSource{
Token: profileCfg.VideoSource.Token,
Framerate: float64(profileCfg.VideoSource.Framerate),
Resolution: VideoResolution{
Width: profileCfg.VideoSource.Resolution.Width,
Height: profileCfg.VideoSource.Resolution.Height,
},
})
seenSources[profileCfg.VideoSource.Token] = true
}
}
return &GetVideoSourcesResponse{
VideoSources: sources,
}, nil
}
// unmarshalBody is a helper to unmarshal SOAP body content
func unmarshalBody(body interface{}, target interface{}) error {
bodyXML, err := xml.Marshal(body)
if err != nil {
return err
}
return xml.Unmarshal(bodyXML, target)
}