From a4626fbb19bed535df03f099c8ed411fde4aab53 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Tue, 1 Sep 2026 01:28:36 +0300 Subject: [PATCH] Made the Connection panel more usable for runtime testing --- cmd/mxl-player/main.go | 142 ++++++++++++++++++------- internal/playback/session_plan.go | 13 +++ internal/playback/session_plan_test.go | 17 +++ 3 files changed, 132 insertions(+), 40 deletions(-) diff --git a/cmd/mxl-player/main.go b/cmd/mxl-player/main.go index 5bbdf50..465b526 100644 --- a/cmd/mxl-player/main.go +++ b/cmd/mxl-player/main.go @@ -368,6 +368,28 @@ func main() { }, }) } + drawUnitStatus := func(label string, unit playback.Unit) { + status, ok := statusStore.Snapshot(unit) + if !ok { + cimgui.Text(fmt.Sprintf("%s: not started", label)) + return + } + cimgui.Text(fmt.Sprintf("%s: %s", label, status.State)) + cimgui.Text(fmt.Sprintf( + "Attempt: %d, failed: %d", + status.Attempt, + status.FailedAttempts, + )) + if status.RetryIn > 0 { + cimgui.Text(fmt.Sprintf( + "Retry in: %s", + status.RetryIn.Round(time.Millisecond), + )) + } + if status.Err != nil { + cimgui.TextWrapped(status.Err.Error()) + } + } playbackDone := make(chan error, 1) go func() { @@ -524,20 +546,48 @@ func main() { cimgui.End() } cimgui.Begin("Connection") - cimgui.InputTextWithHint("Video domain", "/dev/shm/mxl", &videoDomainStr, 0, nil) - cimgui.InputTextWithHint("Audio domain", "/dev/shm/mxl", &audioDomainStr, 0, nil) - cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil) - cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil) - if snapshot, ok := player.Controller.Snapshot(); ok { + + snapshot, hasSnapshot := player.Controller.Snapshot() + videoConfigured := videoStr != "" + audioConfigured := audioStr != "" + if hasSnapshot { videoActive = snapshot.Desired.Video.Active audioActive = snapshot.Desired.Audio.Active + videoConfigured = snapshot.Desired.Video.IsConfigured() + audioConfigured = snapshot.Desired.Audio.IsConfigured() syncRequested = snapshot.Desired.SyncRequested + cimgui.Text(fmt.Sprintf( + "Topology: %s (generation %d)", + snapshot.Plan.Topology, + snapshot.Generation, + )) + } else { + cimgui.Text("Topology: starting") } - if cimgui.Button("Connect") { + + cimgui.Separator() + cimgui.InputTextWithHint("Video domain", "/dev/shm/mxl", &videoDomainStr, 0, nil) + cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil) + cimgui.InputTextWithHint("Audio domain", "/dev/shm/mxl", &audioDomainStr, 0, nil) + cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil) + if cimgui.Button("Apply feeds") { doReconnect() } cimgui.SameLine() - cimgui.Checkbox("Show stats", &showStats) + if videoActive || audioActive { + if cimgui.Button("Stop all") { + enqueueCommand(playback.SessionCommand{Kind: playback.CommandStopAll}) + } + } + if (videoConfigured && !videoActive) || (audioConfigured && !audioActive) { + if videoActive || audioActive { + cimgui.SameLine() + } + if cimgui.Button("Resume all") { + enqueueCommand(playback.SessionCommand{Kind: playback.CommandResumeAll}) + } + } + if cimgui.Checkbox("Synchronize", &syncRequested) { kind := playback.CommandDisableSync if syncRequested { @@ -545,20 +595,36 @@ func main() { } enqueueCommand(playback.SessionCommand{Kind: kind}) } + if hasSnapshot && syncRequested && snapshot.Plan.Topology != playback.TopologySynchronized { + switch { + case !videoConfigured || !audioConfigured: + cimgui.TextWrapped("Sync requested: waiting for both feeds to be configured.") + case !videoActive || !audioActive: + cimgui.TextWrapped("Sync requested: waiting for both feeds to be active.") + case snapshot.Desired.Video.Domain != snapshot.Desired.Audio.Domain: + cimgui.TextWrapped("Sync requested, but native MXL sync requires matching domains. Playing independently.") + default: + cimgui.TextWrapped("Sync requested but currently unavailable. Playing independently.") + } + } + + cimgui.Separator() + cimgui.Text("Video") if videoActive { if cimgui.Button("Stop video") { videoActive = false enqueueCommand(playback.SessionCommand{Kind: playback.CommandStopVideo}) } } - if !videoActive && videoStr != "" { + if !videoActive && videoConfigured { cimgui.SameLine() if cimgui.Button("Resume video") { videoActive = true enqueueCommand(playback.SessionCommand{Kind: playback.CommandResumeVideo}) } } - if videoStr != "" { + if videoConfigured { + cimgui.SameLine() if cimgui.Button("Remove video") { videoActive = false videoStr = "" @@ -567,24 +633,14 @@ func main() { } if videoActive { cimgui.Text("Video desired: active") - } else if videoStr != "" { + } else if videoConfigured { cimgui.Text("Video desired: stopped") } else { cimgui.Text("Video desired: not configured") } - if status, ok := statusStore.Snapshot(playback.UnitVideo); ok { - cimgui.Text(fmt.Sprintf("Video actual: %s", status.State)) - cimgui.Text(fmt.Sprintf("Attempt: %d, failed: %d", status.Attempt, status.FailedAttempts)) - if status.RetryIn > 0 { - cimgui.Text(fmt.Sprintf("Retry in: %s", status.RetryIn.Round(time.Millisecond))) - } - if status.Err != nil { - cimgui.TextWrapped(status.Err.Error()) - } - } else { - cimgui.Text("Video actual: not started") - } + cimgui.Separator() + cimgui.Text("Audio") if audioActive { if cimgui.Button("Stop audio") { audioActive = false @@ -592,14 +648,16 @@ func main() { } } - if !audioActive && audioStr != "" { + if !audioActive && audioConfigured { + cimgui.SameLine() if cimgui.Button("Resume audio") { audioActive = true enqueueCommand(playback.SessionCommand{Kind: playback.CommandResumeAudio}) } } - if audioStr != "" { + if audioConfigured { + cimgui.SameLine() if cimgui.Button("Remove audio") { audioActive = false audioStr = "" @@ -609,31 +667,35 @@ func main() { if audioActive { cimgui.Text("Audio desired: active") - } else if audioStr != "" { + } else if audioConfigured { cimgui.Text("Audio desired: stopped") } else { cimgui.Text("Audio desired: not configured") } - if status, ok := statusStore.Snapshot(playback.UnitAudio); ok { - cimgui.Text(fmt.Sprintf("Audio actual: %s", status.State)) - cimgui.Text(fmt.Sprintf("Attempt: %d, failed: %d", status.Attempt, status.FailedAttempts)) - if status.RetryIn > 0 { - cimgui.Text(fmt.Sprintf("Retry in: %s", status.RetryIn.Round(time.Millisecond))) - } - if status.Err != nil { - cimgui.TextWrapped(status.Err.Error()) + cimgui.Separator() + cimgui.Text("Current playback") + if hasSnapshot { + switch snapshot.Plan.Topology { + case playback.TopologySynchronized: + drawUnitStatus("Synchronized group", playback.UnitSync) + case playback.TopologyIndependent: + if snapshot.Plan.Video.Active { + drawUnitStatus("Video", playback.UnitVideo) + } + if snapshot.Plan.Audio.Active { + drawUnitStatus("Audio", playback.UnitAudio) + } + case playback.TopologyIdle: + cimgui.Text("No active feeds") } } else { - cimgui.Text("Audio actual: not started") - } - if status, ok := statusStore.Snapshot(playback.UnitSync); ok { - cimgui.Text(fmt.Sprintf("Sync actual: %s", status.State)) - if status.Err != nil { - cimgui.TextWrapped(status.Err.Error()) - } + cimgui.Text("Playback controller is starting") } + cimgui.Separator() + cimgui.Checkbox("Show stats", &showStats) + cimgui.End() gui.EndFrame() lastFrame = time.Now() diff --git a/internal/playback/session_plan.go b/internal/playback/session_plan.go index 4a80de7..9b799de 100644 --- a/internal/playback/session_plan.go +++ b/internal/playback/session_plan.go @@ -10,6 +10,19 @@ const ( TopologySynchronized ) +func (t SessionTopology) String() string { + switch t { + case TopologyIdle: + return "idle" + case TopologyIndependent: + return "independent" + case TopologySynchronized: + return "synchronized" + default: + return fmt.Sprintf("SessionTopology(%d)", uint8(t)) + } +} + type SyncPredicate func(video, audio FeedConfig) bool type SessionPlan struct { diff --git a/internal/playback/session_plan_test.go b/internal/playback/session_plan_test.go index b548e0f..4a792e2 100644 --- a/internal/playback/session_plan_test.go +++ b/internal/playback/session_plan_test.go @@ -5,6 +5,23 @@ import ( "testing" ) +func TestSessionTopologyString(t *testing.T) { + tests := []struct { + topology SessionTopology + want string + }{ + {TopologyIdle, "idle"}, + {TopologyIndependent, "independent"}, + {TopologySynchronized, "synchronized"}, + {SessionTopology(99), "SessionTopology(99)"}, + } + for _, tt := range tests { + if got := tt.topology.String(); got != tt.want { + t.Errorf("%d.String() = %q, want %q", tt.topology, got, tt.want) + } + } +} + func TestBuildSessionPlan(t *testing.T) { base := validCommandSession() stoppedVideo := stoppedFeed(base.Video)