playlist timer pause/resume without affecting active feeds
This commit is contained in:
@@ -753,6 +753,21 @@ func main() {
|
|||||||
if cimgui.Button("Next##playlist") && !playlistRuntime.Next() {
|
if cimgui.Button("Next##playlist") && !playlistRuntime.Next() {
|
||||||
log.Print("playlist command queue is full")
|
log.Print("playlist command queue is full")
|
||||||
}
|
}
|
||||||
|
if hasPlaylistSnapshot &&
|
||||||
|
playlistSnapshot.State.HasSelection &&
|
||||||
|
playlistSnapshot.Entry.Duration > 0 &&
|
||||||
|
!playlistSnapshot.Timing.Expired {
|
||||||
|
cimgui.SameLine()
|
||||||
|
if playlistSnapshot.Timing.Paused {
|
||||||
|
if cimgui.Button("Resume timer##playlist") &&
|
||||||
|
!playlistRuntime.Resume() {
|
||||||
|
log.Print("playlist command queue is full")
|
||||||
|
}
|
||||||
|
} else if cimgui.Button("Pause timer##playlist") &&
|
||||||
|
!playlistRuntime.Pause() {
|
||||||
|
log.Print("playlist command queue is full")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
|
if hasPlaylistSnapshot && playlistSnapshot.State.HasSelection {
|
||||||
entry := playlistSnapshot.Entry
|
entry := playlistSnapshot.Entry
|
||||||
@@ -786,6 +801,17 @@ func main() {
|
|||||||
switch {
|
switch {
|
||||||
case entry.Duration == 0:
|
case entry.Duration == 0:
|
||||||
cimgui.Text("Timing: manual advance")
|
cimgui.Text("Timing: manual advance")
|
||||||
|
case playlistSnapshot.Timing.Paused:
|
||||||
|
fraction, remaining := playlistTimingProgress(
|
||||||
|
playlistSnapshot.Timing,
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
cimgui.Text("Timing: paused")
|
||||||
|
cimgui.ProgressBarV(
|
||||||
|
fraction,
|
||||||
|
cimgui.Vec2{X: -1, Y: 0},
|
||||||
|
remaining.Round(time.Second).String(),
|
||||||
|
)
|
||||||
case playlistSnapshot.Timing.Started:
|
case playlistSnapshot.Timing.Started:
|
||||||
fraction, remaining := playlistTimingProgress(
|
fraction, remaining := playlistTimingProgress(
|
||||||
playlistSnapshot.Timing,
|
playlistSnapshot.Timing,
|
||||||
|
|||||||
@@ -111,6 +111,14 @@ func (p *playerPlaylist) Previous() bool {
|
|||||||
return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistPrevious})
|
return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistPrevious})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *playerPlaylist) Pause() bool {
|
||||||
|
return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistPause})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *playerPlaylist) Resume() bool {
|
||||||
|
return p.enqueue(playback.PlaylistCommand{Kind: playback.PlaylistResume})
|
||||||
|
}
|
||||||
|
|
||||||
func (p *playerPlaylist) enqueue(command playback.PlaylistCommand) bool {
|
func (p *playerPlaylist) enqueue(command playback.PlaylistCommand) bool {
|
||||||
select {
|
select {
|
||||||
case p.Commands <- command:
|
case p.Commands <- command:
|
||||||
@@ -154,11 +162,14 @@ func playlistTimingProgress(
|
|||||||
if timing.Expired {
|
if timing.Expired {
|
||||||
return 1, 0
|
return 1, 0
|
||||||
}
|
}
|
||||||
if !timing.Started {
|
remaining := timing.Remaining
|
||||||
|
if timing.Paused {
|
||||||
|
// Retain the remaining time captured when the owned timer stopped.
|
||||||
|
} else if !timing.Started {
|
||||||
return 0, timing.Duration
|
return 0, timing.Duration
|
||||||
|
} else {
|
||||||
|
remaining = timing.Deadline.Sub(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := timing.Deadline.Sub(now)
|
|
||||||
if remaining < 0 {
|
if remaining < 0 {
|
||||||
remaining = 0
|
remaining = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -127,6 +128,8 @@ func TestPlayerPlaylistNavigationHelpers(t *testing.T) {
|
|||||||
{send: func() bool { return runtime.Select(4) }, want: playback.PlaylistCommand{Kind: playback.PlaylistSelect, Index: 4}},
|
{send: func() bool { return runtime.Select(4) }, want: playback.PlaylistCommand{Kind: playback.PlaylistSelect, Index: 4}},
|
||||||
{send: runtime.Next, want: playback.PlaylistCommand{Kind: playback.PlaylistNext}},
|
{send: runtime.Next, want: playback.PlaylistCommand{Kind: playback.PlaylistNext}},
|
||||||
{send: runtime.Previous, want: playback.PlaylistCommand{Kind: playback.PlaylistPrevious}},
|
{send: runtime.Previous, want: playback.PlaylistCommand{Kind: playback.PlaylistPrevious}},
|
||||||
|
{send: runtime.Pause, want: playback.PlaylistCommand{Kind: playback.PlaylistPause}},
|
||||||
|
{send: runtime.Resume, want: playback.PlaylistCommand{Kind: playback.PlaylistResume}},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
if !test.send() {
|
if !test.send() {
|
||||||
@@ -344,6 +347,16 @@ func TestPlaylistTimingProgress(t *testing.T) {
|
|||||||
timing: playback.PlaylistTimingState{Duration: 10 * time.Second, Expired: true},
|
timing: playback.PlaylistTimingState{Duration: 10 * time.Second, Expired: true},
|
||||||
wantFraction: 1,
|
wantFraction: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "paused",
|
||||||
|
timing: playback.PlaylistTimingState{
|
||||||
|
Duration: 10 * time.Second,
|
||||||
|
Paused: true,
|
||||||
|
Remaining: 6 * time.Second,
|
||||||
|
},
|
||||||
|
wantFraction: 0.4,
|
||||||
|
wantRemaining: 6 * time.Second,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "deadline passed",
|
name: "deadline passed",
|
||||||
timing: playback.PlaylistTimingState{
|
timing: playback.PlaylistTimingState{
|
||||||
@@ -358,7 +371,8 @@ func TestPlaylistTimingProgress(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
fraction, remaining := playlistTimingProgress(test.timing, now)
|
fraction, remaining := playlistTimingProgress(test.timing, now)
|
||||||
if fraction != test.wantFraction || remaining != test.wantRemaining {
|
if math.Abs(float64(fraction-test.wantFraction)) > 0.000001 ||
|
||||||
|
remaining != test.wantRemaining {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"playlistTimingProgress() = %v, %v; want %v, %v",
|
"playlistTimingProgress() = %v, %v; want %v, %v",
|
||||||
fraction,
|
fraction,
|
||||||
|
|||||||
@@ -102,6 +102,36 @@ func (c *PlaylistController) Run(
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if command.Kind == PlaylistPause {
|
||||||
|
nextTiming, changed := PausePlaylistTiming(
|
||||||
|
timing,
|
||||||
|
revision,
|
||||||
|
c.now(),
|
||||||
|
)
|
||||||
|
if changed {
|
||||||
|
stopTimer()
|
||||||
|
timing = nextTiming
|
||||||
|
c.publish(state, revision, timing)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if command.Kind == PlaylistResume {
|
||||||
|
nextTiming, changed := ResumePlaylistTiming(
|
||||||
|
timing,
|
||||||
|
revision,
|
||||||
|
c.now(),
|
||||||
|
)
|
||||||
|
if changed {
|
||||||
|
timing = nextTiming
|
||||||
|
if timing.Started {
|
||||||
|
timerRevision = timing.Revision
|
||||||
|
timer = c.newTimer(timing.Remaining)
|
||||||
|
timerC = timer.C()
|
||||||
|
}
|
||||||
|
c.publish(state, revision, timing)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
next, sessionCommand, apply, err := ApplyPlaylistSelection(
|
next, sessionCommand, apply, err := ApplyPlaylistSelection(
|
||||||
c.playlist,
|
c.playlist,
|
||||||
@@ -132,6 +162,14 @@ func (c *PlaylistController) Run(
|
|||||||
readiness = nil
|
readiness = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if timing.Paused &&
|
||||||
|
ready.Revision == timing.Revision &&
|
||||||
|
timing.Duration > 0 &&
|
||||||
|
!timing.Expired {
|
||||||
|
timing.Ready = true
|
||||||
|
c.publish(state, revision, timing)
|
||||||
|
continue
|
||||||
|
}
|
||||||
nextTiming, started := StartPlaylistTiming(
|
nextTiming, started := StartPlaylistTiming(
|
||||||
timing,
|
timing,
|
||||||
ready.Revision,
|
ready.Revision,
|
||||||
|
|||||||
@@ -255,6 +255,113 @@ func TestPlaylistControllerCancellationStopsTimer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlaylistControllerPauseAndResumeTimer(t *testing.T) {
|
||||||
|
controller, commands, readiness, sessions, timers, now, cancel, result :=
|
||||||
|
startTimedPlaylistController(t, timedPlaylist(false))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||||
|
_ = receivePlaylistSession(t, sessions)
|
||||||
|
readiness <- PlaylistReadiness{Revision: 1}
|
||||||
|
oldTimer := receiveFakePlaylistTimer(t, timers)
|
||||||
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Timing.Started
|
||||||
|
})
|
||||||
|
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistPause}
|
||||||
|
paused := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Timing.Paused
|
||||||
|
})
|
||||||
|
if paused.Revision != 1 || paused.Timing.Started {
|
||||||
|
t.Fatalf("paused snapshot = %#v", paused)
|
||||||
|
}
|
||||||
|
if !oldTimer.isStopped() {
|
||||||
|
t.Fatal("Pause did not stop active timer")
|
||||||
|
}
|
||||||
|
oldTimer.fire(now.Add(10 * time.Second))
|
||||||
|
select {
|
||||||
|
case command := <-sessions:
|
||||||
|
t.Fatalf("paused stale timer sent session command: %#v", command)
|
||||||
|
case <-time.After(20 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistResume}
|
||||||
|
_ = receiveFakePlaylistTimer(t, timers)
|
||||||
|
resumed := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Timing.Started && !snapshot.Timing.Paused
|
||||||
|
})
|
||||||
|
if resumed.Revision != 1 {
|
||||||
|
t.Fatalf("resume changed revision: %#v", resumed)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case command := <-sessions:
|
||||||
|
t.Fatalf("pause/resume sent session command: %#v", command)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
close(commands)
|
||||||
|
if err := waitForPlaylistResult(t, result); err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaylistControllerManualSelectionClearsPause(t *testing.T) {
|
||||||
|
controller, commands, _, sessions, _, _, cancel, result :=
|
||||||
|
startTimedPlaylistController(t, timedPlaylist(false))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||||
|
_ = receivePlaylistSession(t, sessions)
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistPause}
|
||||||
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Timing.Paused
|
||||||
|
})
|
||||||
|
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||||
|
_ = receivePlaylistSession(t, sessions)
|
||||||
|
next := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Revision == 2
|
||||||
|
})
|
||||||
|
if next.Timing.Paused || next.State.CurrentIndex != 1 {
|
||||||
|
t.Fatalf("new selection retained pause: %#v", next)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(commands)
|
||||||
|
if err := waitForPlaylistResult(t, result); err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaylistControllerRecordsQueuedReadinessWhilePaused(t *testing.T) {
|
||||||
|
controller, commands, readiness, sessions, timers, _, cancel, result :=
|
||||||
|
startTimedPlaylistController(t, timedPlaylist(false))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistNext}
|
||||||
|
_ = receivePlaylistSession(t, sessions)
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistPause}
|
||||||
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Timing.Paused
|
||||||
|
})
|
||||||
|
|
||||||
|
readiness <- PlaylistReadiness{Revision: 1}
|
||||||
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Timing.Paused && snapshot.Timing.Ready
|
||||||
|
})
|
||||||
|
assertNoPlaylistTimer(t, timers)
|
||||||
|
|
||||||
|
commands <- PlaylistCommand{Kind: PlaylistResume}
|
||||||
|
_ = receiveFakePlaylistTimer(t, timers)
|
||||||
|
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
|
||||||
|
return snapshot.Timing.Started && !snapshot.Timing.Paused
|
||||||
|
})
|
||||||
|
|
||||||
|
close(commands)
|
||||||
|
if err := waitForPlaylistResult(t, result); err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startTimedPlaylistController(
|
func startTimedPlaylistController(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
playlist Playlist,
|
playlist Playlist,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const (
|
|||||||
PlaylistSelect PlaylistCommandKind = iota + 1
|
PlaylistSelect PlaylistCommandKind = iota + 1
|
||||||
PlaylistNext
|
PlaylistNext
|
||||||
PlaylistPrevious
|
PlaylistPrevious
|
||||||
|
PlaylistPause
|
||||||
|
PlaylistResume
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlaylistCommand struct {
|
type PlaylistCommand struct {
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ func (c *PlaylistReadinessCoordinator) Run(ctx context.Context) error {
|
|||||||
playlistSnapshot.Revision == 0 ||
|
playlistSnapshot.Revision == 0 ||
|
||||||
playlistSnapshot.Entry.Duration <= 0 ||
|
playlistSnapshot.Entry.Duration <= 0 ||
|
||||||
playlistSnapshot.Timing.Started ||
|
playlistSnapshot.Timing.Started ||
|
||||||
|
playlistSnapshot.Timing.Paused ||
|
||||||
playlistSnapshot.Revision == emittedRevision {
|
playlistSnapshot.Revision == emittedRevision {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ func TestPlaylistReadinessCoordinatorWaitsForAllConditions(t *testing.T) {
|
|||||||
{name: "zero revision", mutate: func() { value := playlistSnapshotForVideo(1); value.Revision = 0; playlist.set(value, true) }},
|
{name: "zero revision", mutate: func() { value := playlistSnapshotForVideo(1); value.Revision = 0; playlist.set(value, true) }},
|
||||||
{name: "zero duration", mutate: func() { value := playlistSnapshotForVideo(1); value.Entry.Duration = 0; playlist.set(value, true) }},
|
{name: "zero duration", mutate: func() { value := playlistSnapshotForVideo(1); value.Entry.Duration = 0; playlist.set(value, true) }},
|
||||||
{name: "already started", mutate: func() { value := playlistSnapshotForVideo(1); value.Timing.Started = true; playlist.set(value, true) }},
|
{name: "already started", mutate: func() { value := playlistSnapshotForVideo(1); value.Timing.Started = true; playlist.set(value, true) }},
|
||||||
|
{name: "paused", mutate: func() { value := playlistSnapshotForVideo(1); value.Timing.Paused = true; playlist.set(value, true) }},
|
||||||
{name: "session mismatch", mutate: func() {
|
{name: "session mismatch", mutate: func() {
|
||||||
playlist.set(playlistSnapshotForVideo(1), true)
|
playlist.set(playlistSnapshotForVideo(1), true)
|
||||||
value, _ := session.Snapshot()
|
value, _ := session.Snapshot()
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package playback
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type PlaylistTimingState struct {
|
type PlaylistTimingState struct {
|
||||||
Revision uint64
|
Revision uint64
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
Started bool
|
Ready bool
|
||||||
Expired bool
|
Started bool
|
||||||
Deadline time.Time
|
Paused bool
|
||||||
|
Expired bool
|
||||||
|
Remaining time.Duration
|
||||||
|
Deadline time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlaylistTiming(
|
func NewPlaylistTiming(
|
||||||
@@ -15,9 +18,9 @@ func NewPlaylistTiming(
|
|||||||
duration time.Duration,
|
duration time.Duration,
|
||||||
) PlaylistTimingState {
|
) PlaylistTimingState {
|
||||||
return PlaylistTimingState{
|
return PlaylistTimingState{
|
||||||
Revision: revision,
|
Revision: revision,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
Expired: false,
|
Remaining: duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +32,66 @@ func StartPlaylistTiming(
|
|||||||
if revision != current.Revision ||
|
if revision != current.Revision ||
|
||||||
current.Duration <= 0 ||
|
current.Duration <= 0 ||
|
||||||
current.Started ||
|
current.Started ||
|
||||||
|
current.Paused ||
|
||||||
current.Expired {
|
current.Expired {
|
||||||
return current, false
|
return current, false
|
||||||
}
|
}
|
||||||
next := current
|
next := current
|
||||||
|
if next.Remaining <= 0 {
|
||||||
|
next.Remaining = next.Duration
|
||||||
|
}
|
||||||
|
next.Ready = true
|
||||||
next.Started = true
|
next.Started = true
|
||||||
next.Deadline = now.Add(current.Duration)
|
next.Deadline = now.Add(next.Remaining)
|
||||||
|
return next, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func PausePlaylistTiming(
|
||||||
|
current PlaylistTimingState,
|
||||||
|
revision uint64,
|
||||||
|
now time.Time,
|
||||||
|
) (PlaylistTimingState, bool) {
|
||||||
|
if revision != current.Revision ||
|
||||||
|
current.Duration <= 0 ||
|
||||||
|
current.Paused ||
|
||||||
|
current.Expired {
|
||||||
|
return current, false
|
||||||
|
}
|
||||||
|
|
||||||
|
next := current
|
||||||
|
if next.Started {
|
||||||
|
next.Remaining = next.Deadline.Sub(now)
|
||||||
|
if next.Remaining < 0 {
|
||||||
|
next.Remaining = 0
|
||||||
|
}
|
||||||
|
if next.Remaining > next.Duration {
|
||||||
|
next.Remaining = next.Duration
|
||||||
|
}
|
||||||
|
next.Started = false
|
||||||
|
next.Deadline = time.Time{}
|
||||||
|
}
|
||||||
|
next.Paused = true
|
||||||
|
return next, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResumePlaylistTiming(
|
||||||
|
current PlaylistTimingState,
|
||||||
|
revision uint64,
|
||||||
|
now time.Time,
|
||||||
|
) (PlaylistTimingState, bool) {
|
||||||
|
if revision != current.Revision ||
|
||||||
|
current.Duration <= 0 ||
|
||||||
|
!current.Paused ||
|
||||||
|
current.Expired {
|
||||||
|
return current, false
|
||||||
|
}
|
||||||
|
|
||||||
|
next := current
|
||||||
|
next.Paused = false
|
||||||
|
if next.Ready {
|
||||||
|
next.Started = true
|
||||||
|
next.Deadline = now.Add(next.Remaining)
|
||||||
|
}
|
||||||
return next, true
|
return next, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +108,9 @@ func ExpirePlaylistTiming(
|
|||||||
|
|
||||||
next := current
|
next := current
|
||||||
next.Started = false
|
next.Started = false
|
||||||
|
next.Paused = false
|
||||||
next.Deadline = time.Time{}
|
next.Deadline = time.Time{}
|
||||||
next.Expired = true
|
next.Expired = true
|
||||||
|
next.Remaining = 0
|
||||||
return next, true
|
return next, true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import (
|
|||||||
|
|
||||||
func TestNewPlaylistTimingResetsState(t *testing.T) {
|
func TestNewPlaylistTimingResetsState(t *testing.T) {
|
||||||
got := NewPlaylistTiming(7, 10*time.Second)
|
got := NewPlaylistTiming(7, 10*time.Second)
|
||||||
want := PlaylistTimingState{Revision: 7, Duration: 10 * time.Second}
|
want := PlaylistTimingState{
|
||||||
|
Revision: 7,
|
||||||
|
Duration: 10 * time.Second,
|
||||||
|
Remaining: 10 * time.Second,
|
||||||
|
}
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Fatalf("NewPlaylistTiming() = %#v, want %#v", got, want)
|
t.Fatalf("NewPlaylistTiming() = %#v, want %#v", got, want)
|
||||||
}
|
}
|
||||||
@@ -22,10 +26,12 @@ func TestStartPlaylistTiming(t *testing.T) {
|
|||||||
t.Fatal("StartPlaylistTiming() started = false, want true")
|
t.Fatal("StartPlaylistTiming() started = false, want true")
|
||||||
}
|
}
|
||||||
want := PlaylistTimingState{
|
want := PlaylistTimingState{
|
||||||
Revision: 4,
|
Revision: 4,
|
||||||
Duration: 10 * time.Second,
|
Duration: 10 * time.Second,
|
||||||
Started: true,
|
Ready: true,
|
||||||
Deadline: now.Add(10 * time.Second),
|
Started: true,
|
||||||
|
Remaining: 10 * time.Second,
|
||||||
|
Deadline: now.Add(10 * time.Second),
|
||||||
}
|
}
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Fatalf("StartPlaylistTiming() = %#v, want %#v", got, want)
|
t.Fatalf("StartPlaylistTiming() = %#v, want %#v", got, want)
|
||||||
@@ -47,6 +53,7 @@ func TestStartPlaylistTimingIgnoresInapplicableReadiness(t *testing.T) {
|
|||||||
{name: "stale revision", current: NewPlaylistTiming(4, time.Second), revision: 3},
|
{name: "stale revision", current: NewPlaylistTiming(4, time.Second), revision: 3},
|
||||||
{name: "future revision", current: NewPlaylistTiming(4, time.Second), revision: 5},
|
{name: "future revision", current: NewPlaylistTiming(4, time.Second), revision: 5},
|
||||||
{name: "already started", current: started, revision: 4},
|
{name: "already started", current: started, revision: 4},
|
||||||
|
{name: "paused", current: PlaylistTimingState{Revision: 4, Duration: time.Second, Paused: true, Remaining: time.Second}, revision: 4},
|
||||||
{name: "expired", current: PlaylistTimingState{Revision: 4, Duration: time.Second, Expired: true}, revision: 4},
|
{name: "expired", current: PlaylistTimingState{Revision: 4, Duration: time.Second, Expired: true}, revision: 4},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +78,13 @@ func TestExpirePlaylistTiming(t *testing.T) {
|
|||||||
if !expired {
|
if !expired {
|
||||||
t.Fatal("ExpirePlaylistTiming() expired = false, want true")
|
t.Fatal("ExpirePlaylistTiming() expired = false, want true")
|
||||||
}
|
}
|
||||||
want := PlaylistTimingState{Revision: 9, Duration: 5 * time.Second, Expired: true}
|
want := PlaylistTimingState{
|
||||||
|
Revision: 9,
|
||||||
|
Duration: 5 * time.Second,
|
||||||
|
Ready: true,
|
||||||
|
Expired: true,
|
||||||
|
Remaining: 0,
|
||||||
|
}
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Fatalf("ExpirePlaylistTiming() = %#v, want %#v", got, want)
|
t.Fatalf("ExpirePlaylistTiming() = %#v, want %#v", got, want)
|
||||||
}
|
}
|
||||||
@@ -118,3 +131,74 @@ func TestNewPlaylistTimingInvalidatesPreviousDeadline(t *testing.T) {
|
|||||||
t.Fatalf("old expiry changed new timing: %#v, %v", got, expired)
|
t.Fatalf("old expiry changed new timing: %#v, %v", got, expired)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPauseAndResumePlaylistTimingBeforeReadiness(t *testing.T) {
|
||||||
|
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
|
||||||
|
current := NewPlaylistTiming(3, 10*time.Second)
|
||||||
|
|
||||||
|
paused, changed := PausePlaylistTiming(current, 3, now)
|
||||||
|
if !changed || !paused.Paused || paused.Ready || paused.Started {
|
||||||
|
t.Fatalf("PausePlaylistTiming() = %#v, %v", paused, changed)
|
||||||
|
}
|
||||||
|
resumed, changed := ResumePlaylistTiming(paused, 3, now.Add(time.Second))
|
||||||
|
if !changed || resumed.Paused || resumed.Ready || resumed.Started {
|
||||||
|
t.Fatalf("ResumePlaylistTiming() = %#v, %v", resumed, changed)
|
||||||
|
}
|
||||||
|
if resumed.Remaining != 10*time.Second {
|
||||||
|
t.Fatalf("remaining = %v, want 10s", resumed.Remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPauseAndResumeActivePlaylistTimingUsesRemaining(t *testing.T) {
|
||||||
|
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
|
||||||
|
current, _ := StartPlaylistTiming(NewPlaylistTiming(5, 10*time.Second), 5, now)
|
||||||
|
|
||||||
|
paused, changed := PausePlaylistTiming(current, 5, now.Add(4*time.Second))
|
||||||
|
if !changed || !paused.Paused || paused.Started || !paused.Ready {
|
||||||
|
t.Fatalf("PausePlaylistTiming() = %#v, %v", paused, changed)
|
||||||
|
}
|
||||||
|
if paused.Remaining != 6*time.Second || !paused.Deadline.IsZero() {
|
||||||
|
t.Fatalf("paused timing = %#v, want 6s remaining and no deadline", paused)
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeAt := now.Add(20 * time.Second)
|
||||||
|
resumed, changed := ResumePlaylistTiming(paused, 5, resumeAt)
|
||||||
|
if !changed || resumed.Paused || !resumed.Started || !resumed.Ready {
|
||||||
|
t.Fatalf("ResumePlaylistTiming() = %#v, %v", resumed, changed)
|
||||||
|
}
|
||||||
|
if resumed.Deadline != resumeAt.Add(6*time.Second) {
|
||||||
|
t.Fatalf("resumed deadline = %v, want %v", resumed.Deadline, resumeAt.Add(6*time.Second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPauseAndResumePlaylistTimingIgnoreInvalidTransitions(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
base := NewPlaylistTiming(2, time.Second)
|
||||||
|
paused, _ := PausePlaylistTiming(base, 2, now)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
state PlaylistTimingState
|
||||||
|
pause bool
|
||||||
|
revision uint64
|
||||||
|
}{
|
||||||
|
{name: "pause wrong revision", state: base, pause: true, revision: 1},
|
||||||
|
{name: "duplicate pause", state: paused, pause: true, revision: 2},
|
||||||
|
{name: "resume wrong revision", state: paused, revision: 1},
|
||||||
|
{name: "duplicate resume", state: base, revision: 2},
|
||||||
|
{name: "pause expired", state: PlaylistTimingState{Revision: 2, Duration: time.Second, Expired: true}, pause: true, revision: 2},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var got PlaylistTimingState
|
||||||
|
var changed bool
|
||||||
|
if test.pause {
|
||||||
|
got, changed = PausePlaylistTiming(test.state, test.revision, now)
|
||||||
|
} else {
|
||||||
|
got, changed = ResumePlaylistTiming(test.state, test.revision, now)
|
||||||
|
}
|
||||||
|
if changed || got != test.state {
|
||||||
|
t.Fatalf("transition = %#v, %v; want unchanged", got, changed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+9
-9
@@ -2,31 +2,31 @@
|
|||||||
"loop": true,
|
"loop": true,
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"name": "Synchronized feed",
|
"name": "timelapse",
|
||||||
"video": {
|
"video": {
|
||||||
"domain": "/dev/shm/mxl",
|
"domain": "/dev/shm/mxl",
|
||||||
"uuid": "video-uuid"
|
"uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ed"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"domain": "/dev/shm/mxl",
|
"domain": "/dev/shm/mxl",
|
||||||
"uuid": "audio-uuid"
|
"uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ec"
|
||||||
},
|
},
|
||||||
"sync": true,
|
"sync": true,
|
||||||
"duration": "10s"
|
"duration": "10s"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Video only",
|
"name": "F1",
|
||||||
"video": {
|
"video": {
|
||||||
"domain": "/another/domain",
|
"domain": "/dev/shm/mxl",
|
||||||
"uuid": "video-uuid"
|
"uuid": "5fbec3b1-1b0f-417d-9059-8b94a47197ef"
|
||||||
},
|
},
|
||||||
"duration": "15s"
|
"duration": "15s"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Manual audio",
|
"name": "Costa Rica",
|
||||||
"audio": {
|
"audio": {
|
||||||
"domain": "/dev/shm/audio",
|
"domain": "/dev/shm/mxl",
|
||||||
"uuid": "audio-uuid"
|
"uuid": "9d2a041b-01cf-4ee4-bffa-188fe093c99b"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user