github.com/CyCoreSystems/ari@v4.8.4+incompatible/ext/play/session.go (about) 1 package play 2 3 import ( 4 "context" 5 "errors" 6 "sync" 7 "time" 8 9 "github.com/CyCoreSystems/ari" 10 ) 11 12 // Session describes a structured Play session. 13 type Session interface { 14 // Add appends a set of AudioURIs to a Play session. Note that if the Play session 15 // has already been completed, this will NOT make it start again. 16 Add(list ...string) 17 18 // Done returns a channel which is closed when the Play session completes 19 Done() <-chan struct{} 20 21 // Err waits for a session to end and returns its error 22 Err() error 23 24 // StopAudio stops the playback of the audio sequence (if there is one), but 25 // unlike `Stop()`, this does _not_ necessarily terminate the session. If 26 // the Play session is configured to wait for DTMF following the playback, 27 // it will still wait after StopAudio() is called. 28 StopAudio() 29 30 // Result waits for a session to end and returns its result 31 Result() (*Result, error) 32 33 // Stop stops a Play session immediately 34 Stop() 35 } 36 37 type playSession struct { 38 o *Options 39 40 // cancel is the playback context's cancel function 41 cancel context.CancelFunc 42 43 // currentSequence is a pointer to the currently-playing sequence, if there is one 44 currentSequence *sequence 45 46 // digitChan is the channel on which any received DTMF digits will be sent. The received DTMF will also be stored separately, so this channel is primarily for signaling purposes. 47 digitChan chan string 48 49 // closed is a wrapper for done which indicates that done has been closed 50 closed bool 51 52 // done is a channel which is closed when the playback completes execution 53 done chan struct{} 54 55 // mu provides locking for concurrency-related datastructures within the options 56 mu sync.Mutex 57 58 // result is the final result of the playback 59 result *Result 60 } 61 62 type nilSession struct { 63 res *Result 64 } 65 66 func (n *nilSession) Add(list ...string) { 67 } 68 69 func (n *nilSession) Done() <-chan struct{} { 70 ch := make(chan struct{}) 71 close(ch) 72 73 return ch 74 } 75 76 func (n *nilSession) Err() error { 77 return n.res.Error 78 } 79 80 func (n *nilSession) StopAudio() { 81 } 82 83 func (n *nilSession) Result() (*Result, error) { 84 return n.res, n.res.Error 85 } 86 87 func (n *nilSession) Stop() { 88 } 89 90 func errorSession(err error) *nilSession { 91 s := &nilSession{ 92 res: new(Result), 93 } 94 95 s.res.Error = err 96 s.res.Status = Failed 97 return s 98 } 99 100 func newPlaySession(o *Options) *playSession { 101 return &playSession{ 102 o: o, 103 result: new(Result), 104 digitChan: make(chan string, DigitBufferSize), 105 done: make(chan struct{}), 106 } 107 } 108 109 func (s *playSession) play(ctx context.Context, p ari.Player) { 110 ctx, cancel := context.WithCancel(ctx) 111 s.cancel = cancel 112 113 defer s.Stop() 114 115 if s.result == nil { 116 s.result = new(Result) 117 } 118 119 if s.o.uriList == nil || s.o.uriList.Empty() { 120 s.result.Error = errors.New("empty playback URI list") 121 return 122 } 123 124 // cancel if we go over the maximum time 125 go s.watchMaxTime(ctx) 126 127 // Listen for DTMF 128 go s.listenDTMF(ctx, p) 129 130 for i := 0; i < s.o.maxReplays+1; i++ { 131 if ctx.Err() != nil { 132 break 133 } 134 135 // Reset the digit cache 136 s.result.mu.Lock() 137 s.result.DTMF = "" 138 s.result.mu.Unlock() 139 140 // Play the sequence of audio URIs 141 s.playSequence(ctx, p) 142 if s.result.Error != nil { 143 return 144 } 145 146 // Wait for digits in the silence after the playback sequence completes 147 s.waitDigits(ctx) 148 } 149 } 150 151 // playSequence plays the complete audio sequence 152 func (s *playSession) playSequence(ctx context.Context, p ari.Player) { 153 seq := newSequence(s) 154 155 s.mu.Lock() 156 s.currentSequence = seq 157 s.mu.Unlock() 158 159 go seq.Play(ctx, p) 160 161 // Wait for sequence playback to complete (or context closure to be caught) 162 select { 163 case <-ctx.Done(): 164 case <-seq.Done(): 165 if s.result.Status == InProgress { 166 s.result.Status = Finished 167 } 168 } 169 170 // Stop audio playback if it is still running 171 seq.Stop() 172 173 // wait for cleanup of sequence so we can get the proper error result 174 <-seq.Done() 175 } 176 177 // nolint: gocyclo 178 func (s *playSession) waitDigits(ctx context.Context) { 179 overallTimer := time.NewTimer(s.o.overallDigitTimeout) 180 defer overallTimer.Stop() 181 182 digitTimeout := s.o.firstDigitTimeout 183 184 for { 185 select { 186 case <-ctx.Done(): 187 return 188 case <-time.After(digitTimeout): 189 return 190 case <-overallTimer.C: 191 return 192 case <-s.digitChan: 193 if len(s.result.DTMF) > 0 { 194 digitTimeout = s.o.interDigitTimeout 195 } 196 197 // Determine if a match was found 198 if s.o.matchFunc != nil { 199 s.result.mu.Lock() 200 s.result.DTMF, s.result.MatchResult = s.o.matchFunc(s.result.DTMF) 201 s.result.mu.Unlock() 202 203 switch s.result.MatchResult { 204 case Complete: 205 // If we have a complete response, close the entire playback 206 // and return 207 s.Stop() 208 return 209 case Invalid: 210 // If invalid, return without waiting 211 // for any more digits 212 return 213 default: 214 // Incomplete means we should wait for more 215 } 216 } 217 } 218 } 219 } 220 221 // Stop terminates the execution of a playback 222 func (s *playSession) Stop() { 223 if s.result == nil { 224 s.result = new(Result) 225 } 226 227 // Stop any audio which is still playing 228 if s.currentSequence != nil { 229 s.currentSequence.Stop() 230 <-s.currentSequence.Done() 231 } 232 233 // If we have no other status set, set it to Cancelled 234 if s.result.Status == InProgress { 235 s.result.Status = Cancelled 236 } 237 238 // Close out anything else 239 if s.cancel != nil { 240 s.cancel() 241 } 242 243 if !s.closed { 244 s.closed = true 245 close(s.done) 246 } 247 } 248 249 func (s *playSession) watchMaxTime(ctx context.Context) { 250 select { 251 case <-ctx.Done(): 252 return 253 case <-time.After(s.o.maxPlaybackTime): 254 s.Stop() 255 } 256 } 257 func (s *playSession) listenDTMF(ctx context.Context, p ari.Player) { 258 sub := p.Subscribe(ari.Events.ChannelDtmfReceived) 259 defer sub.Cancel() 260 261 for { 262 select { 263 case <-ctx.Done(): 264 return 265 case e := <-sub.Events(): 266 if e == nil { 267 return 268 } 269 v, ok := e.(*ari.ChannelDtmfReceived) 270 if !ok { 271 continue 272 } 273 s.result.mu.Lock() 274 s.result.DTMF += v.Digit 275 s.result.mu.Unlock() 276 277 // Signal receipt of digit, but never block in doing so 278 select { 279 case s.digitChan <- v.Digit: 280 default: 281 } 282 283 // If we have a MatchFunc, stop any playing audio 284 if s.o.matchFunc != nil && s.currentSequence != nil { 285 s.currentSequence.Stop() 286 } 287 } 288 } 289 } 290 func (s *playSession) Add(list ...string) { 291 for _, i := range list { 292 s.o.uriList.Add(i) 293 } 294 } 295 296 func (s *playSession) Done() <-chan struct{} { 297 return s.done 298 } 299 300 func (s *playSession) Err() error { 301 <-s.Done() 302 return s.result.Error 303 } 304 305 func (s *playSession) StopAudio() { 306 if s.currentSequence != nil { 307 s.currentSequence.Stop() 308 <-s.currentSequence.Done() 309 } 310 } 311 312 func (s *playSession) Result() (*Result, error) { 313 <-s.Done() 314 return s.result, s.result.Error 315 }