From 1b667dcdb3ccff7c12469241a16aed30a01f1a9c Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Wed, 26 Aug 2026 20:52:13 +0300 Subject: [PATCH] playback config model --- internal/playback/config.go | 78 +++++++++++ internal/playback/config_test.go | 214 +++++++++++++++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 internal/playback/config.go create mode 100644 internal/playback/config_test.go diff --git a/internal/playback/config.go b/internal/playback/config.go new file mode 100644 index 0000000..7073989 --- /dev/null +++ b/internal/playback/config.go @@ -0,0 +1,78 @@ +package playback + +import ( + "errors" + "fmt" + "time" +) + +var ( + ErrFeedDomainRequired = errors.New("feed domain is required when its UUID is configured") + ErrInvalidMaxAttempts = errors.New("max attempts cannot be negative") + ErrInvalidRetryDelay = errors.New("retry delay must be positive") + ErrInvalidRetryRange = errors.New("maximum retry delay cannot be less than initial retry delay") + ErrActiveFeedNotConfigured = errors.New("feed cannot be active without a UUID") +) + +type FeedConfig struct { + Domain string + UUID string + Active bool +} + +type RetryPolicy struct { + MaxAttempts int // 0 = unlimited + InitialDelay time.Duration + MaxDelay time.Duration +} + +type SessionConfig struct { + Video FeedConfig + Audio FeedConfig + SyncRequested bool + Retry RetryPolicy +} + +func (f FeedConfig) IsConfigured() bool { + return f.UUID != "" +} + +func (f FeedConfig) Validate() error { + if f.Active && !f.IsConfigured() { + return ErrActiveFeedNotConfigured + } + if f.IsConfigured() && f.Domain == "" { + return ErrFeedDomainRequired + } + return nil +} + +func (p RetryPolicy) Validate() error { + if p.MaxAttempts < 0 { + return ErrInvalidMaxAttempts + } + if p.InitialDelay <= 0 || p.MaxDelay <= 0 { + return ErrInvalidRetryDelay + } + if p.MaxDelay < p.InitialDelay { + return ErrInvalidRetryRange + } + return nil +} + +func (c SessionConfig) HasFeeds() bool { + return c.Video.IsConfigured() || c.Audio.IsConfigured() +} + +func (c SessionConfig) Validate() error { + if err := c.Video.Validate(); err != nil { + return fmt.Errorf("video: %w", err) + } + if err := c.Audio.Validate(); err != nil { + return fmt.Errorf("audio: %w", err) + } + if err := c.Retry.Validate(); err != nil { + return fmt.Errorf("retry: %w", err) + } + return nil +} diff --git a/internal/playback/config_test.go b/internal/playback/config_test.go new file mode 100644 index 0000000..87e4f4e --- /dev/null +++ b/internal/playback/config_test.go @@ -0,0 +1,214 @@ +package playback + +import ( + "errors" + "testing" + "time" +) + +func TestSessionConfigValidate(t *testing.T) { + validRetry := RetryPolicy{ + MaxAttempts: 5, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + } + + tests := []struct { + name string + config SessionConfig + wantErr error + }{ + { + name: "audio and video in different domains", + config: SessionConfig{ + Video: FeedConfig{ + Domain: "/dev/shm/mxl-video", + UUID: "video-uuid", + Active: true, + }, + Audio: FeedConfig{ + Domain: "/dev/shm/mxl-audio", + UUID: "audio-uuid", + Active: true, + }, + SyncRequested: true, + Retry: validRetry, + }, + wantErr: nil, + }, + { + name: "audio and video in same domain", + config: SessionConfig{ + Video: FeedConfig{ + Domain: "/dev/shm/mxl", + UUID: "video-uuid", + Active: true, + }, + Audio: FeedConfig{ + Domain: "/dev/shm/mxl", + UUID: "audio-uuid", + Active: true, + }, + SyncRequested: true, + Retry: validRetry, + }, + wantErr: nil, + }, + { + name: "video only", + config: SessionConfig{ + Video: FeedConfig{ + Domain: "/dev/shm/mxl", + UUID: "video-uuid", + Active: true, + }, + Retry: validRetry, + }, + wantErr: nil, + }, + { + name: "audio only", + config: SessionConfig{ + Audio: FeedConfig{ + Domain: "/dev/shm/mxl", + UUID: "audio-uuid", + Active: true, + }, + Retry: validRetry, + }, + wantErr: nil, + }, + { + name: "video UUID without domain", + config: SessionConfig{ + Video: FeedConfig{ + UUID: "video-uuid", + Active: true, + }, + Retry: validRetry, + }, + wantErr: ErrFeedDomainRequired, + }, + { + name: "audio without domain", + config: SessionConfig{ + Audio: FeedConfig{ + Domain: "", + UUID: "audio-uuid", + Active: true, + }, + SyncRequested: true, + Retry: validRetry, + }, + wantErr: ErrFeedDomainRequired, + }, + { + name: "sync with only one feed", + config: SessionConfig{ + Video: FeedConfig{ + Domain: "/dev/shm/mxl-video", + UUID: "video-uuid", + Active: true, + }, + SyncRequested: true, + Retry: validRetry, + }, + wantErr: nil, + }, + { + name: "empty player with valid retry policy", + config: SessionConfig{ + Retry: validRetry, + }, + wantErr: nil, + }, + { + name: "negative MaxAttempts", + config: SessionConfig{ + Retry: RetryPolicy{ + MaxAttempts: -100, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 10 * time.Second, + }, + }, + wantErr: ErrInvalidMaxAttempts, + }, + { + name: "zero InitialDelay", + config: SessionConfig{ + Retry: RetryPolicy{ + MaxAttempts: 0, + InitialDelay: 0, + MaxDelay: 10 * time.Second, + }, + }, + wantErr: ErrInvalidRetryDelay, + }, + { + name: "zero MaxDelay", + config: SessionConfig{ + Retry: RetryPolicy{ + MaxAttempts: 0, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 0, + }, + }, + wantErr: ErrInvalidRetryDelay, + }, + { + name: "MaxDelay smaller than InitialDelay", + config: SessionConfig{ + Retry: RetryPolicy{ + MaxAttempts: 0, + InitialDelay: 500 * time.Millisecond, + MaxDelay: 200 * time.Millisecond, + }, + }, + wantErr: ErrInvalidRetryRange, + }, + { + name: "configured video is stopped", + config: SessionConfig{ + Video: FeedConfig{ + Domain: "/dev/shm/mxl-video", + UUID: "video-uuid", + Active: false, + }, + Retry: validRetry, + }, + wantErr: nil, + }, + { + name: "active video without UUID", + config: SessionConfig{ + Video: FeedConfig{ + Domain: "/dev/shm/mxl-video", + Active: true, + }, + Retry: validRetry, + }, + wantErr: ErrActiveFeedNotConfigured, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + + if tt.wantErr == nil { + if err != nil { + t.Fatalf("Validate() returned unexpected error: %v", err) + } + return + } + + if !errors.Is(err, tt.wantErr) { + t.Fatalf( + "Validate() error = %v, want error matching %v", + err, + tt.wantErr, + ) + } + }) + } +}