adhearsion 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.travis.yml +4 -3
  2. data/CHANGELOG.md +30 -0
  3. data/README.markdown +1 -0
  4. data/adhearsion.gemspec +3 -4
  5. data/bin/ahn +0 -20
  6. data/features/cli_create.feature +1 -1
  7. data/features/cli_restart.feature +25 -1
  8. data/features/cli_start.feature +0 -2
  9. data/features/plugin_generator.feature +66 -15
  10. data/features/support/env.rb +0 -13
  11. data/lib/adhearsion.rb +26 -6
  12. data/lib/adhearsion/call.rb +42 -7
  13. data/lib/adhearsion/call_controller.rb +5 -2
  14. data/lib/adhearsion/call_controller/dial.rb +92 -50
  15. data/lib/adhearsion/call_controller/input.rb +19 -6
  16. data/lib/adhearsion/call_controller/menu_dsl/menu.rb +4 -0
  17. data/lib/adhearsion/call_controller/output.rb +143 -161
  18. data/lib/adhearsion/call_controller/output/abstract_player.rb +30 -0
  19. data/lib/adhearsion/call_controller/output/async_player.rb +26 -0
  20. data/lib/adhearsion/call_controller/output/formatter.rb +81 -0
  21. data/lib/adhearsion/call_controller/output/player.rb +25 -0
  22. data/lib/adhearsion/call_controller/record.rb +19 -2
  23. data/lib/adhearsion/events.rb +3 -0
  24. data/lib/adhearsion/foundation.rb +12 -6
  25. data/lib/adhearsion/foundation/exception_handler.rb +8 -6
  26. data/lib/adhearsion/generators/app/templates/README.md +13 -0
  27. data/lib/adhearsion/generators/app/templates/config/adhearsion.rb +7 -1
  28. data/lib/adhearsion/generators/plugin/plugin_generator.rb +1 -0
  29. data/lib/adhearsion/generators/plugin/templates/plugin-template.gemspec.tt +3 -7
  30. data/lib/adhearsion/generators/plugin/templates/spec/spec_helper.rb.tt +0 -1
  31. data/lib/adhearsion/outbound_call.rb +15 -5
  32. data/lib/adhearsion/punchblock_plugin.rb +13 -2
  33. data/lib/adhearsion/punchblock_plugin/initializer.rb +13 -12
  34. data/lib/adhearsion/router.rb +43 -2
  35. data/lib/adhearsion/router/evented_route.rb +15 -0
  36. data/lib/adhearsion/router/openended_route.rb +16 -0
  37. data/lib/adhearsion/router/route.rb +31 -13
  38. data/lib/adhearsion/router/unaccepting_route.rb +11 -0
  39. data/lib/adhearsion/version.rb +1 -1
  40. data/pre-commit +14 -1
  41. data/spec/adhearsion/call_controller/dial_spec.rb +105 -10
  42. data/spec/adhearsion/call_controller/input_spec.rb +19 -21
  43. data/spec/adhearsion/call_controller/output/async_player_spec.rb +67 -0
  44. data/spec/adhearsion/call_controller/output/formatter_spec.rb +90 -0
  45. data/spec/adhearsion/call_controller/output/player_spec.rb +65 -0
  46. data/spec/adhearsion/call_controller/output_spec.rb +436 -190
  47. data/spec/adhearsion/call_controller/record_spec.rb +49 -6
  48. data/spec/adhearsion/call_controller_spec.rb +10 -2
  49. data/spec/adhearsion/call_spec.rb +138 -0
  50. data/spec/adhearsion/calls_spec.rb +1 -1
  51. data/spec/adhearsion/outbound_call_spec.rb +48 -8
  52. data/spec/adhearsion/punchblock_plugin/initializer_spec.rb +34 -23
  53. data/spec/adhearsion/router/evented_route_spec.rb +34 -0
  54. data/spec/adhearsion/router/openended_route_spec.rb +61 -0
  55. data/spec/adhearsion/router/route_spec.rb +26 -4
  56. data/spec/adhearsion/router/unaccepting_route_spec.rb +72 -0
  57. data/spec/adhearsion/router_spec.rb +107 -2
  58. data/spec/adhearsion_spec.rb +19 -0
  59. data/spec/capture_warnings.rb +28 -21
  60. data/spec/spec_helper.rb +2 -3
  61. data/spec/support/call_controller_test_helpers.rb +31 -30
  62. metadata +32 -29
@@ -26,11 +26,13 @@ module Adhearsion
26
26
  # @param [Hash] options see below
27
27
  #
28
28
  # @option options [String] :from the caller id to be used when the call is placed. It is advised you properly adhere to the
29
- # policy of VoIP termination providers with respect to caller id values.
29
+ # policy of VoIP termination providers with respect to caller id values. Defaults to the caller ID of the dialing call, so for normal bridging scenarios, you do not need to set this.
30
30
  #
31
31
  # @option options [Numeric] :for this option can be thought of best as a timeout.
32
32
  # i.e. timeout after :for if no one answers the call
33
33
  #
34
+ # @option options [CallController] :confirm the controller to execute on answered outbound calls to give an opportunity to screen the call. The calls will be joined if the outbound call is still active after this controller completes.
35
+ #
34
36
  # @example Make a call to the PSTN using my SIP provider for VoIP termination
35
37
  # dial "SIP/19095551001@my.sip.voip.terminator.us"
36
38
  #
@@ -43,77 +45,112 @@ module Adhearsion
43
45
  # @return [DialStatus] the status of the dial operation
44
46
  #
45
47
  def dial(to, options = {}, latch = nil)
46
- targets = to.respond_to?(:has_key?) ? to : Array(to)
48
+ dial = Dial.new to, options, latch, call
49
+ dial.run
50
+ dial.await_completion
51
+ dial.cleanup_calls
52
+ dial.status
53
+ end
47
54
 
48
- status = DialStatus.new
55
+ class Dial
56
+ attr_accessor :status
49
57
 
50
- latch ||= CountDownLatch.new targets.size
58
+ def initialize(to, options, latch, call)
59
+ raise Call::Hangup unless call.alive? && call.active?
60
+ @options, @latch, @call = options, latch, call
61
+ @targets = to.respond_to?(:has_key?) ? to : Array(to)
62
+ set_defaults
63
+ end
51
64
 
52
- call.on_end { |_| latch.countdown! until latch.count == 0 }
65
+ def set_defaults
66
+ @status = DialStatus.new
53
67
 
54
- _for = options.delete :for
55
- options[:timeout] ||= _for if _for
68
+ @latch ||= CountDownLatch.new @targets.size
56
69
 
57
- options[:from] ||= call.from
70
+ @options[:from] ||= @call.from
58
71
 
59
- calls = targets.map do |target, specific_options|
60
- new_call = OutboundCall.new
72
+ _for = @options.delete :for
73
+ @options[:timeout] ||= _for if _for
61
74
 
62
- new_call.on_answer do |event|
63
- calls.each do |call_to_hangup, _|
64
- begin
65
- next if call_to_hangup.id == new_call.id
66
- logger.debug "#dial hanging up call #{call_to_hangup.id} because this call has been answered by another channel"
67
- call_to_hangup.hangup
68
- rescue Celluloid::DeadActorError
69
- # This actor may previously have been shut down due to the call ending
70
- end
71
- end
75
+ @confirmation_controller = @options.delete :confirm
76
+ end
72
77
 
73
- new_call.register_event_handler Punchblock::Event::Unjoined, :call_id => call.id do |unjoined|
74
- new_call["dial_countdown_#{call.id}"] = true
75
- latch.countdown!
76
- throw :pass
78
+ def run
79
+ track_originating_call
80
+ prep_calls
81
+ place_calls
82
+ end
83
+
84
+ def track_originating_call
85
+ @call.on_end { |_| @latch.countdown! until @latch.count == 0 }
86
+ end
87
+
88
+ def prep_calls
89
+ @calls = @targets.map do |target, specific_options|
90
+ new_call = OutboundCall.new
91
+
92
+ new_call.on_end do |event|
93
+ @latch.countdown! unless new_call["dial_countdown_#{@call.id}"]
94
+ status.error! if event.reason == :error
77
95
  end
78
96
 
79
- logger.debug "#dial joining call #{new_call.id} to #{call.id}"
80
- new_call.join call
81
- status.answer!
82
- end
97
+ new_call.on_answer do |event|
98
+ @calls.each do |call_to_hangup, _|
99
+ begin
100
+ next if call_to_hangup.id == new_call.id
101
+ logger.debug "#dial hanging up call #{call_to_hangup.id} because this call has been answered by another channel"
102
+ call_to_hangup.hangup
103
+ rescue Celluloid::DeadActorError
104
+ # This actor may previously have been shut down due to the call ending
105
+ end
106
+ end
83
107
 
84
- new_call.on_end do |event|
85
- latch.countdown! unless new_call["dial_countdown_#{call.id}"]
108
+ new_call.on_unjoined @call do |unjoined|
109
+ new_call["dial_countdown_#{@call.id}"] = true
110
+ @latch.countdown!
111
+ end
112
+
113
+ if @confirmation_controller
114
+ status.unconfirmed!
115
+ new_call.execute_controller @confirmation_controller.new(new_call), lambda { |call| call.signal :confirmed }
116
+ new_call.wait :confirmed
117
+ end
86
118
 
87
- case event.reason
88
- when :error
89
- status.error!
119
+ if new_call.alive? && new_call.active?
120
+ logger.debug "#dial joining call #{new_call.id} to #{@call.id}"
121
+ new_call.join @call
122
+ status.answer!
123
+ end
90
124
  end
125
+
126
+ [new_call, target, specific_options]
91
127
  end
92
128
 
93
- [new_call, target, specific_options]
129
+ status.calls = @calls
94
130
  end
95
131
 
96
- calls.map! do |call, target, specific_options|
97
- local_options = options.dup.deep_merge specific_options if specific_options
98
- call.dial target, (local_options || options)
99
- call
132
+ def place_calls
133
+ @calls.map! do |call, target, specific_options|
134
+ local_options = @options.dup.deep_merge specific_options if specific_options
135
+ call.dial target, (local_options || @options)
136
+ call
137
+ end
100
138
  end
101
139
 
102
- status.calls = calls
103
-
104
- no_timeout = latch.wait options[:timeout]
105
- status.timeout! unless no_timeout
140
+ def await_completion
141
+ @latch.wait(@options[:timeout]) || status.timeout!
142
+ end
106
143
 
107
- logger.debug "#dial finished. Hanging up #{calls.size} outbound calls: #{calls.map(&:id).join ", "}."
108
- calls.each do |outbound_call|
109
- begin
110
- outbound_call.hangup
111
- rescue Celluloid::DeadActorError
112
- # This actor may previously have been shut down due to the call ending
144
+ def cleanup_calls
145
+ logger.debug "#dial finished. Hanging up #{@calls.size} outbound calls: #{@calls.map(&:id).join ", "}."
146
+ @calls.each do |outbound_call|
147
+ begin
148
+ outbound_call.hangup
149
+ rescue Celluloid::DeadActorError
150
+ # This actor may previously have been shut down due to the call ending
151
+ end
113
152
  end
114
153
  end
115
-
116
- status
117
154
  end
118
155
 
119
156
  class DialStatus
@@ -147,6 +184,11 @@ module Adhearsion
147
184
  def error!
148
185
  @result ||= :error
149
186
  end
187
+
188
+ # @private
189
+ def unconfirmed!
190
+ @result ||= :unconfirmed
191
+ end
150
192
  end
151
193
 
152
194
  end
@@ -8,6 +8,10 @@ module Adhearsion
8
8
  def to_s
9
9
  response
10
10
  end
11
+
12
+ def inspect
13
+ "#<Adhearsion::CallController::Input::Result response=#{response.inspect}, status=#{status.inspect}>"
14
+ end
11
15
  end
12
16
 
13
17
  #
@@ -46,12 +50,21 @@ module Adhearsion
46
50
  menu_instance.validate :basic
47
51
  result_of_menu = nil
48
52
 
49
- until MenuDSL::Menu::MenuResultDone === result_of_menu
50
- result_of_menu = menu_instance.continue
53
+ catch :finish do
54
+ until MenuDSL::Menu::MenuResultDone === result_of_menu
55
+ raise unless menu_instance.should_continue?
56
+
57
+ result_of_menu = menu_instance.continue
51
58
 
52
- if result_of_menu.is_a?(MenuDSL::Menu::MenuGetAnotherDigit)
53
- next_digit = play_sound_files_for_menu menu_instance, sound_files
54
- menu_instance << next_digit if next_digit
59
+ if result_of_menu.is_a?(MenuDSL::Menu::MenuGetAnotherDigit)
60
+ next_digit = play_sound_files_for_menu menu_instance, sound_files
61
+ if next_digit
62
+ menu_instance << next_digit
63
+ else
64
+ menu_instance.timeout!
65
+ throw :finish
66
+ end
67
+ end
55
68
  end
56
69
  end
57
70
 
@@ -194,7 +207,7 @@ module Adhearsion
194
207
  def wait_for_digit(timeout = 1)
195
208
  timeout = nil if timeout == -1
196
209
  timeout *= 1_000 if timeout
197
- input_component = execute_component_and_await_completion ::Punchblock::Component::Input.new :mode => :dtmf,
210
+ input_component = execute_component_and_await_completion Punchblock::Component::Input.new :mode => :dtmf,
198
211
  :initial_timeout => timeout,
199
212
  :inter_digit_timeout => timeout,
200
213
  :grammar => {
@@ -91,6 +91,10 @@ module Adhearsion
91
91
  digit_buffer.clear!
92
92
  end
93
93
 
94
+ def timeout!
95
+ @status = :timeout
96
+ end
97
+
94
98
  def execute_invalid_hook
95
99
  builder.execute_hook_for :invalid, digit_buffer_string
96
100
  end
@@ -3,6 +3,13 @@
3
3
  module Adhearsion
4
4
  class CallController
5
5
  module Output
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :AbstractPlayer
9
+ autoload :AsyncPlayer
10
+ autoload :Formatter
11
+ autoload :Player
12
+
6
13
  PlaybackError = Class.new Adhearsion::Error # Represents failure to play audio, such as when the sound file cannot be found
7
14
 
8
15
  #
@@ -11,11 +18,26 @@ module Adhearsion
11
18
  # @param [String, #to_s] text The text to be rendered
12
19
  # @param [Hash] options A set of options for output
13
20
  #
21
+ # @raises [PlaybackError] if the given argument could not be played
22
+ #
14
23
  def say(text, options = {})
15
- play_ssml(text, options) || output(ssml_for_text(text.to_s), options)
24
+ player.play_ssml(text, options) || player.output(Formatter.ssml_for_text(text.to_s), options)
16
25
  end
17
26
  alias :speak :say
18
27
 
28
+ #
29
+ # Speak output using text-to-speech (TTS) and return as soon as it begins
30
+ #
31
+ # @param [String, #to_s] text The text to be rendered
32
+ # @param [Hash] options A set of options for output
33
+ #
34
+ # @raises [PlaybackError] if the given argument could not be played
35
+ #
36
+ def say!(text, options = {})
37
+ async_player.play_ssml(text, options) || async_player.output(Formatter.ssml_for_text(text.to_s), options)
38
+ end
39
+ alias :speak! :say!
40
+
19
41
  #
20
42
  # Plays the specified sound file names. This method will handle Time/DateTime objects (e.g. Time.now),
21
43
  # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. To specify how the Date/Time objects are said
@@ -36,34 +58,70 @@ module Adhearsion
36
58
  # @example Play two sound files
37
59
  # play "/path/to/you-sound-cute.mp3", "/path/to/what-are-you-wearing.wav"
38
60
  #
39
- # @return [Boolean] true is returned if everything was successful. Otherwise, false indicates that
40
- # some sound file(s) could not be played.
41
- #
42
- # @see play_time
43
- # @see play_numeric
44
- # @see play_audio
61
+ # @raises [PlaybackError] if (one of) the given argument(s) could not be played
45
62
  #
46
63
  def play(*arguments)
47
- arguments.inject(true) do |value, argument|
48
- value = case argument
49
- when Hash
50
- play_ssml_for argument.delete(:value), argument
51
- when RubySpeech::SSML::Speak
52
- play_ssml argument
53
- else
54
- play_ssml_for argument
55
- end
56
- end
64
+ player.play_ssml Formatter.ssml_for_collection(arguments)
65
+ true
57
66
  end
58
67
 
59
68
  #
60
- # Plays the specified input arguments, raising an exception if any can't be played.
61
- # @see play
69
+ # Plays the specified sound file names and returns as soon as it begins. This method will handle Time/DateTime objects (e.g. Time.now),
70
+ # Fixnums (e.g. 1000), Strings which are valid Fixnums (e.g "123"), and direct sound files. To specify how the Date/Time objects are said
71
+ # pass in as an array with the first parameter as the Date/Time/DateTime object along with a hash with the
72
+ # additional options. See play_time for more information.
62
73
  #
63
- # @private
74
+ # @example Play file hello-world
75
+ # play 'http://www.example.com/hello-world.mp3'
76
+ # play '/path/on/disk/hello-world.wav'
77
+ # @example Speak current time
78
+ # play Time.now
79
+ # @example Speak today's date
80
+ # play Date.today
81
+ # @example Speak today's date in a specific format
82
+ # play Date.today, :strftime => "%d/%m/%Y", :format => "dmy"
83
+ # @example Play sound file, speak number, play two more sound files
84
+ # play %w"http://www.example.com/a-connect-charge-of.wav 22 /path/to/cents-per-minute.wav /path/to/will-apply.mp3"
85
+ # @example Play two sound files
86
+ # play "/path/to/you-sound-cute.mp3", "/path/to/what-are-you-wearing.wav"
87
+ #
88
+ # @raises [PlaybackError] if (one of) the given argument(s) could not be played
89
+ # @returns [Punchblock::Component::Output]
64
90
  #
65
91
  def play!(*arguments)
66
- play(*arguments) or raise PlaybackError, "One of the passed outputs is invalid"
92
+ async_player.play_ssml Formatter.ssml_for_collection(arguments)
93
+ end
94
+
95
+ #
96
+ # Plays the given audio file.
97
+ # SSML supports http:// paths and full disk paths.
98
+ # The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory.
99
+ #
100
+ # @param [String] file http:// URL or full disk path to the sound file
101
+ # @param [Hash] options Additional options to specify how exactly to say time specified.
102
+ # @option options [String] :fallback The text to play if the file is not available
103
+ #
104
+ # @raises [PlaybackError] if (one of) the given argument(s) could not be played
105
+ #
106
+ def play_audio(file, options = nil)
107
+ player.play_ssml Formatter.ssml_for_audio(file, options)
108
+ true
109
+ end
110
+
111
+ #
112
+ # Plays the given audio file and returns as soon as it begins.
113
+ # SSML supports http:// paths and full disk paths.
114
+ # The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory.
115
+ #
116
+ # @param [String] file http:// URL or full disk path to the sound file
117
+ # @param [Hash] options Additional options to specify how exactly to say time specified.
118
+ # @option options [String] :fallback The text to play if the file is not available
119
+ #
120
+ # @raises [PlaybackError] if (one of) the given argument(s) could not be played
121
+ # @returns [Punchblock::Component::Output]
122
+ #
123
+ def play_audio!(file, options = nil)
124
+ async_player.play_ssml Formatter.ssml_for_audio(file, options)
67
125
  end
68
126
 
69
127
  #
@@ -79,71 +137,63 @@ module Adhearsion
79
137
  # @option options [String] :strftime This format is what defines the string that is sent to the Speech Synthesis Engine.
80
138
  # It uses Time::strftime symbols.
81
139
  #
82
- # @return [Boolean] true if successful, false if the given argument could not be played.
140
+ # @raises [ArgumentError] if the given argument can not be played
83
141
  #
84
142
  def play_time(time, options = {})
85
- return false unless [Date, Time, DateTime].include? time.class
86
-
87
- return false unless options.is_a? Hash
88
- play_ssml ssml_for_time(time, options)
143
+ raise ArgumentError unless [Date, Time, DateTime].include?(time.class) && options.is_a?(Hash)
144
+ player.play_ssml Formatter.ssml_for_time(time, options)
145
+ true
89
146
  end
90
147
 
91
148
  #
92
- # Plays the given Numeric argument or string representing a decimal number.
93
- # When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
94
- # is pronounced as "one hundred" instead of "one zero zero".
149
+ # Plays the given Date, Time, or Integer (seconds since epoch)
150
+ # using the given timezone and format and returns as soon as it begins.
95
151
  #
96
- # @param [Numeric, String] Numeric or String containing a valid Numeric, like "321".
152
+ # @param [Date, Time, DateTime] time Time to be said.
153
+ # @param [Hash] options Additional options to specify how exactly to say time specified.
154
+ # @option options [String] :format This format is used only to disambiguate times that could be interpreted in different ways.
155
+ # For example, 01/06/2011 could mean either the 1st of June or the 6th of January.
156
+ # Please refer to the SSML specification.
157
+ # @see http://www.w3.org/TR/ssml-sayas/#S3.1
158
+ # @option options [String] :strftime This format is what defines the string that is sent to the Speech Synthesis Engine.
159
+ # It uses Time::strftime symbols.
97
160
  #
98
- # @return [Boolean] true if successful, false if the given argument could not be played.
161
+ # @raises [ArgumentError] if the given argument can not be played
162
+ # @returns [Punchblock::Component::Output]
99
163
  #
100
- def play_numeric(number, options = nil)
101
- if number.kind_of?(Numeric) || number =~ /^\d+$/
102
- play_ssml ssml_for_numeric(number, options)
103
- end
164
+ def play_time!(time, options = {})
165
+ raise ArgumentError unless [Date, Time, DateTime].include?(time.class) && options.is_a?(Hash)
166
+ async_player.play_ssml Formatter.ssml_for_time(time, options)
104
167
  end
105
168
 
106
169
  #
107
- # Plays the given audio file.
108
- # SSML supports http:// paths and full disk paths.
109
- # The Punchblock backend will have to handle cases like Asterisk where there is a fixed sounds directory.
170
+ # Plays the given Numeric argument or string representing a decimal number.
171
+ # When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
172
+ # is pronounced as "one hundred" instead of "one zero zero".
110
173
  #
111
- # @param [String] file http:// URL or full disk path to the sound file
112
- # @param [Hash] options Additional options to specify how exactly to say time specified.
113
- # @option options [String] :fallback The text to play if the file is not available
174
+ # @param [Numeric, String] Numeric or String containing a valid Numeric, like "321".
114
175
  #
115
- # @return [Boolean] true on correct play of the file, false on file missing or not playable
176
+ # @raises [ArgumentError] if the given argument can not be played
116
177
  #
117
- def play_audio(file, options = nil)
118
- play_ssml ssml_for_audio(file, options)
119
- end
120
-
121
- # @private
122
- def play_ssml(ssml, options = {})
123
- if [RubySpeech::SSML::Speak, Nokogiri::XML::Document].include? ssml.class
124
- output ssml.to_s, options
125
- end
126
- end
127
-
128
- # @private
129
- def output(content, options = {})
130
- options.merge! :ssml => content
131
- execute_component_and_await_completion ::Punchblock::Component::Output.new(options)
178
+ def play_numeric(number)
179
+ raise ArgumentError unless number.kind_of?(Numeric) || number =~ /^\d+$/
180
+ player.play_ssml Formatter.ssml_for_numeric(number)
181
+ true
132
182
  end
133
183
 
134
184
  #
135
- # Same as interruptible_play, but throws an error if unable to play the output
136
- # @see interruptible_play
185
+ # Plays the given Numeric argument or string representing a decimal number and returns as soon as it begins.
186
+ # When playing numbers, Adhearsion assumes you're saying the number, not the digits. For example, play("100")
187
+ # is pronounced as "one hundred" instead of "one zero zero".
188
+ #
189
+ # @param [Numeric, String] Numeric or String containing a valid Numeric, like "321".
137
190
  #
138
- # @private
191
+ # @raises [ArgumentError] if the given argument can not be played
192
+ # @returns [Punchblock::Component::Output]
139
193
  #
140
- def interruptible_play!(*outputs)
141
- result = nil
142
- outputs.each do |output|
143
- result = stream_file output
144
- break unless result.nil?
145
- end
146
- result
194
+ def play_numeric!(number)
195
+ raise ArgumentError unless number.kind_of?(Numeric) || number =~ /^\d+$/
196
+ async_player.play_ssml Formatter.ssml_for_numeric(number)
147
197
  end
148
198
 
149
199
  #
@@ -161,122 +211,54 @@ module Adhearsion
161
211
  # @param [Hash] Additional options.
162
212
  #
163
213
  # @return [String, nil] The single DTMF character entered by the user, or nil if nothing was entered
214
+ # @raises [PlaybackError] if (one of) the given argument(s) could not be played
164
215
  #
165
216
  def interruptible_play(*outputs)
166
- result = nil
167
- outputs.each do |output|
168
- begin
169
- result = interruptible_play! output
170
- rescue PlaybackError => e
171
- # Ignore this exception and play the next output
172
- logger.error "Error playing back the prompt: #{e.message}"
173
- ensure
174
- break if result
175
- end
176
- end
177
- result
178
- end
179
-
180
- # @private
181
- def detect_type(output)
182
- result = nil
183
- result = :time if [Date, Time, DateTime].include? output.class
184
- result = :numeric if output.kind_of?(Numeric) || output =~ /^\d+$/
185
- result = :audio if !result && (/^\//.match(output.to_s) || URI::regexp.match(output.to_s))
186
- result ||= :text
187
- end
188
-
189
- # @private
190
- def play_ssml_for(*args)
191
- play_ssml ssml_for(args)
192
- end
193
-
194
- #
195
- # Generates SSML for the argument and options passed, using automatic detection
196
- # Directly returns the argument if it is already an SSML document
197
- #
198
- # @param [String, Hash, RubySpeech::SSML::Speak] the argument with options as accepted by the play_ methods, or an SSML document
199
- # @return [RubySpeech::SSML::Speak] an SSML document
200
- #
201
- # @private
202
- #
203
- def ssml_for(*args)
204
- return args[0] if args.size == 1 && args[0].is_a?(RubySpeech::SSML::Speak)
205
- argument, options = args.flatten
206
- options ||= {}
207
- type = detect_type argument
208
- send "ssml_for_#{type}", argument, options
209
- end
210
-
211
- # @private
212
- def ssml_for_text(argument, options = {})
213
- RubySpeech::SSML.draw { argument }
214
- end
215
-
216
- # @private
217
- def ssml_for_time(argument, options = {})
218
- interpretation = case argument
219
- when Date then 'date'
220
- when Time then 'time'
221
- end
222
-
223
- format = options.delete :format
224
- strftime = options.delete :strftime
225
-
226
- time_to_say = strftime ? argument.strftime(strftime) : argument.to_s
227
-
228
- RubySpeech::SSML.draw do
229
- say_as(:interpret_as => interpretation, :format => format) { time_to_say }
230
- end
231
- end
232
-
233
- # @private
234
- def ssml_for_numeric(argument, options = {})
235
- RubySpeech::SSML.draw do
236
- say_as(:interpret_as => 'cardinal') { argument.to_s }
237
- end
238
- end
239
-
240
- # @private
241
- def ssml_for_audio(argument, options = {})
242
- fallback = (options || {}).delete :fallback
243
- RubySpeech::SSML.draw do
244
- audio(:src => argument) { fallback }
217
+ outputs.find do |output|
218
+ digit = stream_file output
219
+ return digit if digit
245
220
  end
246
221
  end
247
222
 
248
223
  #
249
224
  # Plays a single output, not only files, accepting interruption by one of the digits specified
250
- # Currently still stops execution, will be fixed soon in Punchblock
251
225
  #
252
226
  # @param [Object] String or Hash specifying output and options
253
227
  # @param [String] String with the digits that are allowed to interrupt output
254
228
  #
255
229
  # @return [String, nil] The pressed digit, or nil if nothing was pressed
230
+ # @private
256
231
  #
257
232
  def stream_file(argument, digits = '0123456789#*')
258
233
  result = nil
259
- ssml = ssml_for argument
260
- output_component = ::Punchblock::Component::Output.new :ssml => ssml.to_s
261
- input_stopper_component = ::Punchblock::Component::Input.new :mode => :dtmf,
234
+ stopper = Punchblock::Component::Input.new :mode => :dtmf,
262
235
  :grammar => {
263
- :value => grammar_accept(digits).to_s
236
+ :value => grammar_accept(digits)
264
237
  }
265
- input_stopper_component.register_event_handler ::Punchblock::Event::Complete do |event|
266
- output_component.stop! unless output_component.complete?
267
- end
268
- write_and_await_response input_stopper_component
269
- begin
270
- execute_component_and_await_completion output_component
271
- rescue ::Punchblock::ProtocolError => e
272
- raise PlaybackError, "Output failed for argument #{argument.inspect} due to #{e.inspect}"
238
+
239
+ player.output Formatter.ssml_for(argument) do |output_component|
240
+ stopper.register_event_handler Punchblock::Event::Complete do |event|
241
+ output_component.stop! unless output_component.complete?
242
+ end
243
+ write_and_await_response stopper
273
244
  end
274
- input_stopper_component.stop! if input_stopper_component.executing?
275
- reason = input_stopper_component.complete_event.reason
245
+
246
+ stopper.stop! if stopper.executing?
247
+ reason = stopper.complete_event.reason
276
248
  result = reason.interpretation if reason.respond_to? :interpretation
277
249
  return parse_single_dtmf result unless result.nil?
278
250
  result
279
251
  end
252
+
253
+ # @private
254
+ def player
255
+ @player ||= Player.new(self)
256
+ end
257
+
258
+ # @private
259
+ def async_player
260
+ @async_player ||= AsyncPlayer.new(self)
261
+ end
280
262
  end # Output
281
263
  end # CallController
282
264
  end # Adhearsion