From 19e2e076e9c3d99f6fc86cdd298006d058d3e27c Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Tue, 1 Sep 2026 00:43:57 +0300 Subject: [PATCH] playback composition object --- cmd/mxl-player/playback_runtime.go | 127 ++++++++++++++++++++++++ cmd/mxl-player/playback_runtime_test.go | 42 ++++++++ imgui.ini | 2 +- 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 cmd/mxl-player/playback_runtime.go create mode 100644 cmd/mxl-player/playback_runtime_test.go diff --git a/cmd/mxl-player/playback_runtime.go b/cmd/mxl-player/playback_runtime.go new file mode 100644 index 0000000..8291fa1 --- /dev/null +++ b/cmd/mxl-player/playback_runtime.go @@ -0,0 +1,127 @@ +package main + +import ( + "log" + + mxladapter "mxl-player/internal/adapter/mxl" + "mxl-player/internal/output" + "mxl-player/internal/playback" +) + +type playerPlayback struct { + Controller *playback.SessionController + Commands chan playback.SessionCommand + Video *playback.VideoBridge + Status *playback.StatusStore + Audio *output.SDLAudioSink +} + +func newPlayerPlayback( + audioDevice uint32, + retry playback.RetryPolicy, +) (*playerPlayback, error) { + videoBridge := playback.NewVideoBridge() + statusStore := playback.NewStatusStore() + audioSink := output.NewSDLAudioSink(audioDevice) + + observe := func(status playback.Status) { + statusStore.Observe(status) + + if status.Err != nil { + log.Printf( + "%s: state=%v attempt=%d failed=%d: %v", + status.Unit, + status.State, + status.Attempt, + status.FailedAttempts, + status.Err, + ) + return + } + + log.Printf( + "%s: state=%v attempt=%d failed=%d", + status.Unit, + status.State, + status.Attempt, + status.FailedAttempts, + ) + } + + videoWorker, err := playback.NewVideoWorker( + mxladapter.VideoFactory{}, + videoBridge, + retry, + mxladapter.ShouldRetry, + observe, + ) + if err != nil { + _ = audioSink.Close() + return nil, err + } + videoSlot, err := playback.NewVideoSlot(videoWorker) + if err != nil { + _ = audioSink.Close() + return nil, err + } + + audioWorker, err := playback.NewAudioWorker( + mxladapter.AudioFactory{}, + audioSink, + retry, + mxladapter.ShouldRetry, + observe, + ) + if err != nil { + _ = audioSink.Close() + return nil, err + } + audioSlot, err := playback.NewAudioSlot(audioWorker) + if err != nil { + _ = audioSink.Close() + return nil, err + } + + syncWorker, err := playback.NewSyncWorker( + mxladapter.SyncFactory{}, + videoBridge, + audioSink, + retry, + mxladapter.ShouldRetry, + observe, + ) + if err != nil { + _ = audioSink.Close() + return nil, err + } + syncSlot, err := playback.NewSyncSlot(syncWorker) + if err != nil { + _ = audioSink.Close() + return nil, err + } + + controller, err := playback.NewSessionController( + videoSlot, + audioSlot, + syncSlot, + func(video, audio playback.FeedConfig) bool { + return video.Domain == audio.Domain + }, + ) + if err != nil { + _ = audioSink.Close() + return nil, err + } + + return &playerPlayback{ + Controller: controller, + Commands: make(chan playback.SessionCommand, 32), + Video: videoBridge, + Status: statusStore, + Audio: audioSink, + }, nil +} + +func (p *playerPlayback) Close() error { + return p.Audio.Close() +} diff --git a/cmd/mxl-player/playback_runtime_test.go b/cmd/mxl-player/playback_runtime_test.go new file mode 100644 index 0000000..4cc50dd --- /dev/null +++ b/cmd/mxl-player/playback_runtime_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "testing" + "time" + + "mxl-player/internal/playback" +) + +func validRuntimeRetryPolicy() playback.RetryPolicy { + return playback.RetryPolicy{ + MaxAttempts: 3, + InitialDelay: time.Millisecond, + MaxDelay: time.Second, + } +} + +func TestNewPlayerPlaybackBuildsCompleteRuntime(t *testing.T) { + runtime, err := newPlayerPlayback(123, validRuntimeRetryPolicy()) + if err != nil { + t.Fatalf("newPlayerPlayback() error = %v", err) + } + if runtime.Controller == nil || runtime.Video == nil || runtime.Status == nil || runtime.Audio == nil { + t.Fatalf("newPlayerPlayback() = %#v", runtime) + } + if runtime.Commands == nil || cap(runtime.Commands) != 32 { + t.Fatalf("command channel = %#v, capacity = %d", runtime.Commands, cap(runtime.Commands)) + } + if err := runtime.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } +} + +func TestNewPlayerPlaybackRejectsInvalidRetryPolicy(t *testing.T) { + runtime, err := newPlayerPlayback(123, playback.RetryPolicy{}) + if runtime != nil { + t.Fatalf("newPlayerPlayback() runtime = %#v, want nil", runtime) + } + if err == nil { + t.Fatal("newPlayerPlayback() error is nil") + } +} diff --git a/imgui.ini b/imgui.ini index 6a67a49..e3ffc14 100644 --- a/imgui.ini +++ b/imgui.ini @@ -14,7 +14,7 @@ Size=200,200 Collapsed=0 [Window][Connection] -Pos=628,539 +Pos=1322,937 Size=613,164 Collapsed=0