Refactoring #3

Merged
itten merged 87 commits from refactoring into main 2026-09-01 23:52:36 +03:00
7 changed files with 260 additions and 5 deletions
Showing only changes of commit b0e7bc4cc3 - Show all commits
+104
View File
@@ -701,6 +701,110 @@ func main() {
drawFeedsSections()
}
if playlistRuntime != nil &&
cimgui.CollapsingHeaderTreeNodeFlagsV("Playlist", collapsingHeaderFlags) {
playlistSnapshot, hasPlaylistSnapshot :=
playlistRuntime.Controller.Snapshot()
cimgui.TextWrapped(fmt.Sprintf("File: %s", args.PlaylistPath))
cimgui.Text(fmt.Sprintf("Entries: %d", len(configuredPlaylist.Entries)))
if configuredPlaylist.Loop {
cimgui.Text("End behavior: loop")
} else {
cimgui.Text("End behavior: stop")
}
preview := "No entry selected"
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
preview = playlistEntryDisplayName(
playlistSnapshot.Entry,
playlistSnapshot.State.CurrentIndex,
)
}
if cimgui.BeginCombo("Entry##playlist", preview) {
for index, entry := range configuredPlaylist.Entries {
selected := hasPlaylistSnapshot &&
playlistSnapshot.State.HasSelection &&
playlistSnapshot.State.CurrentIndex == index
label := fmt.Sprintf(
"%s##playlist-entry-%d",
playlistEntryDisplayName(entry, index),
index,
)
if cimgui.SelectableBoolV(
label,
selected,
cimgui.SelectableFlagsNone,
cimgui.Vec2{},
) && !playlistRuntime.Select(index) {
log.Print("playlist command queue is full")
}
if selected {
cimgui.SetItemDefaultFocus()
}
}
cimgui.EndCombo()
}
if cimgui.Button("Previous##playlist") && !playlistRuntime.Previous() {
log.Print("playlist command queue is full")
}
cimgui.SameLine()
if cimgui.Button("Next##playlist") && !playlistRuntime.Next() {
log.Print("playlist command queue is full")
}
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
entry := playlistSnapshot.Entry
cimgui.SeparatorText("Current entry")
cimgui.Text(fmt.Sprintf(
"%d of %d: %s",
playlistSnapshot.State.CurrentIndex+1,
len(configuredPlaylist.Entries),
playlistEntryDisplayName(entry, playlistSnapshot.State.CurrentIndex),
))
if entry.Video.IsConfigured() {
cimgui.TextWrapped(fmt.Sprintf(
"Video: %s (%s)",
entry.Video.UUID,
entry.Video.Domain,
))
}
if entry.Audio.IsConfigured() {
cimgui.TextWrapped(fmt.Sprintf(
"Audio: %s (%s)",
entry.Audio.UUID,
entry.Audio.Domain,
))
}
if entry.SyncRequested {
cimgui.Text("Synchronization: requested")
} else {
cimgui.Text("Synchronization: independent")
}
switch {
case entry.Duration == 0:
cimgui.Text("Timing: manual advance")
case playlistSnapshot.Timing.Started:
fraction, remaining := playlistTimingProgress(
playlistSnapshot.Timing,
time.Now(),
)
cimgui.Text(fmt.Sprintf("Duration: %s", entry.Duration))
cimgui.ProgressBarV(
fraction,
cimgui.Vec2{X: -1, Y: 0},
remaining.Round(time.Second).String(),
)
case playlistSnapshot.Timing.Expired:
cimgui.Text("Timing: finished")
default:
cimgui.Text("Timing: waiting for playback")
}
}
}
drawHotkeysSection := func() {
cimgui.Text("F1 - show/hide stats")
cimgui.Text("F2 - show/hide settings")
+39
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"errors"
"fmt"
"time"
"mxl-player/internal/playback"
@@ -135,3 +136,41 @@ func shouldAutoStartPlaylist(
args.AudioFlowId == "" &&
len(playlist.Entries) > 0
}
func playlistEntryDisplayName(entry playback.PlaylistEntry, index int) string {
if entry.Name != "" {
return entry.Name
}
return fmt.Sprintf("Entry %d", index+1)
}
func playlistTimingProgress(
timing playback.PlaylistTimingState,
now time.Time,
) (float32, time.Duration) {
if timing.Duration <= 0 {
return 0, 0
}
if timing.Expired {
return 1, 0
}
if !timing.Started {
return 0, timing.Duration
}
remaining := timing.Deadline.Sub(now)
if remaining < 0 {
remaining = 0
}
if remaining > timing.Duration {
remaining = timing.Duration
}
fraction := 1 - float32(remaining)/float32(timing.Duration)
if fraction < 0 {
fraction = 0
}
if fraction > 1 {
fraction = 1
}
return fraction, remaining
}
+73
View File
@@ -298,6 +298,79 @@ func TestShouldAutoStartPlaylist(t *testing.T) {
}
}
func TestPlaylistEntryDisplayName(t *testing.T) {
tests := []struct {
entry playback.PlaylistEntry
index int
want string
}{
{entry: playback.PlaylistEntry{Name: "News"}, index: 0, want: "News"},
{entry: playback.PlaylistEntry{}, index: 0, want: "Entry 1"},
{entry: playback.PlaylistEntry{}, index: 4, want: "Entry 5"},
}
for _, test := range tests {
if got := playlistEntryDisplayName(test.entry, test.index); got != test.want {
t.Fatalf("playlistEntryDisplayName() = %q, want %q", got, test.want)
}
}
}
func TestPlaylistTimingProgress(t *testing.T) {
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
timing playback.PlaylistTimingState
wantFraction float32
wantRemaining time.Duration
}{
{name: "manual"},
{
name: "waiting",
timing: playback.PlaylistTimingState{Duration: 10 * time.Second},
wantRemaining: 10 * time.Second,
},
{
name: "half complete",
timing: playback.PlaylistTimingState{
Duration: 10 * time.Second,
Started: true,
Deadline: now.Add(5 * time.Second),
},
wantFraction: 0.5,
wantRemaining: 5 * time.Second,
},
{
name: "expired",
timing: playback.PlaylistTimingState{Duration: 10 * time.Second, Expired: true},
wantFraction: 1,
},
{
name: "deadline passed",
timing: playback.PlaylistTimingState{
Duration: 10 * time.Second,
Started: true,
Deadline: now.Add(-time.Second),
},
wantFraction: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fraction, remaining := playlistTimingProgress(test.timing, now)
if fraction != test.wantFraction || remaining != test.wantRemaining {
t.Fatalf(
"playlistTimingProgress() = %v, %v; want %v, %v",
fraction,
remaining,
test.wantFraction,
test.wantRemaining,
)
}
})
}
}
func newPlaylistTestPlayer(t *testing.T) *playerPlayback {
t.Helper()
return &playerPlayback{
+2 -2
View File
@@ -29,8 +29,8 @@ Size=550,720
Collapsed=0
[Window][Settings & Info]
Pos=580,0
Size=700,720
Pos=1220,0
Size=700,1080
Collapsed=0
[Window][Seetings & Info]
+7 -2
View File
@@ -6,6 +6,7 @@ type PlaylistTimingState struct {
Revision uint64
Duration time.Duration
Started bool
Expired bool
Deadline time.Time
}
@@ -16,6 +17,7 @@ func NewPlaylistTiming(
return PlaylistTimingState{
Revision: revision,
Duration: duration,
Expired: false,
}
}
@@ -24,10 +26,12 @@ func StartPlaylistTiming(
revision uint64,
now time.Time,
) (PlaylistTimingState, bool) {
if revision != current.Revision || current.Duration <= 0 || current.Started {
if revision != current.Revision ||
current.Duration <= 0 ||
current.Started ||
current.Expired {
return current, false
}
next := current
next.Started = true
next.Deadline = now.Add(current.Duration)
@@ -48,5 +52,6 @@ func ExpirePlaylistTiming(
next := current
next.Started = false
next.Deadline = time.Time{}
next.Expired = true
return next, true
}
+2 -1
View File
@@ -47,6 +47,7 @@ func TestStartPlaylistTimingIgnoresInapplicableReadiness(t *testing.T) {
{name: "stale revision", current: NewPlaylistTiming(4, time.Second), revision: 3},
{name: "future revision", current: NewPlaylistTiming(4, time.Second), revision: 5},
{name: "already started", current: started, revision: 4},
{name: "expired", current: PlaylistTimingState{Revision: 4, Duration: time.Second, Expired: true}, revision: 4},
}
for _, test := range tests {
@@ -70,7 +71,7 @@ func TestExpirePlaylistTiming(t *testing.T) {
if !expired {
t.Fatal("ExpirePlaylistTiming() expired = false, want true")
}
want := PlaylistTimingState{Revision: 9, Duration: 5 * time.Second}
want := PlaylistTimingState{Revision: 9, Duration: 5 * time.Second, Expired: true}
if got != want {
t.Fatalf("ExpirePlaylistTiming() = %#v, want %#v", got, want)
}
+33
View File
@@ -0,0 +1,33 @@
{
"loop": true,
"entries": [
{
"name": "Synchronized feed",
"video": {
"domain": "/dev/shm/mxl",
"uuid": "video-uuid"
},
"audio": {
"domain": "/dev/shm/mxl",
"uuid": "audio-uuid"
},
"sync": true,
"duration": "10s"
},
{
"name": "Video only",
"video": {
"domain": "/another/domain",
"uuid": "video-uuid"
},
"duration": "15s"
},
{
"name": "Manual audio",
"audio": {
"domain": "/dev/shm/audio",
"uuid": "audio-uuid"
}
}
]
}