From 1f68023dbe68c07ab48095c707072a986ac6f91c Mon Sep 17 00:00:00 2001 From: 0x524a Date: Mon, 1 Dec 2025 23:26:46 -0500 Subject: [PATCH] feat: enhance media service capabilities and add comprehensive tests - Implemented methods to retrieve media service capabilities and video encoder configuration options. - Added new types for media service capabilities, video encoder options, and audio configurations. - Introduced unit tests for media service operations, including GetProfiles, GetProfile, and GetStreamURI, ensuring proper functionality and response validation. - Improved error handling and response parsing in media-related methods. --- client.go | 75 ++- client_test.go | 505 ++++++++++++++ media.go | 1723 ++++++++++++++++++++++++++++++++++++++++++++++++ media_test.go | 1490 +++++++++++++++++++++++++++++++++++++++++ types.go | 101 +++ 5 files changed, 3863 insertions(+), 31 deletions(-) create mode 100644 media_test.go diff --git a/client.go b/client.go index 0cbd83e..26a25bf 100644 --- a/client.go +++ b/client.go @@ -3,7 +3,9 @@ package onvif import ( "context" "crypto/md5" + "crypto/rand" "crypto/tls" + "encoding/hex" "fmt" "io" "net" @@ -14,6 +16,20 @@ import ( "time" ) +// Default client configuration constants +const ( + // DefaultTimeout is the default HTTP client timeout + DefaultTimeout = 30 * time.Second + // DefaultIdleConnTimeout is the default idle connection timeout + DefaultIdleConnTimeout = 90 * time.Second + // DefaultMaxIdleConns is the default maximum idle connections + DefaultMaxIdleConns = 10 + // DefaultMaxIdleConnsPerHost is the default maximum idle connections per host + DefaultMaxIdleConnsPerHost = 5 + // NonceSize is the size of the nonce for digest authentication + NonceSize = 16 +) + // Client represents an ONVIF client for communicating with IP cameras type Client struct { endpoint string @@ -82,11 +98,11 @@ func NewClient(endpoint string, opts ...ClientOption) (*Client, error) { client := &Client{ endpoint: normalizedEndpoint, httpClient: &http.Client{ - Timeout: 30 * time.Second, + Timeout: DefaultTimeout, Transport: &http.Transport{ - MaxIdleConns: 10, - MaxIdleConnsPerHost: 5, - IdleConnTimeout: 90 * time.Second, + MaxIdleConns: DefaultMaxIdleConns, + MaxIdleConnsPerHost: DefaultMaxIdleConnsPerHost, + IdleConnTimeout: DefaultIdleConnTimeout, }, // Don't follow redirects automatically // This prevents http:// from being silently upgraded to https:// @@ -277,24 +293,21 @@ func (c *Client) downloadWithBasicAuth(ctx context.Context, downloadURL string) bodyStr = bodyStr[:200] + "..." } + // Base error message for programmatic use errorMsg := fmt.Sprintf("download failed with status code %d", resp.StatusCode) + // Add structured error details switch resp.StatusCode { case http.StatusUnauthorized: - errorMsg += "\n ❌ Authentication failed (401 Unauthorized)" - errorMsg += "\n 💡 Basic auth failed; trying digest auth..." + errorMsg += ": authentication failed (401 Unauthorized); basic auth failed, trying digest auth" case http.StatusForbidden: - errorMsg += "\n ❌ Access denied (403 Forbidden)" - errorMsg += "\n 💡 User may not have permission to download snapshots" - errorMsg += "\n 💡 Check camera user role/permissions" + errorMsg += ": access denied (403 Forbidden); user may not have permission to download snapshots" case http.StatusNotFound: - errorMsg += "\n ❌ Snapshot URI not found (404)" - errorMsg += "\n 💡 Camera may have revoked the URI" - errorMsg += "\n 💡 Try getting a fresh snapshot URI" + errorMsg += ": snapshot URI not found (404); camera may have revoked the URI, try getting a fresh snapshot URI" } - if bodyStr != "" && resp.StatusCode != http.StatusOK { - errorMsg += fmt.Sprintf("\n 📝 Response: %s", bodyStr) + if bodyStr != "" { + errorMsg += fmt.Sprintf("; response: %s", bodyStr) } return nil, fmt.Errorf("%s", errorMsg) @@ -317,12 +330,12 @@ func (c *Client) downloadWithDigestAuth(ctx context.Context, downloadURL string) // Create a custom transport with digest auth tr := &http.Transport{ Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + Timeout: DefaultTimeout, + KeepAlive: DefaultTimeout, }).Dial, - MaxIdleConns: 10, - MaxIdleConnsPerHost: 5, - IdleConnTimeout: 90 * time.Second, + MaxIdleConns: DefaultMaxIdleConns, + MaxIdleConnsPerHost: DefaultMaxIdleConnsPerHost, + IdleConnTimeout: DefaultIdleConnTimeout, } // Create a custom HTTP client for digest auth @@ -332,7 +345,7 @@ func (c *Client) downloadWithDigestAuth(ctx context.Context, downloadURL string) username: c.username, password: c.password, }, - Timeout: 30 * time.Second, + Timeout: DefaultTimeout, } req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) @@ -360,20 +373,15 @@ func (c *Client) downloadWithDigestAuth(ctx context.Context, downloadURL string) switch resp.StatusCode { case http.StatusUnauthorized: - errorMsg += "\n ❌ Digest authentication failed (401 Unauthorized)" - errorMsg += "\n 💡 Check camera credentials (username/password)" - errorMsg += "\n 💡 Try accessing the snapshot URL manually:" - errorMsg += fmt.Sprintf("\n curl --digest -u username:password '%s'", downloadURL) + errorMsg += ": digest authentication failed (401 Unauthorized); check camera credentials (username/password)" case http.StatusForbidden: - errorMsg += "\n ❌ Access denied (403 Forbidden)" - errorMsg += "\n 💡 User may not have permission to download snapshots" + errorMsg += ": access denied (403 Forbidden); user may not have permission to download snapshots" case http.StatusNotFound: - errorMsg += "\n ❌ Snapshot URI not found (404)" - errorMsg += "\n 💡 Try getting a fresh snapshot URI" + errorMsg += ": snapshot URI not found (404); try getting a fresh snapshot URI" } if bodyStr != "" { - errorMsg += fmt.Sprintf("\n 📝 Response: %s", bodyStr) + errorMsg += fmt.Sprintf("; response: %s", bodyStr) } return nil, fmt.Errorf("%s", errorMsg) @@ -493,7 +501,12 @@ func md5sum(s string) interface{} { return h.Sum(nil) } +// generateNonce generates a cryptographically secure random nonce for digest authentication func generateNonce() string { - // Generate a simple nonce - return fmt.Sprintf("%d", time.Now().UnixNano()) + bytes := make([]byte, NonceSize) + if _, err := rand.Read(bytes); err != nil { + // Fallback to time-based nonce if crypto/rand fails (shouldn't happen) + return fmt.Sprintf("%d", time.Now().UnixNano()) + } + return hex.EncodeToString(bytes) } diff --git a/client_test.go b/client_test.go index b3bacfb..7a58c92 100644 --- a/client_test.go +++ b/client_test.go @@ -2,7 +2,9 @@ package onvif import ( "context" + "encoding/hex" "fmt" + "net" "net/http" "net/http/httptest" "net/url" @@ -794,3 +796,506 @@ func TestInitializeWithLocalhostURLs(t *testing.T) { t.Errorf("Imaging endpoint still contains 0.0.0.0: %v", client.imagingEndpoint) } } + +// TestDownloadFileWithBasicAuth tests DownloadFile with basic authentication +func TestDownloadFileWithBasicAuth(t *testing.T) { + // Create a mock server that requires basic auth + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != "admin" || password != "password" { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "image/jpeg") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("fake image data")) + })) + defer server.Close() + + client, err := NewClient( + server.URL, + WithCredentials("admin", "password"), + ) + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + data, err := client.DownloadFile(ctx, server.URL) + if err != nil { + t.Fatalf("DownloadFile() failed: %v", err) + } + + if string(data) != "fake image data" { + t.Errorf("DownloadFile() = %q, want %q", string(data), "fake image data") + } +} + +// TestDownloadFileWithDigestAuth tests DownloadFile with digest authentication +func TestDownloadFileWithDigestAuth(t *testing.T) { + nonce := "test-nonce-12345" + realm := "test-realm" + opaque := "test-opaque" + + // Create a mock server that requires digest auth + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Digest ") { + // First request - return 401 with digest challenge + w.Header().Set("WWW-Authenticate", fmt.Sprintf( + `Digest realm="%s", nonce="%s", opaque="%s", qop="auth"`, + realm, nonce, opaque)) + w.WriteHeader(http.StatusUnauthorized) + return + } + // Second request with auth - accept it + w.Header().Set("Content-Type", "image/jpeg") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("fake image data with digest")) + })) + defer server.Close() + + client, err := NewClient( + server.URL, + WithCredentials("admin", "password"), + ) + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + data, err := client.DownloadFile(ctx, server.URL) + if err != nil { + t.Fatalf("DownloadFile() failed: %v", err) + } + + if string(data) != "fake image data with digest" { + t.Errorf("DownloadFile() = %q, want %q", string(data), "fake image data with digest") + } +} + +// TestDownloadFileUnauthorized tests DownloadFile with invalid credentials +func TestDownloadFileUnauthorized(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + })) + defer server.Close() + + client, err := NewClient( + server.URL, + WithCredentials("wrong", "wrong"), + ) + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + _, err = client.DownloadFile(ctx, server.URL) + if err == nil { + t.Error("DownloadFile() expected error for unauthorized request") + } + if !strings.Contains(err.Error(), "401") { + t.Errorf("Expected 401 error, got: %v", err) + } +} + +// TestDownloadFileNotFound tests DownloadFile with 404 response +func TestDownloadFileNotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + })) + defer server.Close() + + client, err := NewClient(server.URL) + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + _, err = client.DownloadFile(ctx, server.URL) + if err == nil { + t.Error("DownloadFile() expected error for 404 response") + } + if !strings.Contains(err.Error(), "404") { + t.Errorf("Expected 404 error, got: %v", err) + } +} + +// TestDownloadFileForbidden tests DownloadFile with 403 response +func TestDownloadFileForbidden(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + })) + defer server.Close() + + client, err := NewClient(server.URL) + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + _, err = client.DownloadFile(ctx, server.URL) + if err == nil { + t.Error("DownloadFile() expected error for 403 response") + } + if !strings.Contains(err.Error(), "403") { + t.Errorf("Expected 403 error, got: %v", err) + } +} + +// TestDownloadFileNetworkError tests DownloadFile with network error +func TestDownloadFileNetworkError(t *testing.T) { + client, err := NewClient("http://192.168.999.999/onvif") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + _, err = client.DownloadFile(ctx, "http://192.168.999.999/nonexistent") + if err == nil { + t.Error("DownloadFile() expected error for network failure") + } +} + +// TestDigestAuthTransport tests the digest authentication transport +func TestDigestAuthTransport(t *testing.T) { + nonce := "test-nonce" + realm := "test-realm" + opaque := "test-opaque" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Digest ") { + w.Header().Set("WWW-Authenticate", fmt.Sprintf( + `Digest realm="%s", nonce="%s", opaque="%s", qop="auth"`, + realm, nonce, opaque)) + w.WriteHeader(http.StatusUnauthorized) + return + } + // Verify digest auth header contains required fields + if !strings.Contains(authHeader, `username="admin"`) { + t.Error("Digest auth header missing username") + } + if !strings.Contains(authHeader, `realm="`+realm+`"`) { + t.Error("Digest auth header missing realm") + } + if !strings.Contains(authHeader, `nonce="`+nonce+`"`) { + t.Error("Digest auth header missing nonce") + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + })) + defer server.Close() + + tr := &http.Transport{ + Dial: (&net.Dialer{ + Timeout: DefaultTimeout, + KeepAlive: DefaultTimeout, + }).Dial, + } + + digestClient := &http.Client{ + Transport: &digestAuthTransport{ + transport: tr, + username: "admin", + password: "password", + }, + Timeout: DefaultTimeout, + } + + req, err := http.NewRequest("GET", server.URL, nil) + if err != nil { + t.Fatalf("NewRequest() failed: %v", err) + } + + resp, err := digestClient.Do(req) + if err != nil { + t.Fatalf("Do() failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected 200, got %d", resp.StatusCode) + } +} + +// TestExtractParam tests the extractParam helper function +func TestExtractParam(t *testing.T) { + tests := []struct { + name string + authHeader string + param string + expected string + }{ + { + name: "extract realm", + authHeader: `Digest realm="test-realm", nonce="123"`, + param: "realm", + expected: "test-realm", + }, + { + name: "extract nonce", + authHeader: `Digest realm="test", nonce="abc123"`, + param: "nonce", + expected: "abc123", + }, + { + name: "extract qop", + authHeader: `Digest realm="test", qop="auth"`, + param: "qop", + expected: "auth", + }, + { + name: "missing param", + authHeader: `Digest realm="test"`, + param: "nonce", + expected: "", + }, + { + name: "empty header", + authHeader: "", + param: "realm", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractParam(tt.authHeader, tt.param) + if result != tt.expected { + t.Errorf("extractParam() = %q, want %q", result, tt.expected) + } + }) + } +} + +// TestGenerateNonce tests nonce generation +func TestGenerateNonce(t *testing.T) { + // Generate multiple nonces and verify they're different and valid hex + nonces := make(map[string]bool) + for i := 0; i < 10; i++ { + nonce := generateNonce() + if len(nonce) != NonceSize*2 { // hex encoding doubles the length + t.Errorf("generateNonce() length = %d, want %d", len(nonce), NonceSize*2) + } + // Verify it's valid hex + _, err := hex.DecodeString(nonce) + if err != nil { + t.Errorf("generateNonce() returned invalid hex: %v", err) + } + nonces[nonce] = true + } + + // Verify nonces are unique (very unlikely to collide with crypto/rand) + if len(nonces) < 10 { + t.Error("generateNonce() generated duplicate nonces") + } +} + +// TestMd5Hash tests MD5 hash function +func TestMd5Hash(t *testing.T) { + tests := []struct { + name string + input string + expected string // Expected MD5 hash in hex + }{ + { + name: "empty string", + input: "", + expected: "d41d8cd98f00b204e9800998ecf8427e", + }, + { + name: "simple string", + input: "test", + expected: "098f6bcd4621d373cade4e832627b4f6", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := md5Hash(tt.input) + if result != tt.expected { + t.Errorf("md5Hash(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +// TestErrorTypes tests error type checking +func TestErrorTypes(t *testing.T) { + t.Run("IsONVIFError with ONVIFError", func(t *testing.T) { + err := NewONVIFError("Sender", "InvalidArgs", "test message") + if !IsONVIFError(err) { + t.Error("IsONVIFError() returned false for ONVIFError") + } + }) + + t.Run("IsONVIFError with regular error", func(t *testing.T) { + err := fmt.Errorf("regular error") + if IsONVIFError(err) { + t.Error("IsONVIFError() returned true for regular error") + } + }) + + t.Run("IsONVIFError with wrapped ONVIFError", func(t *testing.T) { + onvifErr := NewONVIFError("Sender", "InvalidArgs", "test") + wrappedErr := fmt.Errorf("wrapped: %w", onvifErr) + if !IsONVIFError(wrappedErr) { + t.Error("IsONVIFError() returned false for wrapped ONVIFError") + } + }) +} + +// TestClientConcurrency tests concurrent access to client +func TestClientConcurrency(t *testing.T) { + client, err := NewClient("http://192.168.1.100/onvif") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + // Test concurrent credential access + done := make(chan bool) + for i := 0; i < 10; i++ { + go func(id int) { + client.SetCredentials(fmt.Sprintf("user%d", id), "pass") + user, pass := client.GetCredentials() + if user == "" || pass == "" { + t.Error("Concurrent credential access failed") + } + done <- true + }(i) + } + + // Wait for all goroutines + for i := 0; i < 10; i++ { + <-done + } +} + +// TestNormalizeEndpointErrorCases tests error cases for normalizeEndpoint +func TestNormalizeEndpointErrorCases(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "empty string", + input: "", + wantErr: true, + }, + { + name: "invalid URL", + input: "://invalid", + wantErr: false, // normalizeEndpoint treats this as IP without scheme + }, + { + name: "URL with empty host", + input: "http:///path", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := normalizeEndpoint(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("normalizeEndpoint() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// TestFixLocalhostURLEdgeCases tests edge cases for fixLocalhostURL +func TestFixLocalhostURLEdgeCases(t *testing.T) { + tests := []struct { + name string + clientURL string + serviceURL string + expectedURL string + }{ + { + name: "invalid service URL", + clientURL: "http://192.168.1.100/onvif", + serviceURL: "://invalid", + expectedURL: "://invalid", // Should return original on parse error + }, + { + name: "invalid client URL", + clientURL: "://invalid", + serviceURL: "http://localhost/path", + expectedURL: "http://localhost/path", // Should return original on parse error + }, + { + name: "service URL with query params", + clientURL: "http://192.168.1.100/onvif", + serviceURL: "http://localhost/path?param=value", + expectedURL: "http://192.168.1.100/path?param=value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := &Client{ + endpoint: tt.clientURL, + } + + result := client.fixLocalhostURL(tt.serviceURL) + if result != tt.expectedURL { + t.Errorf("fixLocalhostURL() = %q, want %q", result, tt.expectedURL) + } + }) + } +} + +// TestWithInsecureSkipVerify tests the WithInsecureSkipVerify option +func TestWithInsecureSkipVerify(t *testing.T) { + client, err := NewClient( + "https://192.168.1.100/onvif", + WithInsecureSkipVerify(), + ) + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + transport, ok := client.httpClient.Transport.(*http.Transport) + if !ok { + t.Fatal("Transport is not *http.Transport") + } + + if transport.TLSClientConfig == nil { + t.Error("TLSClientConfig is nil") + } else if !transport.TLSClientConfig.InsecureSkipVerify { + t.Error("InsecureSkipVerify is not set") + } +} + +// TestDownloadFileContextCancellation tests context cancellation +func TestDownloadFileContextCancellation(t *testing.T) { + // Create a slow server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Second) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("data")) + })) + defer server.Close() + + client, err := NewClient(server.URL) + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + _, err = client.DownloadFile(ctx, server.URL) + if err == nil { + t.Error("DownloadFile() expected error for cancelled context") + } + if !strings.Contains(err.Error(), "context deadline exceeded") && !strings.Contains(err.Error(), "context canceled") { + t.Errorf("Expected context error, got: %v", err) + } +} \ No newline at end of file diff --git a/media.go b/media.go index afe90e7..3660df2 100644 --- a/media.go +++ b/media.go @@ -598,3 +598,1726 @@ func (c *Client) SetVideoEncoderConfiguration(ctx context.Context, config *Video return nil } + +// GetMediaServiceCapabilities retrieves media service capabilities +func (c *Client) GetMediaServiceCapabilities(ctx context.Context) (*MediaServiceCapabilities, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetServiceCapabilities struct { + XMLName xml.Name `xml:"trt:GetServiceCapabilities"` + Xmlns string `xml:"xmlns:trt,attr"` + } + + type GetServiceCapabilitiesResponse struct { + XMLName xml.Name `xml:"GetServiceCapabilitiesResponse"` + Capabilities struct { + SnapshotUri bool `xml:"SnapshotUri,attr"` + Rotation bool `xml:"Rotation,attr"` + VideoSourceMode bool `xml:"VideoSourceMode,attr"` + OSD bool `xml:"OSD,attr"` + TemporaryOSDText bool `xml:"TemporaryOSDText,attr"` + EXICompression bool `xml:"EXICompression,attr"` + ProfileCapabilities *struct { + MaximumNumberOfProfiles int `xml:"MaximumNumberOfProfiles,attr"` + } `xml:"ProfileCapabilities"` + StreamingCapabilities *struct { + RTPMulticast bool `xml:"RTPMulticast,attr"` + RTP_TCP bool `xml:"RTP_TCP,attr"` + RTP_RTSP_TCP bool `xml:"RTP_RTSP_TCP,attr"` + } `xml:"StreamingCapabilities"` + } `xml:"Capabilities"` + } + + req := GetServiceCapabilities{ + Xmlns: mediaNamespace, + } + + var resp GetServiceCapabilitiesResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetMediaServiceCapabilities failed: %w", err) + } + + caps := &MediaServiceCapabilities{ + SnapshotUri: resp.Capabilities.SnapshotUri, + Rotation: resp.Capabilities.Rotation, + VideoSourceMode: resp.Capabilities.VideoSourceMode, + OSD: resp.Capabilities.OSD, + TemporaryOSDText: resp.Capabilities.TemporaryOSDText, + EXICompression: resp.Capabilities.EXICompression, + } + + if resp.Capabilities.ProfileCapabilities != nil { + caps.MaximumNumberOfProfiles = resp.Capabilities.ProfileCapabilities.MaximumNumberOfProfiles + } + + if resp.Capabilities.StreamingCapabilities != nil { + caps.RTPMulticast = resp.Capabilities.StreamingCapabilities.RTPMulticast + caps.RTP_TCP = resp.Capabilities.StreamingCapabilities.RTP_TCP + caps.RTP_RTSP_TCP = resp.Capabilities.StreamingCapabilities.RTP_RTSP_TCP + } + + return caps, nil +} + +// GetVideoEncoderConfigurationOptions retrieves available options for video encoder configuration +func (c *Client) GetVideoEncoderConfigurationOptions(ctx context.Context, configurationToken string) (*VideoEncoderConfigurationOptions, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetVideoEncoderConfigurationOptions struct { + XMLName xml.Name `xml:"trt:GetVideoEncoderConfigurationOptions"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken,omitempty"` + ProfileToken string `xml:"trt:ProfileToken,omitempty"` + } + + type GetVideoEncoderConfigurationOptionsResponse struct { + XMLName xml.Name `xml:"GetVideoEncoderConfigurationOptionsResponse"` + Options struct { + QualityRange *struct { + Min float64 `xml:"Min"` + Max float64 `xml:"Max"` + } `xml:"QualityRange"` + JPEG *struct { + ResolutionsAvailable []struct { + Width int `xml:"Width"` + Height int `xml:"Height"` + } `xml:"ResolutionsAvailable"` + FrameRateRange *struct { + Min float64 `xml:"Min"` + Max float64 `xml:"Max"` + } `xml:"FrameRateRange"` + EncodingIntervalRange *struct { + Min int `xml:"Min"` + Max int `xml:"Max"` + } `xml:"EncodingIntervalRange"` + } `xml:"JPEG"` + H264 *struct { + ResolutionsAvailable []struct { + Width int `xml:"Width"` + Height int `xml:"Height"` + } `xml:"ResolutionsAvailable"` + GovLengthRange *struct { + Min int `xml:"Min"` + Max int `xml:"Max"` + } `xml:"GovLengthRange"` + FrameRateRange *struct { + Min float64 `xml:"Min"` + Max float64 `xml:"Max"` + } `xml:"FrameRateRange"` + EncodingIntervalRange *struct { + Min int `xml:"Min"` + Max int `xml:"Max"` + } `xml:"EncodingIntervalRange"` + H264ProfilesSupported []string `xml:"H264ProfilesSupported"` + } `xml:"H264"` + Extension struct{} `xml:"Extension"` + } `xml:"Options"` + } + + req := GetVideoEncoderConfigurationOptions{ + Xmlns: mediaNamespace, + } + if configurationToken != "" { + req.ConfigurationToken = configurationToken + } + + var resp GetVideoEncoderConfigurationOptionsResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetVideoEncoderConfigurationOptions failed: %w", err) + } + + options := &VideoEncoderConfigurationOptions{} + + if resp.Options.QualityRange != nil { + options.QualityRange = &FloatRange{ + Min: resp.Options.QualityRange.Min, + Max: resp.Options.QualityRange.Max, + } + } + + if resp.Options.JPEG != nil { + jpegOpts := &JPEGOptions{} + if resp.Options.JPEG.FrameRateRange != nil { + jpegOpts.FrameRateRange = &FloatRange{ + Min: resp.Options.JPEG.FrameRateRange.Min, + Max: resp.Options.JPEG.FrameRateRange.Max, + } + } + if resp.Options.JPEG.EncodingIntervalRange != nil { + jpegOpts.EncodingIntervalRange = &IntRange{ + Min: resp.Options.JPEG.EncodingIntervalRange.Min, + Max: resp.Options.JPEG.EncodingIntervalRange.Max, + } + } + for _, res := range resp.Options.JPEG.ResolutionsAvailable { + jpegOpts.ResolutionsAvailable = append(jpegOpts.ResolutionsAvailable, &VideoResolution{ + Width: res.Width, + Height: res.Height, + }) + } + options.JPEG = jpegOpts + } + + if resp.Options.H264 != nil { + h264Opts := &H264Options{} + if resp.Options.H264.FrameRateRange != nil { + h264Opts.FrameRateRange = &FloatRange{ + Min: resp.Options.H264.FrameRateRange.Min, + Max: resp.Options.H264.FrameRateRange.Max, + } + } + if resp.Options.H264.GovLengthRange != nil { + h264Opts.GovLengthRange = &IntRange{ + Min: resp.Options.H264.GovLengthRange.Min, + Max: resp.Options.H264.GovLengthRange.Max, + } + } + if resp.Options.H264.EncodingIntervalRange != nil { + h264Opts.EncodingIntervalRange = &IntRange{ + Min: resp.Options.H264.EncodingIntervalRange.Min, + Max: resp.Options.H264.EncodingIntervalRange.Max, + } + } + for _, res := range resp.Options.H264.ResolutionsAvailable { + h264Opts.ResolutionsAvailable = append(h264Opts.ResolutionsAvailable, &VideoResolution{ + Width: res.Width, + Height: res.Height, + }) + } + h264Opts.H264ProfilesSupported = resp.Options.H264.H264ProfilesSupported + options.H264 = h264Opts + } + + return options, nil +} + +// GetAudioEncoderConfiguration retrieves audio encoder configuration +func (c *Client) GetAudioEncoderConfiguration(ctx context.Context, configurationToken string) (*AudioEncoderConfiguration, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetAudioEncoderConfiguration struct { + XMLName xml.Name `xml:"trt:GetAudioEncoderConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + type GetAudioEncoderConfigurationResponse struct { + XMLName xml.Name `xml:"GetAudioEncoderConfigurationResponse"` + Configuration 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 *struct { + Address *struct { + Type string `xml:"Type"` + IPv4Address string `xml:"IPv4Address"` + IPv6Address string `xml:"IPv6Address"` + } `xml:"Address"` + Port int `xml:"Port"` + TTL int `xml:"TTL"` + AutoStart bool `xml:"AutoStart"` + } `xml:"Multicast"` + SessionTimeout string `xml:"SessionTimeout"` + } `xml:"Configuration"` + } + + req := GetAudioEncoderConfiguration{ + Xmlns: mediaNamespace, + ConfigurationToken: configurationToken, + } + + var resp GetAudioEncoderConfigurationResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetAudioEncoderConfiguration failed: %w", err) + } + + config := &AudioEncoderConfiguration{ + Token: resp.Configuration.Token, + Name: resp.Configuration.Name, + UseCount: resp.Configuration.UseCount, + Encoding: resp.Configuration.Encoding, + Bitrate: resp.Configuration.Bitrate, + SampleRate: resp.Configuration.SampleRate, + } + + if resp.Configuration.Multicast != nil { + config.Multicast = &MulticastConfiguration{ + Port: resp.Configuration.Multicast.Port, + TTL: resp.Configuration.Multicast.TTL, + AutoStart: resp.Configuration.Multicast.AutoStart, + } + if resp.Configuration.Multicast.Address != nil { + config.Multicast.Address = &IPAddress{ + Type: resp.Configuration.Multicast.Address.Type, + IPv4Address: resp.Configuration.Multicast.Address.IPv4Address, + IPv6Address: resp.Configuration.Multicast.Address.IPv6Address, + } + } + } + + return config, nil +} + +// SetAudioEncoderConfiguration sets audio encoder configuration +func (c *Client) SetAudioEncoderConfiguration(ctx context.Context, config *AudioEncoderConfiguration, forcePersistence bool) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type SetAudioEncoderConfiguration struct { + XMLName xml.Name `xml:"trt:SetAudioEncoderConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + Xmlnst string `xml:"xmlns:tt,attr"` + Configuration struct { + Token string `xml:"token,attr"` + Name string `xml:"tt:Name"` + UseCount int `xml:"tt:UseCount"` + Encoding string `xml:"tt:Encoding"` + Bitrate int `xml:"tt:Bitrate,omitempty"` + SampleRate int `xml:"tt:SampleRate,omitempty"` + Multicast *struct { + Address *struct { + Type string `xml:"tt:Type"` + IPv4Address string `xml:"tt:IPv4Address,omitempty"` + IPv6Address string `xml:"tt:IPv6Address,omitempty"` + } `xml:"tt:Address,omitempty"` + Port int `xml:"tt:Port,omitempty"` + TTL int `xml:"tt:TTL,omitempty"` + AutoStart bool `xml:"tt:AutoStart,omitempty"` + } `xml:"tt:Multicast,omitempty"` + SessionTimeout string `xml:"tt:SessionTimeout,omitempty"` + } `xml:"trt:Configuration"` + ForcePersistence bool `xml:"trt:ForcePersistence"` + } + + req := SetAudioEncoderConfiguration{ + Xmlns: mediaNamespace, + Xmlnst: "http://www.onvif.org/ver10/schema", + ForcePersistence: forcePersistence, + } + + req.Configuration.Token = config.Token + req.Configuration.Name = config.Name + req.Configuration.UseCount = config.UseCount + req.Configuration.Encoding = config.Encoding + if config.Bitrate > 0 { + req.Configuration.Bitrate = config.Bitrate + } + if config.SampleRate > 0 { + req.Configuration.SampleRate = config.SampleRate + } + + if config.Multicast != nil { + req.Configuration.Multicast = &struct { + Address *struct { + Type string `xml:"tt:Type"` + IPv4Address string `xml:"tt:IPv4Address,omitempty"` + IPv6Address string `xml:"tt:IPv6Address,omitempty"` + } `xml:"tt:Address,omitempty"` + Port int `xml:"tt:Port,omitempty"` + TTL int `xml:"tt:TTL,omitempty"` + AutoStart bool `xml:"tt:AutoStart,omitempty"` + }{ + Port: config.Multicast.Port, + TTL: config.Multicast.TTL, + AutoStart: config.Multicast.AutoStart, + } + if config.Multicast.Address != nil { + req.Configuration.Multicast.Address = &struct { + Type string `xml:"tt:Type"` + IPv4Address string `xml:"tt:IPv4Address,omitempty"` + IPv6Address string `xml:"tt:IPv6Address,omitempty"` + }{ + Type: config.Multicast.Address.Type, + IPv4Address: config.Multicast.Address.IPv4Address, + IPv6Address: config.Multicast.Address.IPv6Address, + } + } + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("SetAudioEncoderConfiguration failed: %w", err) + } + + return nil +} + +// GetMetadataConfiguration retrieves metadata configuration +func (c *Client) GetMetadataConfiguration(ctx context.Context, configurationToken string) (*MetadataConfiguration, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetMetadataConfiguration struct { + XMLName xml.Name `xml:"trt:GetMetadataConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + type GetMetadataConfigurationResponse struct { + XMLName xml.Name `xml:"GetMetadataConfigurationResponse"` + Configuration struct { + Token string `xml:"token,attr"` + Name string `xml:"Name"` + UseCount int `xml:"UseCount"` + PTZStatus *struct { + Status bool `xml:"Status"` + Position bool `xml:"Position"` + } `xml:"PTZStatus"` + Events *struct{} `xml:"Events"` + Analytics bool `xml:"Analytics"` + Multicast *struct { + Address *struct { + Type string `xml:"Type"` + IPv4Address string `xml:"IPv4Address"` + IPv6Address string `xml:"IPv6Address"` + } `xml:"Address"` + Port int `xml:"Port"` + TTL int `xml:"TTL"` + AutoStart bool `xml:"AutoStart"` + } `xml:"Multicast"` + SessionTimeout string `xml:"SessionTimeout"` + } `xml:"Configuration"` + } + + req := GetMetadataConfiguration{ + Xmlns: mediaNamespace, + ConfigurationToken: configurationToken, + } + + var resp GetMetadataConfigurationResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetMetadataConfiguration failed: %w", err) + } + + config := &MetadataConfiguration{ + Token: resp.Configuration.Token, + Name: resp.Configuration.Name, + UseCount: resp.Configuration.UseCount, + Analytics: resp.Configuration.Analytics, + } + + if resp.Configuration.PTZStatus != nil { + config.PTZStatus = &PTZFilter{ + Status: resp.Configuration.PTZStatus.Status, + Position: resp.Configuration.PTZStatus.Position, + } + } + + if resp.Configuration.Events != nil { + config.Events = &EventSubscription{} + } + + if resp.Configuration.Multicast != nil { + config.Multicast = &MulticastConfiguration{ + Port: resp.Configuration.Multicast.Port, + TTL: resp.Configuration.Multicast.TTL, + AutoStart: resp.Configuration.Multicast.AutoStart, + } + if resp.Configuration.Multicast.Address != nil { + config.Multicast.Address = &IPAddress{ + Type: resp.Configuration.Multicast.Address.Type, + IPv4Address: resp.Configuration.Multicast.Address.IPv4Address, + IPv6Address: resp.Configuration.Multicast.Address.IPv6Address, + } + } + } + + return config, nil +} + +// SetMetadataConfiguration sets metadata configuration +func (c *Client) SetMetadataConfiguration(ctx context.Context, config *MetadataConfiguration, forcePersistence bool) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type SetMetadataConfiguration struct { + XMLName xml.Name `xml:"trt:SetMetadataConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + Xmlnst string `xml:"xmlns:tt,attr"` + Configuration struct { + Token string `xml:"token,attr"` + Name string `xml:"tt:Name"` + UseCount int `xml:"tt:UseCount"` + PTZStatus *struct { + Status bool `xml:"tt:Status"` + Position bool `xml:"tt:Position"` + } `xml:"tt:PTZStatus,omitempty"` + Events *struct{} `xml:"tt:Events,omitempty"` + Analytics bool `xml:"tt:Analytics,omitempty"` + Multicast *struct { + Address *struct { + Type string `xml:"tt:Type"` + IPv4Address string `xml:"tt:IPv4Address,omitempty"` + IPv6Address string `xml:"tt:IPv6Address,omitempty"` + } `xml:"tt:Address,omitempty"` + Port int `xml:"tt:Port,omitempty"` + TTL int `xml:"tt:TTL,omitempty"` + AutoStart bool `xml:"tt:AutoStart,omitempty"` + } `xml:"tt:Multicast,omitempty"` + SessionTimeout string `xml:"tt:SessionTimeout,omitempty"` + } `xml:"trt:Configuration"` + ForcePersistence bool `xml:"trt:ForcePersistence"` + } + + req := SetMetadataConfiguration{ + Xmlns: mediaNamespace, + Xmlnst: "http://www.onvif.org/ver10/schema", + ForcePersistence: forcePersistence, + } + + req.Configuration.Token = config.Token + req.Configuration.Name = config.Name + req.Configuration.UseCount = config.UseCount + req.Configuration.Analytics = config.Analytics + + if config.PTZStatus != nil { + req.Configuration.PTZStatus = &struct { + Status bool `xml:"tt:Status"` + Position bool `xml:"tt:Position"` + }{ + Status: config.PTZStatus.Status, + Position: config.PTZStatus.Position, + } + } + + if config.Events != nil { + req.Configuration.Events = &struct{}{} + } + + if config.Multicast != nil { + req.Configuration.Multicast = &struct { + Address *struct { + Type string `xml:"tt:Type"` + IPv4Address string `xml:"tt:IPv4Address,omitempty"` + IPv6Address string `xml:"tt:IPv6Address,omitempty"` + } `xml:"tt:Address,omitempty"` + Port int `xml:"tt:Port,omitempty"` + TTL int `xml:"tt:TTL,omitempty"` + AutoStart bool `xml:"tt:AutoStart,omitempty"` + }{ + Port: config.Multicast.Port, + TTL: config.Multicast.TTL, + AutoStart: config.Multicast.AutoStart, + } + if config.Multicast.Address != nil { + req.Configuration.Multicast.Address = &struct { + Type string `xml:"tt:Type"` + IPv4Address string `xml:"tt:IPv4Address,omitempty"` + IPv6Address string `xml:"tt:IPv6Address,omitempty"` + }{ + Type: config.Multicast.Address.Type, + IPv4Address: config.Multicast.Address.IPv4Address, + IPv6Address: config.Multicast.Address.IPv6Address, + } + } + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("SetMetadataConfiguration failed: %w", err) + } + + return nil +} + +// GetVideoSourceModes retrieves available video source modes +func (c *Client) GetVideoSourceModes(ctx context.Context, videoSourceToken string) ([]*VideoSourceMode, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetVideoSourceModes struct { + XMLName xml.Name `xml:"trt:GetVideoSourceModes"` + Xmlns string `xml:"xmlns:trt,attr"` + VideoSourceToken string `xml:"trt:VideoSourceToken"` + } + + type GetVideoSourceModesResponse struct { + XMLName xml.Name `xml:"GetVideoSourceModesResponse"` + VideoSourceModes []struct { + Token string `xml:"token,attr"` + Enabled bool `xml:"Enabled"` + Resolution struct { + Width int `xml:"Width"` + Height int `xml:"Height"` + } `xml:"Resolution"` + } `xml:"VideoSourceModes"` + } + + req := GetVideoSourceModes{ + Xmlns: mediaNamespace, + VideoSourceToken: videoSourceToken, + } + + var resp GetVideoSourceModesResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetVideoSourceModes failed: %w", err) + } + + modes := make([]*VideoSourceMode, len(resp.VideoSourceModes)) + for i, m := range resp.VideoSourceModes { + modes[i] = &VideoSourceMode{ + Token: m.Token, + Enabled: m.Enabled, + Resolution: &VideoResolution{ + Width: m.Resolution.Width, + Height: m.Resolution.Height, + }, + } + } + + return modes, nil +} + +// SetVideoSourceMode sets the video source mode +func (c *Client) SetVideoSourceMode(ctx context.Context, videoSourceToken, modeToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type SetVideoSourceMode struct { + XMLName xml.Name `xml:"trt:SetVideoSourceMode"` + Xmlns string `xml:"xmlns:trt,attr"` + VideoSourceToken string `xml:"trt:VideoSourceToken"` + ModeToken string `xml:"trt:ModeToken"` + } + + req := SetVideoSourceMode{ + Xmlns: mediaNamespace, + VideoSourceToken: videoSourceToken, + ModeToken: modeToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("SetVideoSourceMode failed: %w", err) + } + + return nil +} + +// SetSynchronizationPoint sets a synchronization point for the stream +func (c *Client) SetSynchronizationPoint(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type SetSynchronizationPoint struct { + XMLName xml.Name `xml:"trt:SetSynchronizationPoint"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := SetSynchronizationPoint{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("SetSynchronizationPoint failed: %w", err) + } + + return nil +} + +// GetOSDs retrieves all OSD configurations +func (c *Client) GetOSDs(ctx context.Context, configurationToken string) ([]*OSDConfiguration, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetOSDs struct { + XMLName xml.Name `xml:"trt:GetOSDs"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken,omitempty"` + } + + type GetOSDsResponse struct { + XMLName xml.Name `xml:"GetOSDsResponse"` + OSDs []struct { + Token string `xml:"token,attr"` + } `xml:"OSDs"` + } + + req := GetOSDs{ + Xmlns: mediaNamespace, + } + if configurationToken != "" { + req.ConfigurationToken = configurationToken + } + + var resp GetOSDsResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetOSDs failed: %w", err) + } + + osds := make([]*OSDConfiguration, len(resp.OSDs)) + for i, o := range resp.OSDs { + osds[i] = &OSDConfiguration{ + Token: o.Token, + } + } + + return osds, nil +} + +// GetOSD retrieves a specific OSD configuration +func (c *Client) GetOSD(ctx context.Context, osdToken string) (*OSDConfiguration, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetOSD struct { + XMLName xml.Name `xml:"trt:GetOSD"` + Xmlns string `xml:"xmlns:trt,attr"` + OSDToken string `xml:"trt:OSDToken"` + } + + type GetOSDResponse struct { + XMLName xml.Name `xml:"GetOSDResponse"` + OSD struct { + Token string `xml:"token,attr"` + } `xml:"OSD"` + } + + req := GetOSD{ + Xmlns: mediaNamespace, + OSDToken: osdToken, + } + + var resp GetOSDResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetOSD failed: %w", err) + } + + return &OSDConfiguration{ + Token: resp.OSD.Token, + }, nil +} + +// SetOSD sets OSD configuration +func (c *Client) SetOSD(ctx context.Context, osd *OSDConfiguration) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type SetOSD struct { + XMLName xml.Name `xml:"trt:SetOSD"` + Xmlns string `xml:"xmlns:trt,attr"` + Xmlnst string `xml:"xmlns:tt,attr"` + OSD struct { + Token string `xml:"token,attr"` + } `xml:"trt:OSD"` + } + + req := SetOSD{ + Xmlns: mediaNamespace, + Xmlnst: "http://www.onvif.org/ver10/schema", + } + req.OSD.Token = osd.Token + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("SetOSD failed: %w", err) + } + + return nil +} + +// CreateOSD creates a new OSD configuration +func (c *Client) CreateOSD(ctx context.Context, videoSourceConfigurationToken string, osd *OSDConfiguration) (*OSDConfiguration, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type CreateOSD struct { + XMLName xml.Name `xml:"trt:CreateOSD"` + Xmlns string `xml:"xmlns:trt,attr"` + Xmlnst string `xml:"xmlns:tt,attr"` + VideoSourceConfigurationToken string `xml:"trt:VideoSourceConfigurationToken"` + OSD struct { + Token string `xml:"token,attr,omitempty"` + } `xml:"trt:OSD"` + } + + type CreateOSDResponse struct { + XMLName xml.Name `xml:"CreateOSDResponse"` + OSD struct { + Token string `xml:"token,attr"` + } `xml:"OSD"` + } + + req := CreateOSD{ + Xmlns: mediaNamespace, + Xmlnst: "http://www.onvif.org/ver10/schema", + VideoSourceConfigurationToken: videoSourceConfigurationToken, + } + if osd != nil && osd.Token != "" { + req.OSD.Token = osd.Token + } + + var resp CreateOSDResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("CreateOSD failed: %w", err) + } + + return &OSDConfiguration{ + Token: resp.OSD.Token, + }, nil +} + +// DeleteOSD deletes an OSD configuration +func (c *Client) DeleteOSD(ctx context.Context, osdToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type DeleteOSD struct { + XMLName xml.Name `xml:"trt:DeleteOSD"` + Xmlns string `xml:"xmlns:trt,attr"` + OSDToken string `xml:"trt:OSDToken"` + } + + req := DeleteOSD{ + Xmlns: mediaNamespace, + OSDToken: osdToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("DeleteOSD failed: %w", err) + } + + return nil +} + +// StartMulticastStreaming starts multicast streaming +func (c *Client) StartMulticastStreaming(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type StartMulticastStreaming struct { + XMLName xml.Name `xml:"trt:StartMulticastStreaming"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := StartMulticastStreaming{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("StartMulticastStreaming failed: %w", err) + } + + return nil +} + +// StopMulticastStreaming stops multicast streaming +func (c *Client) StopMulticastStreaming(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type StopMulticastStreaming struct { + XMLName xml.Name `xml:"trt:StopMulticastStreaming"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := StopMulticastStreaming{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("StopMulticastStreaming failed: %w", err) + } + + return nil +} + +// GetProfile retrieves a specific media profile +func (c *Client) GetProfile(ctx context.Context, profileToken string) (*Profile, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetProfile struct { + XMLName xml.Name `xml:"trt:GetProfile"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + type GetProfileResponse struct { + XMLName xml.Name `xml:"GetProfileResponse"` + Profile struct { + Token string `xml:"token,attr"` + Name string `xml:"Name"` + } `xml:"Profile"` + } + + req := GetProfile{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + var resp GetProfileResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetProfile failed: %w", err) + } + + return &Profile{ + Token: resp.Profile.Token, + Name: resp.Profile.Name, + }, nil +} + +// SetProfile sets profile configuration +func (c *Client) SetProfile(ctx context.Context, profile *Profile) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type SetProfile struct { + XMLName xml.Name `xml:"trt:SetProfile"` + Xmlns string `xml:"xmlns:trt,attr"` + Xmlnst string `xml:"xmlns:tt,attr"` + Profile struct { + Token string `xml:"token,attr"` + Name string `xml:"tt:Name"` + } `xml:"trt:Profile"` + } + + req := SetProfile{ + Xmlns: mediaNamespace, + Xmlnst: "http://www.onvif.org/ver10/schema", + } + req.Profile.Token = profile.Token + req.Profile.Name = profile.Name + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("SetProfile failed: %w", err) + } + + return nil +} + +// AddVideoEncoderConfiguration adds video encoder configuration to a profile +func (c *Client) AddVideoEncoderConfiguration(ctx context.Context, profileToken, configurationToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type AddVideoEncoderConfiguration struct { + XMLName xml.Name `xml:"trt:AddVideoEncoderConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + req := AddVideoEncoderConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + ConfigurationToken: configurationToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("AddVideoEncoderConfiguration failed: %w", err) + } + + return nil +} + +// RemoveVideoEncoderConfiguration removes video encoder configuration from a profile +func (c *Client) RemoveVideoEncoderConfiguration(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type RemoveVideoEncoderConfiguration struct { + XMLName xml.Name `xml:"trt:RemoveVideoEncoderConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := RemoveVideoEncoderConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("RemoveVideoEncoderConfiguration failed: %w", err) + } + + return nil +} + +// AddAudioEncoderConfiguration adds audio encoder configuration to a profile +func (c *Client) AddAudioEncoderConfiguration(ctx context.Context, profileToken, configurationToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type AddAudioEncoderConfiguration struct { + XMLName xml.Name `xml:"trt:AddAudioEncoderConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + req := AddAudioEncoderConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + ConfigurationToken: configurationToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("AddAudioEncoderConfiguration failed: %w", err) + } + + return nil +} + +// RemoveAudioEncoderConfiguration removes audio encoder configuration from a profile +func (c *Client) RemoveAudioEncoderConfiguration(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type RemoveAudioEncoderConfiguration struct { + XMLName xml.Name `xml:"trt:RemoveAudioEncoderConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := RemoveAudioEncoderConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("RemoveAudioEncoderConfiguration failed: %w", err) + } + + return nil +} + +// AddAudioSourceConfiguration adds audio source configuration to a profile +func (c *Client) AddAudioSourceConfiguration(ctx context.Context, profileToken, configurationToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type AddAudioSourceConfiguration struct { + XMLName xml.Name `xml:"trt:AddAudioSourceConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + req := AddAudioSourceConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + ConfigurationToken: configurationToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("AddAudioSourceConfiguration failed: %w", err) + } + + return nil +} + +// RemoveAudioSourceConfiguration removes audio source configuration from a profile +func (c *Client) RemoveAudioSourceConfiguration(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type RemoveAudioSourceConfiguration struct { + XMLName xml.Name `xml:"trt:RemoveAudioSourceConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := RemoveAudioSourceConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("RemoveAudioSourceConfiguration failed: %w", err) + } + + return nil +} + +// AddVideoSourceConfiguration adds video source configuration to a profile +func (c *Client) AddVideoSourceConfiguration(ctx context.Context, profileToken, configurationToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type AddVideoSourceConfiguration struct { + XMLName xml.Name `xml:"trt:AddVideoSourceConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + req := AddVideoSourceConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + ConfigurationToken: configurationToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("AddVideoSourceConfiguration failed: %w", err) + } + + return nil +} + +// RemoveVideoSourceConfiguration removes video source configuration from a profile +func (c *Client) RemoveVideoSourceConfiguration(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type RemoveVideoSourceConfiguration struct { + XMLName xml.Name `xml:"trt:RemoveVideoSourceConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := RemoveVideoSourceConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("RemoveVideoSourceConfiguration failed: %w", err) + } + + return nil +} + +// AddPTZConfiguration adds PTZ configuration to a profile +func (c *Client) AddPTZConfiguration(ctx context.Context, profileToken, configurationToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type AddPTZConfiguration struct { + XMLName xml.Name `xml:"trt:AddPTZConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + req := AddPTZConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + ConfigurationToken: configurationToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("AddPTZConfiguration failed: %w", err) + } + + return nil +} + +// RemovePTZConfiguration removes PTZ configuration from a profile +func (c *Client) RemovePTZConfiguration(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type RemovePTZConfiguration struct { + XMLName xml.Name `xml:"trt:RemovePTZConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := RemovePTZConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("RemovePTZConfiguration failed: %w", err) + } + + return nil +} + +// AddMetadataConfiguration adds metadata configuration to a profile +func (c *Client) AddMetadataConfiguration(ctx context.Context, profileToken, configurationToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type AddMetadataConfiguration struct { + XMLName xml.Name `xml:"trt:AddMetadataConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + req := AddMetadataConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + ConfigurationToken: configurationToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("AddMetadataConfiguration failed: %w", err) + } + + return nil +} + +// RemoveMetadataConfiguration removes metadata configuration from a profile +func (c *Client) RemoveMetadataConfiguration(ctx context.Context, profileToken string) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type RemoveMetadataConfiguration struct { + XMLName xml.Name `xml:"trt:RemoveMetadataConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ProfileToken string `xml:"trt:ProfileToken"` + } + + req := RemoveMetadataConfiguration{ + Xmlns: mediaNamespace, + ProfileToken: profileToken, + } + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("RemoveMetadataConfiguration failed: %w", err) + } + + return nil +} + +// GetAudioEncoderConfigurationOptions retrieves available options for audio encoder configuration +func (c *Client) GetAudioEncoderConfigurationOptions(ctx context.Context, configurationToken, profileToken string) (*AudioEncoderConfigurationOptions, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetAudioEncoderConfigurationOptions struct { + XMLName xml.Name `xml:"trt:GetAudioEncoderConfigurationOptions"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken,omitempty"` + ProfileToken string `xml:"trt:ProfileToken,omitempty"` + } + + type GetAudioEncoderConfigurationOptionsResponse struct { + XMLName xml.Name `xml:"GetAudioEncoderConfigurationOptionsResponse"` + Options struct { + EncodingOptions []string `xml:"EncodingOptions"` + BitrateList []int `xml:"BitrateList"` + SampleRateList []int `xml:"SampleRateList"` + } `xml:"Options"` + } + + req := GetAudioEncoderConfigurationOptions{ + Xmlns: mediaNamespace, + } + if configurationToken != "" { + req.ConfigurationToken = configurationToken + } + if profileToken != "" { + req.ProfileToken = profileToken + } + + var resp GetAudioEncoderConfigurationOptionsResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetAudioEncoderConfigurationOptions failed: %w", err) + } + + return &AudioEncoderConfigurationOptions{ + EncodingOptions: resp.Options.EncodingOptions, + BitrateList: resp.Options.BitrateList, + SampleRateList: resp.Options.SampleRateList, + }, nil +} + +// GetMetadataConfigurationOptions retrieves available options for metadata configuration +func (c *Client) GetMetadataConfigurationOptions(ctx context.Context, configurationToken, profileToken string) (*MetadataConfigurationOptions, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetMetadataConfigurationOptions struct { + XMLName xml.Name `xml:"trt:GetMetadataConfigurationOptions"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken,omitempty"` + ProfileToken string `xml:"trt:ProfileToken,omitempty"` + } + + type GetMetadataConfigurationOptionsResponse struct { + XMLName xml.Name `xml:"GetMetadataConfigurationOptionsResponse"` + Options struct { + PTZStatusFilterOptions *struct { + Status bool `xml:"Status"` + Position bool `xml:"Position"` + } `xml:"PTZStatusFilterOptions"` + Extension struct{} `xml:"Extension"` + } `xml:"Options"` + } + + req := GetMetadataConfigurationOptions{ + Xmlns: mediaNamespace, + } + if configurationToken != "" { + req.ConfigurationToken = configurationToken + } + if profileToken != "" { + req.ProfileToken = profileToken + } + + var resp GetMetadataConfigurationOptionsResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetMetadataConfigurationOptions failed: %w", err) + } + + options := &MetadataConfigurationOptions{} + if resp.Options.PTZStatusFilterOptions != nil { + options.PTZStatusFilterOptions = &PTZFilter{ + Status: resp.Options.PTZStatusFilterOptions.Status, + Position: resp.Options.PTZStatusFilterOptions.Position, + } + } + + return options, nil +} + +// GetAudioOutputConfiguration retrieves audio output configuration +func (c *Client) GetAudioOutputConfiguration(ctx context.Context, configurationToken string) (*AudioOutputConfiguration, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetAudioOutputConfiguration struct { + XMLName xml.Name `xml:"trt:GetAudioOutputConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + type GetAudioOutputConfigurationResponse struct { + XMLName xml.Name `xml:"GetAudioOutputConfigurationResponse"` + Configuration struct { + Token string `xml:"token,attr"` + Name string `xml:"Name"` + UseCount int `xml:"UseCount"` + OutputToken string `xml:"OutputToken"` + } `xml:"Configuration"` + } + + req := GetAudioOutputConfiguration{ + Xmlns: mediaNamespace, + ConfigurationToken: configurationToken, + } + + var resp GetAudioOutputConfigurationResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetAudioOutputConfiguration failed: %w", err) + } + + return &AudioOutputConfiguration{ + Token: resp.Configuration.Token, + Name: resp.Configuration.Name, + UseCount: resp.Configuration.UseCount, + OutputToken: resp.Configuration.OutputToken, + }, nil +} + +// SetAudioOutputConfiguration sets audio output configuration +func (c *Client) SetAudioOutputConfiguration(ctx context.Context, config *AudioOutputConfiguration, forcePersistence bool) error { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type SetAudioOutputConfiguration struct { + XMLName xml.Name `xml:"trt:SetAudioOutputConfiguration"` + Xmlns string `xml:"xmlns:trt,attr"` + Xmlnst string `xml:"xmlns:tt,attr"` + Configuration struct { + Token string `xml:"token,attr"` + Name string `xml:"tt:Name"` + UseCount int `xml:"tt:UseCount"` + OutputToken string `xml:"tt:OutputToken"` + } `xml:"trt:Configuration"` + ForcePersistence bool `xml:"trt:ForcePersistence"` + } + + req := SetAudioOutputConfiguration{ + Xmlns: mediaNamespace, + Xmlnst: "http://www.onvif.org/ver10/schema", + ForcePersistence: forcePersistence, + } + + req.Configuration.Token = config.Token + req.Configuration.Name = config.Name + req.Configuration.UseCount = config.UseCount + req.Configuration.OutputToken = config.OutputToken + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, nil); err != nil { + return fmt.Errorf("SetAudioOutputConfiguration failed: %w", err) + } + + return nil +} + +// GetAudioOutputConfigurationOptions retrieves available options for audio output configuration +func (c *Client) GetAudioOutputConfigurationOptions(ctx context.Context, configurationToken string) (*AudioOutputConfigurationOptions, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetAudioOutputConfigurationOptions struct { + XMLName xml.Name `xml:"trt:GetAudioOutputConfigurationOptions"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken,omitempty"` + } + + type GetAudioOutputConfigurationOptionsResponse struct { + XMLName xml.Name `xml:"GetAudioOutputConfigurationOptionsResponse"` + Options struct { + OutputTokensAvailable []string `xml:"OutputTokensAvailable"` + } `xml:"Options"` + } + + req := GetAudioOutputConfigurationOptions{ + Xmlns: mediaNamespace, + } + if configurationToken != "" { + req.ConfigurationToken = configurationToken + } + + var resp GetAudioOutputConfigurationOptionsResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetAudioOutputConfigurationOptions failed: %w", err) + } + + return &AudioOutputConfigurationOptions{ + OutputTokensAvailable: resp.Options.OutputTokensAvailable, + }, nil +} + +// GetAudioDecoderConfigurationOptions retrieves available options for audio decoder configuration +func (c *Client) GetAudioDecoderConfigurationOptions(ctx context.Context, configurationToken string) (*AudioDecoderConfigurationOptions, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetAudioDecoderConfigurationOptions struct { + XMLName xml.Name `xml:"trt:GetAudioDecoderConfigurationOptions"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken,omitempty"` + } + + type GetAudioDecoderConfigurationOptionsResponse struct { + XMLName xml.Name `xml:"GetAudioDecoderConfigurationOptionsResponse"` + Options struct { + AACDecOptions *struct { + BitrateList []int `xml:"BitrateList"` + SampleRateList []int `xml:"SampleRateList"` + } `xml:"AACDecOptions"` + G711DecOptions *struct { + BitrateList []int `xml:"BitrateList"` + } `xml:"G711DecOptions"` + G726DecOptions *struct { + BitrateList []int `xml:"BitrateList"` + } `xml:"G726DecOptions"` + } `xml:"Options"` + } + + req := GetAudioDecoderConfigurationOptions{ + Xmlns: mediaNamespace, + } + if configurationToken != "" { + req.ConfigurationToken = configurationToken + } + + var resp GetAudioDecoderConfigurationOptionsResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetAudioDecoderConfigurationOptions failed: %w", err) + } + + options := &AudioDecoderConfigurationOptions{} + if resp.Options.AACDecOptions != nil { + options.AACDecOptions = &AudioDecoderOptions{ + BitrateList: resp.Options.AACDecOptions.BitrateList, + SampleRateList: resp.Options.AACDecOptions.SampleRateList, + } + } + if resp.Options.G711DecOptions != nil { + options.G711DecOptions = &AudioDecoderOptions{ + BitrateList: resp.Options.G711DecOptions.BitrateList, + } + } + if resp.Options.G726DecOptions != nil { + options.G726DecOptions = &AudioDecoderOptions{ + BitrateList: resp.Options.G726DecOptions.BitrateList, + } + } + + return options, nil +} + +// GetGuaranteedNumberOfVideoEncoderInstances retrieves the guaranteed number of video encoder instances +func (c *Client) GetGuaranteedNumberOfVideoEncoderInstances(ctx context.Context, configurationToken string) (*GuaranteedNumberOfVideoEncoderInstances, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetGuaranteedNumberOfVideoEncoderInstances struct { + XMLName xml.Name `xml:"trt:GetGuaranteedNumberOfVideoEncoderInstances"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken"` + } + + type GetGuaranteedNumberOfVideoEncoderInstancesResponse struct { + XMLName xml.Name `xml:"GetGuaranteedNumberOfVideoEncoderInstancesResponse"` + TotalNumber int `xml:"TotalNumber"` + JPEG int `xml:"JPEG"` + H264 int `xml:"H264"` + MPEG4 int `xml:"MPEG4"` + } + + req := GetGuaranteedNumberOfVideoEncoderInstances{ + Xmlns: mediaNamespace, + ConfigurationToken: configurationToken, + } + + var resp GetGuaranteedNumberOfVideoEncoderInstancesResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetGuaranteedNumberOfVideoEncoderInstances failed: %w", err) + } + + return &GuaranteedNumberOfVideoEncoderInstances{ + TotalNumber: resp.TotalNumber, + JPEG: resp.JPEG, + H264: resp.H264, + MPEG4: resp.MPEG4, + }, nil +} + +// GetOSDOptions retrieves available options for OSD configuration +func (c *Client) GetOSDOptions(ctx context.Context, configurationToken string) (*OSDConfigurationOptions, error) { + endpoint := c.mediaEndpoint + if endpoint == "" { + endpoint = c.endpoint + } + + type GetOSDOptions struct { + XMLName xml.Name `xml:"trt:GetOSDOptions"` + Xmlns string `xml:"xmlns:trt,attr"` + ConfigurationToken string `xml:"trt:ConfigurationToken,omitempty"` + } + + type GetOSDOptionsResponse struct { + XMLName xml.Name `xml:"GetOSDOptionsResponse"` + Options struct { + MaximumNumberOfOSDs int `xml:"MaximumNumberOfOSDs"` + } `xml:"Options"` + } + + req := GetOSDOptions{ + Xmlns: mediaNamespace, + } + if configurationToken != "" { + req.ConfigurationToken = configurationToken + } + + var resp GetOSDOptionsResponse + + username, password := c.GetCredentials() + soapClient := soap.NewClient(c.httpClient, username, password) + + if err := soapClient.Call(ctx, endpoint, "", req, &resp); err != nil { + return nil, fmt.Errorf("GetOSDOptions failed: %w", err) + } + + return &OSDConfigurationOptions{ + MaximumNumberOfOSDs: resp.Options.MaximumNumberOfOSDs, + }, nil +} diff --git a/media_test.go b/media_test.go new file mode 100644 index 0000000..3d0054d --- /dev/null +++ b/media_test.go @@ -0,0 +1,1490 @@ +package onvif + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// TestGetProfiles tests GetProfiles operation +func TestGetProfiles(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + Main Profile + + H264 + + 1920 + 1080 + + 5.0 + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL+"/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + profiles, err := client.GetProfiles(ctx) + if err != nil { + t.Fatalf("GetProfiles() failed: %v", err) + } + + if len(profiles) != 1 { + t.Errorf("Expected 1 profile, got %d", len(profiles)) + } + + if profiles[0].Token != "Profile1" { + t.Errorf("Expected token Profile1, got %s", profiles[0].Token) + } + + if profiles[0].Name != "Main Profile" { + t.Errorf("Expected name 'Main Profile', got %s", profiles[0].Name) + } +} + +// TestGetProfile tests GetProfile operation +func TestGetProfile(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + Main Profile + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + profile, err := client.GetProfile(ctx, "Profile1") + if err != nil { + t.Fatalf("GetProfile() failed: %v", err) + } + + if profile.Token != "Profile1" { + t.Errorf("Expected token Profile1, got %s", profile.Token) + } +} + +// TestSetProfile tests SetProfile operation +func TestSetProfile(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + profile := &Profile{ + Token: "Profile1", + Name: "Updated Profile", + } + + err = client.SetProfile(ctx, profile) + if err != nil { + t.Fatalf("SetProfile() failed: %v", err) + } +} + +// TestGetStreamURI tests GetStreamURI operation +func TestGetStreamURI(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + rtsp://192.168.1.100:554/stream1 + false + true + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + uri, err := client.GetStreamURI(ctx, "Profile1") + if err != nil { + t.Fatalf("GetStreamURI() failed: %v", err) + } + + if uri.URI != "rtsp://192.168.1.100:554/stream1" { + t.Errorf("Expected URI 'rtsp://192.168.1.100:554/stream1', got %s", uri.URI) + } +} + +// TestGetSnapshotURI tests GetSnapshotURI operation +func TestGetSnapshotURI(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + http://192.168.1.100/snapshot.jpg + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + uri, err := client.GetSnapshotURI(ctx, "Profile1") + if err != nil { + t.Fatalf("GetSnapshotURI() failed: %v", err) + } + + if !strings.Contains(uri.URI, "snapshot") { + t.Errorf("Expected snapshot URI, got %s", uri.URI) + } +} + +// TestGetVideoSources tests GetVideoSources operation +func TestGetVideoSources(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + 30.0 + + 1920 + 1080 + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + sources, err := client.GetVideoSources(ctx) + if err != nil { + t.Fatalf("GetVideoSources() failed: %v", err) + } + + if len(sources) != 1 { + t.Errorf("Expected 1 video source, got %d", len(sources)) + } + + if sources[0].Token != "VideoSource1" { + t.Errorf("Expected token VideoSource1, got %s", sources[0].Token) + } +} + +// TestGetAudioSources tests GetAudioSources operation +func TestGetAudioSources(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + 2 + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + sources, err := client.GetAudioSources(ctx) + if err != nil { + t.Fatalf("GetAudioSources() failed: %v", err) + } + + if len(sources) != 1 { + t.Errorf("Expected 1 audio source, got %d", len(sources)) + } +} + +// TestGetAudioOutputs tests GetAudioOutputs operation +func TestGetAudioOutputs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + outputs, err := client.GetAudioOutputs(ctx) + if err != nil { + t.Fatalf("GetAudioOutputs() failed: %v", err) + } + + if len(outputs) != 1 { + t.Errorf("Expected 1 audio output, got %d", len(outputs)) + } +} + +// TestCreateProfile tests CreateProfile operation +func TestCreateProfile(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + New Profile + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + profile, err := client.CreateProfile(ctx, "New Profile", "") + if err != nil { + t.Fatalf("CreateProfile() failed: %v", err) + } + + if profile.Token != "NewProfile1" { + t.Errorf("Expected token NewProfile1, got %s", profile.Token) + } +} + +// TestDeleteProfile tests DeleteProfile operation +func TestDeleteProfile(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.DeleteProfile(ctx, "Profile1") + if err != nil { + t.Fatalf("DeleteProfile() failed: %v", err) + } +} + +// TestGetVideoEncoderConfiguration tests GetVideoEncoderConfiguration operation +func TestGetVideoEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + H264 Config + H264 + + 1920 + 1080 + + 5.0 + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config, err := client.GetVideoEncoderConfiguration(ctx, "VideoEnc1") + if err != nil { + t.Fatalf("GetVideoEncoderConfiguration() failed: %v", err) + } + + if config.Token != "VideoEnc1" { + t.Errorf("Expected token VideoEnc1, got %s", config.Token) + } + + if config.Encoding != "H264" { + t.Errorf("Expected encoding H264, got %s", config.Encoding) + } +} + +// TestSetVideoEncoderConfiguration tests SetVideoEncoderConfiguration operation +func TestSetVideoEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config := &VideoEncoderConfiguration{ + Token: "VideoEnc1", + Name: "H264 Config", + Encoding: "H264", + Resolution: &VideoResolution{ + Width: 1920, + Height: 1080, + }, + Quality: 5.0, + } + + err = client.SetVideoEncoderConfiguration(ctx, config, true) + if err != nil { + t.Fatalf("SetVideoEncoderConfiguration() failed: %v", err) + } +} + +// TestGetMediaServiceCapabilities tests GetMediaServiceCapabilities operation +func TestGetMediaServiceCapabilities(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + caps, err := client.GetMediaServiceCapabilities(ctx) + if err != nil { + t.Fatalf("GetMediaServiceCapabilities() failed: %v", err) + } + + if !caps.SnapshotUri { + t.Error("Expected SnapshotUri to be true") + } + + if caps.MaximumNumberOfProfiles != 10 { + t.Errorf("Expected MaximumNumberOfProfiles 10, got %d", caps.MaximumNumberOfProfiles) + } +} + +// TestGetVideoEncoderConfigurationOptions tests GetVideoEncoderConfigurationOptions operation +func TestGetVideoEncoderConfigurationOptions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + 1.0 + 10.0 + + + + 1920 + 1080 + + Baseline + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + options, err := client.GetVideoEncoderConfigurationOptions(ctx, "VideoEnc1") + if err != nil { + t.Fatalf("GetVideoEncoderConfigurationOptions() failed: %v", err) + } + + if options.QualityRange == nil { + t.Error("Expected QualityRange to be set") + } + + if options.H264 == nil { + t.Error("Expected H264 options to be set") + } +} + +// TestGetAudioEncoderConfiguration tests GetAudioEncoderConfiguration operation +func TestGetAudioEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + AAC Config + AAC + 128000 + 48000 + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config, err := client.GetAudioEncoderConfiguration(ctx, "AudioEnc1") + if err != nil { + t.Fatalf("GetAudioEncoderConfiguration() failed: %v", err) + } + + if config.Token != "AudioEnc1" { + t.Errorf("Expected token AudioEnc1, got %s", config.Token) + } + + if config.Encoding != "AAC" { + t.Errorf("Expected encoding AAC, got %s", config.Encoding) + } +} + +// TestSetAudioEncoderConfiguration tests SetAudioEncoderConfiguration operation +func TestSetAudioEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config := &AudioEncoderConfiguration{ + Token: "AudioEnc1", + Name: "AAC Config", + Encoding: "AAC", + Bitrate: 128000, + SampleRate: 48000, + } + + err = client.SetAudioEncoderConfiguration(ctx, config, true) + if err != nil { + t.Fatalf("SetAudioEncoderConfiguration() failed: %v", err) + } +} + +// TestGetMetadataConfiguration tests GetMetadataConfiguration operation +func TestGetMetadataConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + Metadata Config + + true + true + + false + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config, err := client.GetMetadataConfiguration(ctx, "Metadata1") + if err != nil { + t.Fatalf("GetMetadataConfiguration() failed: %v", err) + } + + if config.Token != "Metadata1" { + t.Errorf("Expected token Metadata1, got %s", config.Token) + } + + if config.PTZStatus == nil { + t.Error("Expected PTZStatus to be set") + } +} + +// TestSetMetadataConfiguration tests SetMetadataConfiguration operation +func TestSetMetadataConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config := &MetadataConfiguration{ + Token: "Metadata1", + Name: "Metadata Config", + Analytics: false, + PTZStatus: &PTZFilter{ + Status: true, + Position: true, + }, + } + + err = client.SetMetadataConfiguration(ctx, config, true) + if err != nil { + t.Fatalf("SetMetadataConfiguration() failed: %v", err) + } +} + +// TestGetVideoSourceModes tests GetVideoSourceModes operation +func TestGetVideoSourceModes(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + true + + 1920 + 1080 + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + modes, err := client.GetVideoSourceModes(ctx, "VideoSource1") + if err != nil { + t.Fatalf("GetVideoSourceModes() failed: %v", err) + } + + if len(modes) != 1 { + t.Errorf("Expected 1 mode, got %d", len(modes)) + } + + if modes[0].Token != "Mode1" { + t.Errorf("Expected token Mode1, got %s", modes[0].Token) + } +} + +// TestSetVideoSourceMode tests SetVideoSourceMode operation +func TestSetVideoSourceMode(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.SetVideoSourceMode(ctx, "VideoSource1", "Mode1") + if err != nil { + t.Fatalf("SetVideoSourceMode() failed: %v", err) + } +} + +// TestSetSynchronizationPoint tests SetSynchronizationPoint operation +func TestSetSynchronizationPoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.SetSynchronizationPoint(ctx, "Profile1") + if err != nil { + t.Fatalf("SetSynchronizationPoint() failed: %v", err) + } +} + +// TestGetOSDs tests GetOSDs operation +func TestGetOSDs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + osds, err := client.GetOSDs(ctx, "") + if err != nil { + t.Fatalf("GetOSDs() failed: %v", err) + } + + if len(osds) != 2 { + t.Errorf("Expected 2 OSDs, got %d", len(osds)) + } +} + +// TestGetOSD tests GetOSD operation +func TestGetOSD(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + osd, err := client.GetOSD(ctx, "OSD1") + if err != nil { + t.Fatalf("GetOSD() failed: %v", err) + } + + if osd.Token != "OSD1" { + t.Errorf("Expected token OSD1, got %s", osd.Token) + } +} + +// TestSetOSD tests SetOSD operation +func TestSetOSD(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + osd := &OSDConfiguration{ + Token: "OSD1", + } + + err = client.SetOSD(ctx, osd) + if err != nil { + t.Fatalf("SetOSD() failed: %v", err) + } +} + +// TestCreateOSD tests CreateOSD operation +func TestCreateOSD(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + osd, err := client.CreateOSD(ctx, "VideoSourceConfig1", nil) + if err != nil { + t.Fatalf("CreateOSD() failed: %v", err) + } + + if osd.Token != "NewOSD1" { + t.Errorf("Expected token NewOSD1, got %s", osd.Token) + } +} + +// TestDeleteOSD tests DeleteOSD operation +func TestDeleteOSD(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.DeleteOSD(ctx, "OSD1") + if err != nil { + t.Fatalf("DeleteOSD() failed: %v", err) + } +} + +// TestStartMulticastStreaming tests StartMulticastStreaming operation +func TestStartMulticastStreaming(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.StartMulticastStreaming(ctx, "Profile1") + if err != nil { + t.Fatalf("StartMulticastStreaming() failed: %v", err) + } +} + +// TestStopMulticastStreaming tests StopMulticastStreaming operation +func TestStopMulticastStreaming(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.StopMulticastStreaming(ctx, "Profile1") + if err != nil { + t.Fatalf("StopMulticastStreaming() failed: %v", err) + } +} + +// TestAddVideoEncoderConfiguration tests AddVideoEncoderConfiguration operation +func TestAddVideoEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.AddVideoEncoderConfiguration(ctx, "Profile1", "VideoEnc1") + if err != nil { + t.Fatalf("AddVideoEncoderConfiguration() failed: %v", err) + } +} + +// TestRemoveVideoEncoderConfiguration tests RemoveVideoEncoderConfiguration operation +func TestRemoveVideoEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.RemoveVideoEncoderConfiguration(ctx, "Profile1") + if err != nil { + t.Fatalf("RemoveVideoEncoderConfiguration() failed: %v", err) + } +} + +// TestAddAudioEncoderConfiguration tests AddAudioEncoderConfiguration operation +func TestAddAudioEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.AddAudioEncoderConfiguration(ctx, "Profile1", "AudioEnc1") + if err != nil { + t.Fatalf("AddAudioEncoderConfiguration() failed: %v", err) + } +} + +// TestRemoveAudioEncoderConfiguration tests RemoveAudioEncoderConfiguration operation +func TestRemoveAudioEncoderConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.RemoveAudioEncoderConfiguration(ctx, "Profile1") + if err != nil { + t.Fatalf("RemoveAudioEncoderConfiguration() failed: %v", err) + } +} + +// TestAddAudioSourceConfiguration tests AddAudioSourceConfiguration operation +func TestAddAudioSourceConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.AddAudioSourceConfiguration(ctx, "Profile1", "AudioSourceConfig1") + if err != nil { + t.Fatalf("AddAudioSourceConfiguration() failed: %v", err) + } +} + +// TestRemoveAudioSourceConfiguration tests RemoveAudioSourceConfiguration operation +func TestRemoveAudioSourceConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.RemoveAudioSourceConfiguration(ctx, "Profile1") + if err != nil { + t.Fatalf("RemoveAudioSourceConfiguration() failed: %v", err) + } +} + +// TestAddVideoSourceConfiguration tests AddVideoSourceConfiguration operation +func TestAddVideoSourceConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.AddVideoSourceConfiguration(ctx, "Profile1", "VideoSourceConfig1") + if err != nil { + t.Fatalf("AddVideoSourceConfiguration() failed: %v", err) + } +} + +// TestRemoveVideoSourceConfiguration tests RemoveVideoSourceConfiguration operation +func TestRemoveVideoSourceConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.RemoveVideoSourceConfiguration(ctx, "Profile1") + if err != nil { + t.Fatalf("RemoveVideoSourceConfiguration() failed: %v", err) + } +} + +// TestAddPTZConfiguration tests AddPTZConfiguration operation +func TestAddPTZConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.AddPTZConfiguration(ctx, "Profile1", "PTZConfig1") + if err != nil { + t.Fatalf("AddPTZConfiguration() failed: %v", err) + } +} + +// TestRemovePTZConfiguration tests RemovePTZConfiguration operation +func TestRemovePTZConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.RemovePTZConfiguration(ctx, "Profile1") + if err != nil { + t.Fatalf("RemovePTZConfiguration() failed: %v", err) + } +} + +// TestAddMetadataConfiguration tests AddMetadataConfiguration operation +func TestAddMetadataConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.AddMetadataConfiguration(ctx, "Profile1", "Metadata1") + if err != nil { + t.Fatalf("AddMetadataConfiguration() failed: %v", err) + } +} + +// TestRemoveMetadataConfiguration tests RemoveMetadataConfiguration operation +func TestRemoveMetadataConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + err = client.RemoveMetadataConfiguration(ctx, "Profile1") + if err != nil { + t.Fatalf("RemoveMetadataConfiguration() failed: %v", err) + } +} + +// TestGetAudioEncoderConfigurationOptions tests GetAudioEncoderConfigurationOptions operation +func TestGetAudioEncoderConfigurationOptions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + AAC + G711 + 64000 + 128000 + 44100 + 48000 + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + options, err := client.GetAudioEncoderConfigurationOptions(ctx, "AudioEnc1", "") + if err != nil { + t.Fatalf("GetAudioEncoderConfigurationOptions() failed: %v", err) + } + + if len(options.EncodingOptions) == 0 { + t.Error("Expected encoding options to be set") + } +} + +// TestGetMetadataConfigurationOptions tests GetMetadataConfigurationOptions operation +func TestGetMetadataConfigurationOptions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + true + true + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + options, err := client.GetMetadataConfigurationOptions(ctx, "Metadata1", "") + if err != nil { + t.Fatalf("GetMetadataConfigurationOptions() failed: %v", err) + } + + if options.PTZStatusFilterOptions == nil { + t.Error("Expected PTZStatusFilterOptions to be set") + } +} + +// TestGetAudioOutputConfiguration tests GetAudioOutputConfiguration operation +func TestGetAudioOutputConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + Audio Output Config + AudioOutput1 + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config, err := client.GetAudioOutputConfiguration(ctx, "AudioOutputConfig1") + if err != nil { + t.Fatalf("GetAudioOutputConfiguration() failed: %v", err) + } + + if config.Token != "AudioOutputConfig1" { + t.Errorf("Expected token AudioOutputConfig1, got %s", config.Token) + } +} + +// TestSetAudioOutputConfiguration tests SetAudioOutputConfiguration operation +func TestSetAudioOutputConfiguration(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(``)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + config := &AudioOutputConfiguration{ + Token: "AudioOutputConfig1", + Name: "Audio Output Config", + OutputToken: "AudioOutput1", + } + + err = client.SetAudioOutputConfiguration(ctx, config, true) + if err != nil { + t.Fatalf("SetAudioOutputConfiguration() failed: %v", err) + } +} + +// TestGetAudioOutputConfigurationOptions tests GetAudioOutputConfigurationOptions operation +func TestGetAudioOutputConfigurationOptions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + AudioOutput1 + AudioOutput2 + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + options, err := client.GetAudioOutputConfigurationOptions(ctx, "") + if err != nil { + t.Fatalf("GetAudioOutputConfigurationOptions() failed: %v", err) + } + + if len(options.OutputTokensAvailable) == 0 { + t.Error("Expected output tokens to be available") + } +} + +// TestGetAudioDecoderConfigurationOptions tests GetAudioDecoderConfigurationOptions operation +func TestGetAudioDecoderConfigurationOptions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + + 64000 + 128000 + 44100 + 48000 + + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + options, err := client.GetAudioDecoderConfigurationOptions(ctx, "") + if err != nil { + t.Fatalf("GetAudioDecoderConfigurationOptions() failed: %v", err) + } + + if options.AACDecOptions == nil { + t.Error("Expected AACDecOptions to be set") + } +} + +// TestGetGuaranteedNumberOfVideoEncoderInstances tests GetGuaranteedNumberOfVideoEncoderInstances operation +func TestGetGuaranteedNumberOfVideoEncoderInstances(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + 4 + 2 + 2 + 0 + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + instances, err := client.GetGuaranteedNumberOfVideoEncoderInstances(ctx, "VideoEnc1") + if err != nil { + t.Fatalf("GetGuaranteedNumberOfVideoEncoderInstances() failed: %v", err) + } + + if instances.TotalNumber != 4 { + t.Errorf("Expected TotalNumber 4, got %d", instances.TotalNumber) + } + + if instances.H264 != 2 { + t.Errorf("Expected H264 2, got %d", instances.H264) + } +} + +// TestGetOSDOptions tests GetOSDOptions operation +func TestGetOSDOptions(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := ` + + + + + 10 + + + +` + w.Header().Set("Content-Type", "application/soap+xml") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient(server.URL + "/onvif/media_service") + if err != nil { + t.Fatalf("NewClient() failed: %v", err) + } + + ctx := context.Background() + options, err := client.GetOSDOptions(ctx, "") + if err != nil { + t.Fatalf("GetOSDOptions() failed: %v", err) + } + + if options.MaximumNumberOfOSDs != 10 { + t.Errorf("Expected MaximumNumberOfOSDs 10, got %d", options.MaximumNumberOfOSDs) + } +} + diff --git a/types.go b/types.go index c8b93fc..be51cf5 100644 --- a/types.go +++ b/types.go @@ -322,6 +322,107 @@ type FilterType struct { // ProfileExtension represents profile extension type ProfileExtension struct{} +// MediaServiceCapabilities represents media service capabilities +type MediaServiceCapabilities struct { + SnapshotUri bool + Rotation bool + VideoSourceMode bool + OSD bool + TemporaryOSDText bool + EXICompression bool + MaximumNumberOfProfiles int + RTPMulticast bool + RTP_TCP bool + RTP_RTSP_TCP bool +} + +// VideoEncoderConfigurationOptions represents available options for video encoder configuration +type VideoEncoderConfigurationOptions struct { + QualityRange *FloatRange + JPEG *JPEGOptions + H264 *H264Options +} + +// JPEGOptions represents JPEG encoder options +type JPEGOptions struct { + ResolutionsAvailable []*VideoResolution + FrameRateRange *FloatRange + EncodingIntervalRange *IntRange +} + +// H264Options represents H264 encoder options +type H264Options struct { + ResolutionsAvailable []*VideoResolution + GovLengthRange *IntRange + FrameRateRange *FloatRange + EncodingIntervalRange *IntRange + H264ProfilesSupported []string +} + +// VideoSourceMode represents a video source mode +type VideoSourceMode struct { + Token string + Enabled bool + Resolution *VideoResolution +} + +// OSDConfiguration represents OSD (On-Screen Display) configuration +type OSDConfiguration struct { + Token string + // Additional fields can be added based on ONVIF spec +} + +// AudioEncoderConfigurationOptions represents available options for audio encoder configuration +type AudioEncoderConfigurationOptions struct { + EncodingOptions []string + BitrateList []int + SampleRateList []int +} + +// MetadataConfigurationOptions represents available options for metadata configuration +type MetadataConfigurationOptions struct { + PTZStatusFilterOptions *PTZFilter +} + +// AudioOutputConfiguration represents audio output configuration +type AudioOutputConfiguration struct { + Token string + Name string + UseCount int + OutputToken string +} + +// AudioOutputConfigurationOptions represents available options for audio output configuration +type AudioOutputConfigurationOptions struct { + OutputTokensAvailable []string +} + +// AudioDecoderConfigurationOptions represents available options for audio decoder configuration +type AudioDecoderConfigurationOptions struct { + AACDecOptions *AudioDecoderOptions + G711DecOptions *AudioDecoderOptions + G726DecOptions *AudioDecoderOptions +} + +// AudioDecoderOptions represents audio decoder options +type AudioDecoderOptions struct { + BitrateList []int + SampleRateList []int +} + +// GuaranteedNumberOfVideoEncoderInstances represents guaranteed number of video encoder instances +type GuaranteedNumberOfVideoEncoderInstances struct { + TotalNumber int + JPEG int + H264 int + MPEG4 int +} + +// OSDConfigurationOptions represents available options for OSD configuration +type OSDConfigurationOptions struct { + MaximumNumberOfOSDs int +} + // StreamSetup represents stream setup parameters type StreamSetup struct { Stream string // RTP-Unicast, RTP-Multicast