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  }