From 87b012db80a34b0bd25f68408c2cc3c59f0b1591 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Thu, 27 Aug 2026 09:27:14 +0300 Subject: [PATCH] add local MXL video adapter --- internal/adapter/mxl/retry.go | 1 + internal/adapter/mxl/retry_test.go | 2 +- internal/adapter/mxl/video.go | 91 ++++++++++++++++++++++++++++++ internal/adapter/mxl/video_test.go | 75 ++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 internal/adapter/mxl/video.go create mode 100644 internal/adapter/mxl/video_test.go diff --git a/internal/adapter/mxl/retry.go b/internal/adapter/mxl/retry.go index 0cbbe92..b54393f 100644 --- a/internal/adapter/mxl/retry.go +++ b/internal/adapter/mxl/retry.go @@ -3,6 +3,7 @@ package mxladapter import ( "context" "errors" + "mxl-player/internal/source" ) diff --git a/internal/adapter/mxl/retry_test.go b/internal/adapter/mxl/retry_test.go index 5475ea4..4d8f05b 100644 --- a/internal/adapter/mxl/retry_test.go +++ b/internal/adapter/mxl/retry_test.go @@ -67,7 +67,7 @@ func TestShouldRetrySourceError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ShouldRetry(tt.err); got != tt.want { - t.Errorf("shouldRetrySourceError() = %t, want %t", got, tt.want) + t.Errorf("ShouldRetry() = %t, want %t", got, tt.want) } }) } diff --git a/internal/adapter/mxl/video.go b/internal/adapter/mxl/video.go new file mode 100644 index 0000000..45f69e5 --- /dev/null +++ b/internal/adapter/mxl/video.go @@ -0,0 +1,91 @@ +package mxladapter + +import ( + "context" + "errors" + "fmt" + "time" + + "mxl-player/internal/playback" + "mxl-player/internal/source" +) + +const DefaultVideoReadTimeout = 200 * time.Millisecond + +type VideoFactory struct { + ReadTimeout time.Duration +} + +type videoReader struct { + source *source.Source + timeout time.Duration +} + +var _ playback.VideoReaderFactory = VideoFactory{} +var _ playback.VideoReader = (*videoReader)(nil) + +func (f VideoFactory) OpenVideo( + ctx context.Context, + config playback.FeedConfig, +) (playback.VideoReader, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if err := config.Validate(); err != nil { + return nil, &source.SourceError{ + Op: "validate video feed", + Kind: source.ErrorKindInvalidConfig, + Err: err, + } + } + if !config.IsConfigured() { + return nil, &source.SourceError{ + Op: "validate video feed", + Kind: source.ErrorKindInvalidConfig, + Err: errors.New("video feed is not configured"), + } + } + + src, err := source.Open(config.Domain, config.UUID) + if err != nil { + return nil, fmt.Errorf("open local MXL video: %w", err) + } + + if err := ctx.Err(); err != nil { + _ = src.Close() + return nil, err + } + + timeout := f.ReadTimeout + if timeout <= 0 { + timeout = DefaultVideoReadTimeout + } + + return &videoReader{ + source: src, + timeout: timeout, + }, nil +} + +func (r *videoReader) ReadVideo( + ctx context.Context, +) (playback.VideoFrame, error) { + frame, err := r.source.NextCtx(ctx, r.timeout) + if err != nil { + return playback.VideoFrame{}, err + } + + return playback.VideoFrame{ + Index: frame.Index, + Width: frame.Width, + Height: frame.Height, + Stride: frame.Stride, + Size: frame.Size, + Invalid: frame.Invalid, + Payload: frame.Payload, + }, nil +} + +func (r *videoReader) Close() error { + return r.source.Close() +} diff --git a/internal/adapter/mxl/video_test.go b/internal/adapter/mxl/video_test.go new file mode 100644 index 0000000..94467a8 --- /dev/null +++ b/internal/adapter/mxl/video_test.go @@ -0,0 +1,75 @@ +package mxladapter + +import ( + "context" + "errors" + "testing" + + "mxl-player/internal/playback" + "mxl-player/internal/source" +) + +func TestVideoFactoryOpenVideoCanceled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + reader, err := (VideoFactory{}).OpenVideo(ctx, playback.FeedConfig{}) + if reader != nil { + t.Fatal("OpenVideo() reader is not nil after cancellation") + } + if !errors.Is(err, context.Canceled) { + t.Fatalf("OpenVideo() error = %v, want context.Canceled", err) + } +} + +func TestVideoFactoryOpenVideoRejectsInvalidConfig(t *testing.T) { + tests := []struct { + name string + config playback.FeedConfig + }{ + { + name: "feed is not configured", + config: playback.FeedConfig{}, + }, + { + name: "active feed has no UUID", + config: playback.FeedConfig{ + Domain: "/dev/shm/mxl", + Active: true, + }, + }, + { + name: "configured feed has no domain", + config: playback.FeedConfig{ + UUID: "video-uuid", + Active: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader, err := (VideoFactory{}).OpenVideo( + context.Background(), + tt.config, + ) + + if reader != nil { + t.Fatal("OpenVideo() reader is not nil for invalid config") + } + if err == nil { + t.Fatal("OpenVideo() error is nil for invalid config") + } + if got := source.KindOf(err); got != source.ErrorKindInvalidConfig { + t.Fatalf( + "source.KindOf(OpenVideo()) = %v, want %v", + got, + source.ErrorKindInvalidConfig, + ) + } + if ShouldRetry(err) { + t.Fatal("ShouldRetry(OpenVideo()) = true for invalid config") + } + }) + } +}