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