/
player.go
227 lines (200 loc) · 5.21 KB
/
player.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
package protocol
import (
"errors"
"sync"
"time"
)
// ErrTimeout is the error returned when a requested operation could not be
// performed in time.
var ErrTimeout = errors.New("operation timed out")
type command int
const (
cmdStop = iota // stop playback
cmdPause // pause playback
cmdResume // resume playback from paused position
cmdSkip // skip/jump to position
commandTimeout = time.Second
)
// control struct is the internal control structure to control the playback
// loop routine.
type control struct {
Command command
Position time.Duration
}
// TimedActionsPlayer can playback an array of TimeActions. It can be used by
// protocols that can pre-calculate TimeActions.
//
// All of the SkippableScriptPlayer methods are implemented except for
// ScriptLoader. Protocols only need to implement ScriptLoader themselves and
// set the Script field with their result.
type TimedActionsPlayer struct {
// Script that the player will use.
Script []TimedAction
wg sync.WaitGroup
ctrl chan control
latency time.Duration
posLimitFunc func(int) int
speedLimitFunc func(int) int
}
// NewTimedActionsPlayer returns a new TimedActionsPlayer.
func NewTimedActionsPlayer() *TimedActionsPlayer {
return &TimedActionsPlayer{
ctrl: make(chan control),
posLimitFunc: func(p int) int { return p },
speedLimitFunc: func(s int) int { return s },
}
}
// Play will start executing the loaded script from the start.
func (ta *TimedActionsPlayer) Play() <-chan Action {
// Only play one script at a time
ta.wg.Wait()
ta.wg.Add(1)
out := make(chan Action)
go ta.playbackLoop(out, ta.ctrl)
return out
}
// Latency implements the LatencyCalibrator interface to calibrate the latency.
func (ta *TimedActionsPlayer) Latency(t time.Duration) {
ta.latency = t
}
// LimitPosition implements the PositionLimiter interface.
// low is the lowest position in percent to move to.
// high is the highst position in percent to move to.
func (ta *TimedActionsPlayer) LimitPosition(low, high int) {
if low >= high || high <= low {
// Ignore invalid config
return
}
ta.posLimitFunc = func(p int) int {
if p < low {
return low
}
if p > high {
return high
}
return p
}
}
// LimitSpeed implements the SpeedLimiter interface.
// slow is the slowest speed in percent to move with.
// fast is the highst speed in percent to move with.
func (ta *TimedActionsPlayer) LimitSpeed(slow, fast int) {
if slow >= fast || fast <= slow {
// Ignore invalid config
return
}
ta.speedLimitFunc = func(s int) int {
if s < slow {
return slow
}
if s > fast {
return fast
}
return s
}
}
// sendCommand to the playbackLoop with a timeout.
func (ta *TimedActionsPlayer) sendCommand(c control) error {
select {
case ta.ctrl <- c:
return nil
case <-time.After(commandTimeout):
return ErrTimeout
}
}
// Stop stops playback and resets player.
func (ta *TimedActionsPlayer) Stop() error {
return ta.sendCommand(control{
Command: cmdStop,
})
}
// Pause will halt playback at the current position.
func (ta *TimedActionsPlayer) Pause() error {
return ta.sendCommand(control{
Command: cmdPause,
})
}
// Resume will continue playback from the paused location.
func (ta *TimedActionsPlayer) Resume() error {
return ta.sendCommand(control{
Command: cmdResume,
})
}
// Skip will jump to a specific position.
func (ta *TimedActionsPlayer) Skip(p time.Duration) error {
return ta.sendCommand(control{
Command: cmdSkip,
Position: p,
})
}
// Dump will return the loaded script as TimedActions.
func (ta *TimedActionsPlayer) Dump() (TimedActions, error) {
var s = make(TimedActions, len(ta.Script))
copy(s, ta.Script)
return s, nil
}
// playbackLoop will play the loaded script to out and can be controlled using
// ctrl.
func (ta *TimedActionsPlayer) playbackLoop(out chan<- Action, ctrl <-chan control) {
defer func() {
ta.wg.Done()
close(out)
}()
var (
cursor int // event position in script
startTime = time.Now() // time playback started/resumed
startPosition time.Duration // timecode where playback started
paused bool
)
for cursor < len(ta.Script) {
a := ta.Script[cursor]
if a.Time < startPosition {
cursor++
continue
}
var nextEventTime <-chan time.Time
if !paused {
nextEventTime = time.After(
a.Time - calcPosition(startTime, startPosition) + ta.latency)
}
select {
case cmd := <-ctrl:
switch cmd.Command {
case cmdStop:
return
case cmdPause:
if !paused {
paused = true
startPosition = calcPosition(
startTime,
startPosition,
) + ta.latency
}
case cmdResume:
if paused {
paused = false
startTime = time.Now()
continue
}
case cmdSkip:
startTime = time.Now()
startPosition = cmd.Position
cursor = 0
continue
}
case <-nextEventTime:
if !paused {
out <- Action{
Position: ta.posLimitFunc(a.Position),
Speed: ta.speedLimitFunc(a.Speed),
}
cursor++
}
}
}
}
// calcPosition will return the current timecode in the script based on start
// time and starting position.
func calcPosition(startTime time.Time, startPosition time.Duration) time.Duration {
return time.Now().Add(startPosition).Sub(startTime)
}