explicit info in GUI for playlist
This commit is contained in:
@@ -811,6 +811,33 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
cimgui.Text("End behavior: stop")
|
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"
|
preview := "No entry selected"
|
||||||
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
|
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ Size=400,400
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Settings & Info]
|
[Window][Settings & Info]
|
||||||
Pos=580,0
|
Pos=1220,0
|
||||||
Size=700,720
|
Size=700,1080
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][Stats]
|
[Window][Stats]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const (
|
|||||||
type PlaylistEvent struct {
|
type PlaylistEvent struct {
|
||||||
Revision uint64
|
Revision uint64
|
||||||
Kind PlaylistEventKind
|
Kind PlaylistEventKind
|
||||||
|
Failure Status
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaylistReadiness is retained as an alias for callers that only publish
|
// PlaylistReadiness is retained as an alias for callers that only publish
|
||||||
@@ -47,6 +48,15 @@ type PlaylistController struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
snapshot PlaylistSnapshot
|
snapshot PlaylistSnapshot
|
||||||
hasSnapshot bool
|
hasSnapshot bool
|
||||||
|
failure PlaylistFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaylistFailure struct {
|
||||||
|
EntryIndex int
|
||||||
|
EntryName string
|
||||||
|
Revision uint64
|
||||||
|
Policy PlaylistFailurePolicy
|
||||||
|
Status Status
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlaylistSnapshot struct {
|
type PlaylistSnapshot struct {
|
||||||
@@ -54,6 +64,8 @@ type PlaylistSnapshot struct {
|
|||||||
Entry PlaylistEntry
|
Entry PlaylistEntry
|
||||||
Revision uint64
|
Revision uint64
|
||||||
Timing PlaylistTimingState
|
Timing PlaylistTimingState
|
||||||
|
Failure PlaylistFailure
|
||||||
|
HasFailure bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -155,6 +167,7 @@ func (c *PlaylistController) Run(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if apply {
|
if apply {
|
||||||
|
c.clearFailure()
|
||||||
stopTimer()
|
stopTimer()
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -179,6 +192,13 @@ func (c *PlaylistController) Run(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stopTimer()
|
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
|
// A failed entry must not retain a live or apparently active
|
||||||
// duration clock, even when the policy is to wait.
|
// duration clock, even when the policy is to wait.
|
||||||
timing = NewPlaylistTiming(revision, timing.Duration)
|
timing = NewPlaylistTiming(revision, timing.Duration)
|
||||||
@@ -289,11 +309,25 @@ func (c *PlaylistController) publish(
|
|||||||
Entry: entry,
|
Entry: entry,
|
||||||
Revision: revision,
|
Revision: revision,
|
||||||
Timing: timing,
|
Timing: timing,
|
||||||
|
Failure: c.failure,
|
||||||
|
HasFailure: c.failure.Revision != 0,
|
||||||
}
|
}
|
||||||
c.hasSnapshot = true
|
c.hasSnapshot = true
|
||||||
c.mu.Unlock()
|
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) {
|
func stopPlaylistTimer(timer playlistTimer) {
|
||||||
if timer == nil || timer.Stop() {
|
if timer == nil || timer.Stop() {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package playback
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -152,7 +153,15 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
|
|||||||
|
|
||||||
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0}
|
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 0}
|
||||||
<-sessions
|
<-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 {
|
if test.wantAdvance {
|
||||||
select {
|
select {
|
||||||
@@ -160,16 +169,23 @@ func TestPlaylistControllerFailurePolicy(t *testing.T) {
|
|||||||
case <-time.After(time.Second):
|
case <-time.After(time.Second):
|
||||||
t.Fatal("failure did not advance playlist")
|
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 {
|
if snapshot.State.CurrentIndex != 1 || snapshot.Revision != 2 {
|
||||||
t.Fatalf("snapshot = %#v, want index 1 revision 2", snapshot)
|
t.Fatalf("snapshot = %#v, want index 1 revision 2", snapshot)
|
||||||
}
|
}
|
||||||
|
assertPlaylistFailure(t, snapshot, failureErr)
|
||||||
} else {
|
} else {
|
||||||
select {
|
select {
|
||||||
case command := <-sessions:
|
case command := <-sessions:
|
||||||
t.Fatalf("unexpected session command: %#v", command)
|
t.Fatalf("unexpected session command: %#v", command)
|
||||||
case <-time.After(20 * time.Millisecond):
|
case <-time.After(20 * time.Millisecond):
|
||||||
}
|
}
|
||||||
|
snapshot := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.HasFailure
|
||||||
|
})
|
||||||
|
assertPlaylistFailure(t, snapshot, failureErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,13 +112,14 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
statuses := c.statuses.SnapshotAll()
|
statuses := c.statuses.SnapshotAll()
|
||||||
if IsSessionFailed(sessionSnapshot, statuses) {
|
if failure, failed := SessionFailureStatus(sessionSnapshot, statuses); failed {
|
||||||
if playlistSnapshot.Revision == emittedFailedRevision {
|
if playlistSnapshot.Revision == emittedFailedRevision {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
failed := PlaylistEvent{
|
failed := PlaylistEvent{
|
||||||
Revision: playlistSnapshot.Revision,
|
Revision: playlistSnapshot.Revision,
|
||||||
Kind: PlaylistEventFailed,
|
Kind: PlaylistEventFailed,
|
||||||
|
Failure: failure,
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -217,24 +218,39 @@ func IsSessionFailed(
|
|||||||
session SessionSnapshot,
|
session SessionSnapshot,
|
||||||
statuses PlaybackStatusSnapshot,
|
statuses PlaybackStatusSnapshot,
|
||||||
) bool {
|
) bool {
|
||||||
|
_, failed := SessionFailureStatus(session, statuses)
|
||||||
|
return failed
|
||||||
|
}
|
||||||
|
|
||||||
|
func SessionFailureStatus(
|
||||||
|
session SessionSnapshot,
|
||||||
|
statuses PlaybackStatusSnapshot,
|
||||||
|
) (Status, bool) {
|
||||||
if statuses.Generation != session.Generation {
|
if statuses.Generation != session.Generation {
|
||||||
return false
|
return Status{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch session.Plan.Topology {
|
switch session.Plan.Topology {
|
||||||
case TopologyIndependent:
|
case TopologyIndependent:
|
||||||
return (session.Plan.Video.Active && statusIsFailed(
|
if session.Plan.Video.Active && statusIsFailed(
|
||||||
statuses.Video, statuses.HasVideo, session.Generation, session.Plan.Video,
|
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,
|
statuses.Audio, statuses.HasAudio, session.Generation, session.Plan.Audio,
|
||||||
))
|
) {
|
||||||
|
return statuses.Audio, true
|
||||||
|
}
|
||||||
|
return Status{}, false
|
||||||
case TopologySynchronized:
|
case TopologySynchronized:
|
||||||
return statuses.HasSync &&
|
failed := statuses.HasSync &&
|
||||||
statuses.Sync.Generation == session.Generation &&
|
statuses.Sync.Generation == session.Generation &&
|
||||||
statuses.Sync.State == StateFailed &&
|
statuses.Sync.State == StateFailed &&
|
||||||
sameSyncSource(statuses.Sync.Pair, session.Plan.Sync)
|
sameSyncSource(statuses.Sync.Pair, session.Plan.Sync)
|
||||||
|
return statuses.Sync, failed
|
||||||
default:
|
default:
|
||||||
return false
|
return Status{}, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user