explicit info in GUI for playlist

This commit is contained in:
Dmitry Sergeev
2026-09-01 23:59:45 +03:00
parent 841c14f497
commit ea8c9eeddd
5 changed files with 125 additions and 19 deletions
+27
View File
@@ -811,6 +811,33 @@ func main() {
} else {
cimgui.Text("End behavior: stop")
}
cimgui.Text(fmt.Sprintf(
"Failure behavior: %s",
configuredPlaylist.OnFailure,
))
if hasPlaylistSnapshot && playlistSnapshot.HasFailure {
failure := playlistSnapshot.Failure
name := failure.EntryName
if name == "" {
name = fmt.Sprintf("Entry %d", failure.EntryIndex+1)
}
cimgui.SeparatorText("Last failure")
cimgui.TextWrapped(fmt.Sprintf(
"%s: %s failed",
name,
failure.Status.Unit,
))
cimgui.Text(fmt.Sprintf(
"Attempts: %d | failed attempts: %d",
failure.Status.Attempt,
failure.Status.FailedAttempts,
))
cimgui.Text(fmt.Sprintf("Policy: %s", failure.Policy))
if failure.Status.Err != nil {
cimgui.TextWrapped(failure.Status.Err.Error())
}
}
preview := "No entry selected"
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
+2 -2
View File
@@ -4,8 +4,8 @@ Size=400,400
Collapsed=0
[Window][Settings & Info]
Pos=580,0
Size=700,720
Pos=1220,0
Size=700,1080
Collapsed=0
[Window][Stats]
+42 -8
View File
@@ -17,6 +17,7 @@ const (
type PlaylistEvent struct {
Revision uint64
Kind PlaylistEventKind
Failure Status
}
// PlaylistReadiness is retained as an alias for callers that only publish
@@ -47,13 +48,24 @@ type PlaylistController struct {
mu sync.RWMutex
snapshot PlaylistSnapshot
hasSnapshot bool
failure PlaylistFailure
}
type PlaylistFailure struct {
EntryIndex int
EntryName string
Revision uint64
Policy PlaylistFailurePolicy
Status Status
}
type PlaylistSnapshot struct {
State PlaylistState
Entry PlaylistEntry
Revision uint64
Timing PlaylistTimingState
State PlaylistState
Entry PlaylistEntry
Revision uint64
Timing PlaylistTimingState
Failure PlaylistFailure
HasFailure bool
}
var (
@@ -155,6 +167,7 @@ func (c *PlaylistController) Run(
continue
}
if apply {
c.clearFailure()
stopTimer()
select {
case <-ctx.Done():
@@ -179,6 +192,13 @@ func (c *PlaylistController) Run(
continue
}
stopTimer()
c.setFailure(PlaylistFailure{
EntryIndex: state.CurrentIndex,
EntryName: c.playlist.Entries[state.CurrentIndex].Name,
Revision: revision,
Policy: c.playlist.OnFailure,
Status: ready.Failure,
})
// A failed entry must not retain a live or apparently active
// duration clock, even when the policy is to wait.
timing = NewPlaylistTiming(revision, timing.Duration)
@@ -285,15 +305,29 @@ func (c *PlaylistController) publish(
c.mu.Lock()
c.snapshot = PlaylistSnapshot{
State: state,
Entry: entry,
Revision: revision,
Timing: timing,
State: state,
Entry: entry,
Revision: revision,
Timing: timing,
Failure: c.failure,
HasFailure: c.failure.Revision != 0,
}
c.hasSnapshot = true
c.mu.Unlock()
}
func (c *PlaylistController) setFailure(failure PlaylistFailure) {
c.mu.Lock()
c.failure = failure
c.mu.Unlock()
}
func (c *PlaylistController) clearFailure() {
c.mu.Lock()
c.failure = PlaylistFailure{}
c.mu.Unlock()
}
func stopPlaylistTimer(timer playlistTimer) {
if timer == nil || timer.Stop() {
return
+31 -2
View File
@@ -2,6 +2,7 @@ package playback
import (
"context"
"errors"
"testing"
"time"
)
@@ -152,7 +153,15 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0}
<-sessions
events <- PlaylistEvent{Revision: 1, Kind: PlaylistEventFailed}
failureErr := errors.New("flow unavailable")
events <- PlaylistEvent{
Revision: 1,
Kind: PlaylistEventFailed,
Failure: Status{
Unit: UnitVideo, State: StateFailed,
Attempt: 2, FailedAttempts: 2, Err: failureErr,
},
}
if test.wantAdvance {
select {
@@ -160,16 +169,23 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
case <-time.After(time.Second):
t.Fatal("failure did not advance playlist")
}
snapshot, _ := controller.Snapshot()
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.State.CurrentIndex == 1 && snapshot.Revision == 2
})
if snapshot.State.CurrentIndex != 1 || snapshot.Revision != 2 {
t.Fatalf("snapshot = %#v, want index 1 revision 2", snapshot)
}
assertPlaylistFailure(t, snapshot, failureErr)
} else {
select {
case command := <-sessions:
t.Fatalf("unexpected session command: %#v", command)
case <-time.After(20 * time.Millisecond):
}
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.HasFailure
})
assertPlaylistFailure(t, snapshot, failureErr)
}
cancel()
@@ -179,3 +195,16 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
})
}
}
func assertPlaylistFailure(t *testing.T, snapshot PlaylistSnapshot, wantErr error) {
t.Helper()
if !snapshot.HasFailure {
t.Fatal("snapshot has no playlist failure")
}
failure := snapshot.Failure
if failure.EntryIndex != 0 || failure.EntryName != "first" ||
failure.Status.Unit != UnitVideo || failure.Status.Attempt != 2 ||
failure.Status.FailedAttempts != 2 || !errors.Is(failure.Status.Err, wantErr) {
t.Fatalf("failure = %#v", failure)
}
}
+23 -7
View File
@@ -112,13 +112,14 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
continue
}
statuses := c.statuses.SnapshotAll()
if IsSessionFailed(sessionSnapshot, statuses) {
if failure, failed := SessionFailureStatus(sessionSnapshot, statuses); failed {
if playlistSnapshot.Revision == emittedFailedRevision {
continue
}
failed := PlaylistEvent{
Revision: playlistSnapshot.Revision,
Kind: PlaylistEventFailed,
Failure: failure,
}
select {
case <-ctx.Done():
@@ -217,24 +218,39 @@ func IsSessionFailed(
session SessionSnapshot,
statuses PlaybackStatusSnapshot,
) bool {
_, failed := SessionFailureStatus(session, statuses)
return failed
}
func SessionFailureStatus(
session SessionSnapshot,
statuses PlaybackStatusSnapshot,
) (Status, bool) {
if statuses.Generation != session.Generation {
return false
return Status{}, false
}
switch session.Plan.Topology {
case TopologyIndependent:
return (session.Plan.Video.Active && statusIsFailed(
if session.Plan.Video.Active && statusIsFailed(
statuses.Video, statuses.HasVideo, session.Generation, session.Plan.Video,
)) || (session.Plan.Audio.Active && statusIsFailed(
) {
return statuses.Video, true
}
if session.Plan.Audio.Active && statusIsFailed(
statuses.Audio, statuses.HasAudio, session.Generation, session.Plan.Audio,
))
) {
return statuses.Audio, true
}
return Status{}, false
case TopologySynchronized:
return statuses.HasSync &&
failed := statuses.HasSync &&
statuses.Sync.Generation == session.Generation &&
statuses.Sync.State == StateFailed &&
sameSyncSource(statuses.Sync.Pair, session.Plan.Sync)
return statuses.Sync, failed
default:
return false
return Status{}, false
}
}