github.com/CyCoreSystems/ari@v4.8.4+incompatible/ext/play/options.go (about) 1 package play 2 3 import ( 4 "container/list" 5 "strings" 6 "sync" 7 "time" 8 9 "github.com/pkg/errors" 10 ) 11 12 var ( 13 // DefaultPlaybackStartTimeout is the default amount of time to wait for a playback to start before declaring that the playback has failed. 14 DefaultPlaybackStartTimeout = 2 * time.Second 15 16 // DefaultMaxPlaybackTime is the default maximum amount of time any playback is allowed to run. If this time is exeeded, the playback will be cancelled. 17 DefaultMaxPlaybackTime = 10 * time.Minute 18 19 // DefaultFirstDigitTimeout is the default amount of time to wait, after the playback for all audio completes, for the first digit to be received. 20 DefaultFirstDigitTimeout = 4 * time.Second 21 22 // DefaultInterDigitTimeout is the maximum time to wait for additional 23 // digits after the first is received. 24 DefaultInterDigitTimeout = 3 * time.Second 25 26 // DefaultOverallDigitTimeout is the default maximum time to wait for a 27 // response, after the playback for all audio is complete, regardless of the 28 // number of received digits or pattern matching. 29 DefaultOverallDigitTimeout = 3 * time.Minute 30 31 // DigitBufferSize is the number of digits stored in the received-digit 32 // event buffer before further digit events are ignored. NOTE that digits 33 // overflowing this buffer are still stored in the digits received buffer. 34 // This only affects the digit _signaling_ buffer. 35 DigitBufferSize = 20 36 ) 37 38 // Result describes the result of a playback operation 39 type Result struct { 40 mu sync.Mutex 41 42 // Duration indicates how long the playback execution took, from start to finish 43 Duration time.Duration 44 45 // DTMF records any DTMF which was received by the playback, as modified by any match functions 46 DTMF string 47 48 // Error indicates any error encountered which caused the termination of the playback 49 Error error 50 51 // MatchResult indicates the final result of any applied match function for DTMF digits which were received 52 MatchResult MatchResult 53 54 // Status indicates the resulting status of the playback, why it was stopped 55 Status Status 56 } 57 58 // Status indicates the final status of a playback, be it individual of an entire sequence. This Status indicates the reason the playback stopped. 59 type Status int 60 61 const ( 62 // InProgress indicates that the audio is currently playing or is staged to play 63 InProgress Status = iota 64 65 // Cancelled indicates that the audio was cancelled. This cancellation could be due 66 // to anything from the control context being closed or a DTMF Match being found 67 Cancelled 68 69 // Failed indicates that the audio playback failed. This indicates that one 70 // or more of the audio playbacks failed to be played. This could be due to 71 // a system, network, or Asterisk error, but it could also be due to an 72 // invalid audio URI. Check the returned error for more details. 73 Failed 74 75 // Finished indicates that the playback completed playing all bound audio 76 // URIs in full. Note that for a prompt-style execution, this also means 77 // that no DTMF was matched to the match function. 78 Finished 79 80 // Hangup indicates that the audio playback was interrupted due to a hangup. 81 Hangup 82 83 // Timeout indicates that audio playback timed out. It is not known whether this was due to a failure in the playback, a network loss, or some other problem. 84 Timeout 85 ) 86 87 // MatchResult indicates the status of a match for the received DTMF of a playback 88 type MatchResult int 89 90 const ( 91 // Incomplete indicates that there are not enough digits to determine a match 92 Incomplete MatchResult = iota 93 94 // Complete indicates that a match was found and the current DTMF pattern is complete 95 Complete 96 97 // Invalid indicates that a match cannot befound from the current DTMF received set 98 Invalid 99 ) 100 101 type uriList struct { 102 list *list.List 103 current *list.Element 104 mu sync.Mutex 105 } 106 107 func (u *uriList) Empty() bool { 108 if u == nil || u.list == nil || u.list.Len() == 0 { 109 return true 110 } 111 return false 112 } 113 114 func (u *uriList) Add(uri string) { 115 u.mu.Lock() 116 defer u.mu.Unlock() 117 118 if u.list == nil { 119 u.list = list.New() 120 } 121 122 u.list.PushBack(uri) 123 124 if u.current == nil { 125 u.current = u.list.Front() 126 } 127 } 128 129 func (u *uriList) First() string { 130 if u.list == nil { 131 return "" 132 } 133 134 u.mu.Lock() 135 defer u.mu.Unlock() 136 137 u.current = u.list.Front() 138 return u.val() 139 } 140 141 func (u *uriList) Next() string { 142 if u.list == nil { 143 return "" 144 } 145 146 u.mu.Lock() 147 defer u.mu.Unlock() 148 149 if u.current == nil { 150 return "" 151 } 152 153 u.current = u.current.Next() 154 return u.val() 155 } 156 157 func (u *uriList) val() string { 158 if u.current == nil { 159 return "" 160 } 161 162 ret, ok := u.current.Value.(string) 163 if !ok { 164 return "" 165 } 166 return ret 167 } 168 169 // Options represent the various playback options which can modify the operation of a Playback. 170 type Options struct { 171 // uriList is the list of audio URIs to play 172 uriList *uriList 173 174 // playbackStartTimeout defines the amount of time to wait for a playback to 175 // start before declaring it failed. 176 // 177 // This value is important because ARI does NOT report playback failures in 178 // any usable way. 179 // 180 // If not specified, the default is DefaultPlaybackStartTimeout 181 playbackStartTimeout time.Duration 182 183 // maxPlaybackTime is the maximum amount of time to wait for a playback 184 // session to complete, everything included. The playback will be 185 // terminated if this time is exceeded. 186 maxPlaybackTime time.Duration 187 188 // firstDigitTimeout is the maximum length of time to wait 189 // after the prompt sequence ends for the user to enter 190 // a response. 191 // 192 // If not specified, the default is DefaultFirstDigitTimeout. 193 firstDigitTimeout time.Duration 194 195 // interDigitTimeout is the maximum length of time to wait 196 // for an additional digit after a digit is received. 197 // 198 // If not specified, the default is DefaultInterDigitTimeout. 199 interDigitTimeout time.Duration 200 201 // overallDigitTimeout is the maximum length of time to wait 202 // for a response regardless of digits received after the completion 203 // of all audio playbacks. 204 // If not specified, the default is DefaultOverallTimeout. 205 overallDigitTimeout time.Duration 206 207 // matchFunc is an optional function which, if supplied, returns 208 // a string and an int. 209 // 210 // The string is allows the MatchFunc to return a different number 211 // to be used as `result.Data`. This is commonly used for prompts 212 // which look for a terminator. In such a practice, the terminator 213 // would be stripped from the match and this argument would be populated 214 // with the result. Otherwise, the original string should be returned. 215 // NOTE: Whatever is returned here will become `result.Data`. 216 // 217 // The int parameter indicates the result of the match, and it should 218 // be one of: 219 // Incomplete (0) : insufficient digits to determine match. 220 // Complete (1) : A match was found. 221 // Invalid (2) : A match could not be found, given the digits received. 222 // If this function returns a non-zero int, then the prompt will be stopped. 223 // If not specified MatchAny will be used. 224 matchFunc func(string) (string, MatchResult) 225 226 // maxReplays is the maximum number of times the audio sequence will be 227 // replayed if there is no response. By default, the audio sequence is 228 // played only once. 229 maxReplays int 230 } 231 232 // NewDefaultOptions returns a set of options which represent reasonable defaults for most simple playbacks. 233 func NewDefaultOptions() *Options { 234 opts := &Options{ 235 playbackStartTimeout: DefaultPlaybackStartTimeout, 236 maxPlaybackTime: DefaultMaxPlaybackTime, 237 uriList: new(uriList), 238 } 239 240 MatchAny()(opts) // nolint No error is possible with MatchAny 241 242 return opts 243 } 244 245 // ApplyOptions applies a set of OptionFuncs to the Playback 246 func (o *Options) ApplyOptions(opts ...OptionFunc) (err error) { 247 for _, f := range opts { 248 err = f(o) 249 if err != nil { 250 return errors.Wrap(err, "failed to apply option") 251 } 252 } 253 return nil 254 } 255 256 // NewPromptOptions returns a set of options which represent reasonable defaults for most prompt playbacks. It will terminate when any single DTMF digit is received. 257 func NewPromptOptions() *Options { 258 opts := NewDefaultOptions() 259 260 opts.firstDigitTimeout = DefaultFirstDigitTimeout 261 opts.interDigitTimeout = DefaultInterDigitTimeout 262 opts.overallDigitTimeout = DefaultOverallDigitTimeout 263 264 return opts 265 } 266 267 // OptionFunc defines an interface for functions which can modify a play session's Options 268 type OptionFunc func(*Options) error 269 270 // NoExitOnDTMF disables exiting the playback when DTMF is received. Note that 271 // this is just a wrapper for MatchFunc(nil), so it is mutually exclusive with 272 // MatchFunc; whichever comes later will win. 273 func NoExitOnDTMF() OptionFunc { 274 return func(o *Options) error { 275 o.matchFunc = nil 276 return nil 277 } 278 } 279 280 // URI adds a set of audio URIs to a playback 281 func URI(uri ...string) OptionFunc { 282 return func(o *Options) error { 283 if o.uriList == nil { 284 o.uriList = new(uriList) 285 } 286 287 for _, u := range uri { 288 if u != "" { 289 o.uriList.Add(u) 290 } 291 } 292 return nil 293 } 294 } 295 296 // PlaybackStartTimeout overrides the default playback start timeout 297 func PlaybackStartTimeout(timeout time.Duration) OptionFunc { 298 return func(o *Options) error { 299 o.playbackStartTimeout = timeout 300 return nil 301 } 302 } 303 304 // DigitTimeouts sets the digit timeouts. Passing a negative value to any of these indicates that the default value (shown in parentheses below) should be used. 305 // 306 // - First digit timeout (4 sec): The time (after the stop of the audio) to wait for the first digit to be received 307 // 308 // - Inter digit timeout (3 sec): The time (after receiving a digit) to wait for the _next_ digit to be received 309 // 310 // - Overall digit timeout (3 min): The maximum amount of time to wait (after the stop of the audio) for digits to be received, regardless of the digit frequency 311 // 312 func DigitTimeouts(first, inter, overall time.Duration) OptionFunc { 313 return func(o *Options) error { 314 if first >= 0 { 315 o.firstDigitTimeout = first 316 } 317 if inter >= 0 { 318 o.interDigitTimeout = inter 319 } 320 if overall >= 0 { 321 o.overallDigitTimeout = overall 322 } 323 return nil 324 } 325 } 326 327 // Replays sets the number of replays of the audio sequence before exiting 328 func Replays(count int) OptionFunc { 329 return func(o *Options) error { 330 o.maxReplays = count 331 return nil 332 } 333 } 334 335 // MatchAny indicates that the playback should be considered Matched and terminated if 336 // any DTMF digit is received during the playback or post-playback time. 337 func MatchAny() OptionFunc { 338 return func(o *Options) error { 339 o.matchFunc = func(pat string) (string, MatchResult) { 340 if len(pat) > 0 { 341 return pat, Complete 342 } 343 return pat, Incomplete 344 } 345 return nil 346 } 347 } 348 349 // MatchNone indicates that the playback should never be terminated due to DTMF 350 func MatchNone() OptionFunc { 351 return func(o *Options) error { 352 o.matchFunc = nil 353 return nil 354 } 355 } 356 357 // MatchDiscrete indicates that the playback should be considered Matched and terminated if 358 // the received DTMF digits match any of the discrete list of strings. 359 func MatchDiscrete(list []string) OptionFunc { 360 return func(o *Options) error { 361 o.matchFunc = func(pat string) (string, MatchResult) { 362 var maxLen int 363 for _, t := range list { 364 if t == pat { 365 return pat, Complete 366 } 367 if len(t) > maxLen { 368 maxLen = len(t) 369 } 370 } 371 if len(pat) > maxLen { 372 return pat, Invalid 373 } 374 return pat, Incomplete 375 } 376 return nil 377 } 378 } 379 380 // MatchHash indicates that the playback should be considered Matched and terminated if it contains a hash (#). The hash (and any subsequent digits) is removed from the final result. 381 func MatchHash() OptionFunc { 382 return func(o *Options) error { 383 o.matchFunc = func(pat string) (string, MatchResult) { 384 if strings.Contains(pat, "#") { 385 return strings.Split(pat, "#")[0], Complete 386 } 387 return pat, Incomplete 388 } 389 return nil 390 } 391 } 392 393 // MatchTerminator indicates that the playback shoiuld be considered Matched and terminated if it contains the provided Terminator string. The terminator (and any subsequent digits) is removed from the final result. 394 func MatchTerminator(terminator string) OptionFunc { 395 return func(o *Options) error { 396 o.matchFunc = func(pat string) (string, MatchResult) { 397 if strings.Contains(pat, terminator) { 398 return strings.Split(pat, terminator)[0], Complete 399 } 400 return pat, Incomplete 401 } 402 return nil 403 } 404 } 405 406 // MatchLen indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved. 407 func MatchLen(length int) OptionFunc { 408 return func(o *Options) error { 409 o.matchFunc = func(pat string) (string, MatchResult) { 410 if len(pat) >= length { 411 return pat, Complete 412 } 413 return pat, Incomplete 414 } 415 return nil 416 } 417 } 418 419 // MatchLenOrTerminator indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved or if the given terminator is received. If the terminator is present, it and any subsequent digits will be removed from the final result. 420 func MatchLenOrTerminator(length int, terminator string) OptionFunc { 421 return func(o *Options) error { 422 o.matchFunc = func(pat string) (string, MatchResult) { 423 if len(pat) >= length { 424 return pat, Complete 425 } 426 if strings.Contains(pat, terminator) { 427 return strings.Split(pat, terminator)[0], Complete 428 } 429 return pat, Incomplete 430 } 431 return nil 432 } 433 } 434 435 // MatchFunc uses the provided match function to determine when the playback should be terminated based on DTMF input. 436 func MatchFunc(f func(string) (string, MatchResult)) OptionFunc { 437 return func(o *Options) error { 438 o.matchFunc = f 439 return nil 440 } 441 }