Skip to content
benlangfeld edited this page Mar 5, 2012 · 13 revisions

Asterisk Rayo Extension

rayo-asterisk extends the Rayo specification and has its base namespace at urn:xmpp:rayo:asterisk. It provides the means to access raw AGI and AMI transports via Rayo.

AMI

AMI actions and events are supported by way of the server's own AMI connections. Events are sent to all registered parties by way of an XMPP presence stanza:

<presence to="16577@app.rayo.net/1" from="call.rayo.net">
  <event xmlns="urn:xmpp:rayo:asterisk:ami:1" name="Newchannel">
    <attribute name="Channel" value="SIP/101-3f3f"/>
    <attribute name="State" value="Ring"/>
    <attribute name="Callerid" value="101"/>
    <attribute name="Uniqueid" value="1094154427.10"/>
  </event>
</presence>

Actions constitute a component for the purposes of the Rayo specification. This allows actions to have a complex lifecycle. One executes an action like so:

<iq id="1234" type="set" to="call.rayo.net" from="16577@app.rayo.net/1">
  <action xmlns="urn:xmpp:rayo:asterisk:ami:1" name="Originate">
    <param name="Channel" value="SIP/101test"/>
    <param name="Context" value="default"/>
    <param name="Exten" value="8135551212"/>
    <param name="Priority" value="1"/>
    <param name="Callerid" value="3125551212"/>
    <param name="Timeout" value="30000"/>
    <param name="Variable" value="var1=23|var2=24|var3=25"/>
    <param name="Async" value="1"/>
  </action>
</iq>

The server will respond with a unique ID for the component like so:

<iq id='1234' type='result' to='16577@app.rayo.net/1' from='call.rayo.net'>
  <ref id='fgh4590' xmlns='urn:xmpp:rayo:1'/>
</iq>

If the action raises events (is a 'causal action') then these will be received from the component like so:

<presence from="call.rayo.net/fgh4590" to="16577@app.rayo.net/1">
  <event xmlns="urn:xmpp:rayo:asterisk:ami:1" name="OriginateResponse">
    <attribute name="Exten" value="8135551212"/>
    <attribute name="CallerID" value="3125551212"/>
    ...
  </event>
</presence>

Once the event finishes executing (returns a response), you will receive a complete event like so:

<presence from="call.rayo.net/fgh4590" to="16577@app.rayo.net/1">
  <complete xmlns="urn:xmpp:rayo:ext:1">
    <success xmlns="urn:xmpp:rayo:asterisk:ami:complete:1">
      <message>Originate successfully queued</message>
    </success>
  </complete>
</presence>

AGI

AGI actions may be executed on a Rayo call like so:

<iq id="1234" type="set" to="abc123@call.rayo.net/1" from="16577@app.rayo.net/1">
  <command xmlns="urn:xmpp:rayo:asterisk:agi:1" name="GET VARIABLE">
    <param value="UNIQUEID"/>
  </command>
</iq>

The server will respond with a unique ID for the component like so:

<iq id='1234' type='result' to='16577@app.rayo.net/1' from='abc123@call.rayo.net/1'>
  <ref id='fgh4590' xmlns='urn:xmpp:rayo:1'/>
</iq>

You will then receive a complete event indicating the result of the command's execution:

<presence from="abc123@call.rayo.net/fgh4590" to="16577@app.rayo.net/1">
  <complete xmlns="urn:xmpp:rayo:ext:1">
    <success xmlns="urn:xmpp:rayo:asterisk:agi:complete:1">
      <code>200</code>
      <result>0</result>
      <data>1187188485.0</data>
    </success>
  </complete>
</presence>

Mapping Rayo to Asterisk AsyncAGI + AMI

Events

Offer

An incoming call to Asterisk will result in an event similar to this:

Event: AsyncAGI
Privilege: agi,all
SubEvent: Start
Channel: SIP/501-081f0730
Env: agi_request:%20async%0aagi_channel:%20SIP/501-081f0730%0aagi_language:%20es%0aagi_type:%20SIP%0
aagi_uniqueid:%20edialer-sercom-1238148792.62%0aagi_callerid:%20unknown%0aagi_calleridname:%20unknow
n%0aagi_callingpres:%200%0aagi_callingani2:%200%0aagi_callington:%200%0aagi_callingtns:%200%0aagi_dn
id:%20unknown%0aagi_rdnis:%20unknown%0aagi_context:%20sip_sercom%0aagi_extension:%20801%0aagi_priori
ty:%202%0aagi_enhanced:%200.0%0aagi_accountcode:%20%0a%0a

Here, a call actor should be created, assigned an ID, and have its SIP headers fetched (using SIP_HEADER()). These headers should include:

  • Max-Forwards
  • Content-Length
  • Contact
  • To
  • CSeq
  • Via
  • Call-ID
  • Content-Type
  • From

The AGI environment variables should be added to the SIP headers as 'x-agi_request', etc.

End

A Hangup event will be received via AMI in this form:

Event: Hangup
Privilege: call,all
Channel: SIP/501-081f0730
Uniqueid: edialer-sercom-1238148792.62
Cause: 16
Cause-txt: Normal Clearing

The corresponding call actor should be looked up by channel ID and be sent this message. The reason element should be established from the "Cause" attribute, with a mapping like so:

  • 0, 16 -> <hangup/>
  • 18,102 -> <timeout/>
  • 17 -> <busy/>
  • 19,21,22 -> <reject/>
  • 1,2,3,6,7,27,28,29,30,31,34,38,41,42,43,44,45,50,52,54,57,58,65,66,69,81,88,95,96,97,98,99,100,101,103,111,127 -> <error/>

Errors should have the error code set like so: <error code="127"/>

Ringing

Ringing state is established via an AMI event like so, where the state is "Ringing":

Event: Newstate
Privilege: call,all
Channel: SIP/501-081f0730
State: Ringing
CallerID: <unknown>
CallerIDName: <unknown>
Uniqueid: edialer-sercom-1238148792.62

This should result in a <ringing/> event being sent to the Rayo client.

Answered

Answered status is established via an AMI event like so, where the state is "Up":

Event: Newstate
Privilege: call,all
Channel: SIP/501-081f0730
State: Up
CallerID: <unknown>
CallerIDName: <unknown>
Uniqueid: edialer-sercom-1238148792.62

This should result in an <answered/> event being sent to the Rayo client.

Joined

Joined events should be sent to both calls whenever a bridge is confirmed as created.

Unjoined

On receipt of an 'Unlink' AMI event (eg below), both calls should receive an unjoined event.

Event: Unlink
Channel1: SIP/1234-00000007
Channel2: SIP/5678-00000006
Uniqueid1: 1319717537.11
Uniqueid2: 1319717537.10
CallerID1: 5678
CallerID2: 5678

Commands

Accept

In order to accept a call (send SIP 183 Session Progress), the translator should turn an <accept/> command into the following AMI dialog:

Action: AGI
Channel: SIP/501-081f0730
Command: EXEC PROGRESS

Response: Success
Message: Added AGI command to queue

Event: AsyncAGI
Privilege: agi,all
SubEvent: End
Channel: SIP/501-081f0730

Answer

In order to answer a call (send SIP 200 OK), the translator should turn an <answer/> command into the following AMI dialog:

Action: AGI
Channel: SIP/501-081f0730
Command: EXEC ANSWER

Response: Success
Message: Added AGI command to queue

Event: AsyncAGI
Privilege: agi,all
SubEvent: End
Channel: SIP/501-081f0730

Hangup

In order to hangup a call, the translator should turn an <answer/> command into the following AMI dialog:

Action: Hangup
Channel: SIP/501-081f0730

Event: Hangup
Channel: SIP/501-081f0730
Uniqueid: 1124989110.20474
Cause: 16

Response: Success
Message: Channel Hungup

Reject

In order to reject a call, the translator should turn a <reject/> command into the following AMI dialog:

The reject reason (<busy/>, <decline/> or <error/>) should be mapped to

Redirect

In order to redirect a call, the translator should turn a <redirect/> command into the following AMI dialog:

Action: Redirect
Channel: SIP/501-081f0730
Exten: 8600029
Context: default
Priority: 1

Response: Success
Message: Channel Redirected

Where 'Exten' matches the URI provided in the 'to' attribute of <redirect/>.

Mute / Unmute

Muting a channel appears not to be possible with Asterisk without a MOH related hack.

Join

Joining two calls must be done by channel ID as below:

Action: Bridge
Channel1: SIP/5678-00000006
Channel2: SIP/1234-00000007

Response: Success
Message: Launched bridge thread with success

Event: BridgeAction
Privilege: call,all
Response: Success
Channel1: SIP/5678-00000006
Channel2: SIP/1234-00000007

Event: Bridge
Privilege: call,all
Bridgestate: Link
Bridgetype: core
Channel1: SIP/1234-00000007
Channel2: SIP/5678-00000006
Uniqueid1: 1319717537.11
Uniqueid2: 1319717537.10
CallerID1: 5678
CallerID2: 5678

The BridgeAction event confirms the join for the joining party (Channel1), while the Bridge event confirms for the joined party (Channel2). Each of these events should trigger the respective <joined/> event for each call.

Unjoin

In order to unjoin calls, an extension should first be added (via AMI's Command action and the 'add extension' CLI command) which will throw the calls back to AsyncAGI. The channels should then be redirected to this extension to get back control:

Action: Redirect
Channel: SIP/1234-6378
ExtraChannel: SIP/4321-45cf6c80
Exten: 680
Priority: 1
Context: default

Event: Newchannel
Channel: AsyncGoto/SIP/1234-6378
State: Up
CallerID:
Uniqueid: 1124983197.19239

Event: Rename
Oldname: SIP/1234-6378
Newname: SIP/1234-6378
Uniqueid: 1124982513.19184

Event: Rename
Oldname: AsyncGoto/SIP/1234-6378
Newname: SIP/1234-6378
Uniqueid: 1124983197.19239

Event: Rename
Oldname: SIP/1234-6378
Newname: AsyncGoto/SIP/1234-6378
Uniqueid: 1124982513.19184

Event: Newexten
Channel: SIP/1234-6378
Context: default
Extension: 680
Priority: 1
Application: SetVar
AppData: channel=SIP/1234
Uniqueid: 1124983197.19239

Response: Success
Message: Dual Redirect successful

A new AGIAsync Start event will be received, but this time it should trigger an unjoined event rather than an Offer.

Dial

A <dial/> command should result in the creation of a new call actor, which is assigned an ID to which a <ref/> should be returned. The call should then attempt an origination via AMI like so:

Action: Originate
Async: true
Application: AGI
Data: agi:async
Channel: sip/501
CallerID: 

Response: Success
Message: Originate successfully queued

At this point in time, it appears that a best effort attempt to match ringinging/answered status (Newchannel, Newcallerid, Newstate events) channels to the channel specified to Originate must be made until the OriginateResponse event is received for confirmation against the ActionID

Components

Output

A typical Rayo output command might look like this:

<output xmlns='urn:xmpp:rayo:output:1' 
      interrupt-on='any|dtmf|speech|none'
      start-offset='2000'
      start-paused='false'
      repeat-interval='2000'
      repeat-times='10'
      max-time='30000'
      voice='allison'>
  <audio src='http://acme.com/greeting.mp3'>
    Thanks for calling ACME company
  </audio>
  <audio src='http://acme.com/package-shipped.mp3'>
    Your package was shipped on
  </audio>
  <say-as interpret-as='date'>12/01/2011</say-as>
</output>

TTS Engine

Where available (and configured), an attempt should be made to pass the entire SSML document to a TTS engine via MRCPSynth(). The attributes of <output/> should be used like so:

  • interrupt-on: A value of 'speech' should return an error indicating a lack of support for ASR. Values of 'any' or 'dtmf' should set the 'i' option to MRCPSynth to 'any'
  • start-offset: Unsupported. Return an error if set.
  • start-paused: If set to true, simply do not begin the component executing until a resume command is received.
  • repeat-interval: Unsupported. Return an error if set.
  • repeat-times: Unsupported. Return an error if set.
  • max-time: Unsupported. Return an error if set.
  • voice: pass-through as the 'v' option to MRCPSynth

Once MRCPSynth returns, a Rayo complete success event should be sent.

Asterisk native

If no TTS engine is available, we need to make a best attempt to parse the document and use Asterisk's native media output functions. SSML elements should be executed as follows in sequence:

  • Text - return an error indicating a lack of support
  • <audio/> - play using PLAYBACK
  • <say-as interpret-as='date'/> - play using SAYUNIXTIME (Adhearsion::VoIP::Asterisk::Commands#play_time)
  • <say-as interpret-as='cardinal'/> - play using SAYNUMBER (Adhearsion::VoIP::Asterisk::Commands#play_numeric)

Once the whole document has been processed and played correctly, a Rayo complete success event should be sent.

Actions

  • Stop - unsupported
  • Pause - unsupported
  • Resume - supported in order to allow 'start-paused'
  • Seek - unsupported
  • SpeedUp - unsupported
  • SpeedDown - unsupported
  • VolumeUp - unsupported
  • VolumeDown - unsupported

If the call hangs up during the execution of the component, the compoonent should send a Rayo complete hangup event.

Input

A typical Rayo input command might look something like this:

<input xmlns='urn:xmpp:rayo:input:1'
    mode='any|dtmf|speech'
    terminator='#'
    recognizer='en-US'
    initial-timeout='2000'
    inter-digit-timeout='2000'
    sensitivity='0.5'
    min-confidence='0.5'>
  <grammar content-type='application/grammar+grxml'>
    !CDATA[[
      <?xml version="1.0"?>
      <grammar mode="dtmf" version="1.0" 
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
               xsi:schemaLocation="http://www.w3.org/2001/06/grammar 
                                   http://www.w3.org/TR/speech-grammar/grammar.xsd"
               xmlns="http://www.w3.org/2001/06/grammar">

      <rule id="digit">
       <one-of>
         <item> 0 </item>
         <item> 1 </item>
         <item> 2 </item>
         <item> 3 </item>
         <item> 4 </item>
         <item> 5 </item>
         <item> 6 </item>
         <item> 7 </item>
         <item> 8 </item>
         <item> 9 </item>
       </one-of>
      </rule>

      <rule id="pin" scope="public">
       <one-of>
         <item>
           <item repeat="4"><ruleref uri="#digit"/></item>
           #
         </item>
         <item>
           * 9
         </item>
       </one-of>
      </rule>

      </grammar>
    ]]
  </grammar>
</input>

This is a GRXML based DTMF grammar. Speech grammars will be supported by way of the generic speech recognition API provided by asteriskUniMRCP, where available (and configured). DTMF grammars will utilise asteriskUniMRCP if possible, or fall back to manual matching of DTMF events.

ASR Engine

Typical execution of a speech recognition routine in extensions.conf would look like this:

exten => s,1,Answer()
exten => s,2,SpeechCreate()
exten => s,3,SpeechLoadGrammar(digit,/usr/local/unimrcp/data/grammar.xml)
exten => s,4,SpeechActivateGrammar(digit)
exten => s,5,SpeechBackground(hello-world,20)
exten => s,6,GotoIf($["${SPEECH(results)}" = "0"]?7:9)
exten => s,7,Playback(vm-nonumber)
exten => s,8,Goto(5)
exten => s,9,Verbose(1,The recognized input is ${SPEECH_TEXT(0)})
exten => s,10,Verbose(1,The score is ${SPEECH_SCORE(0)})
exten => s,11,Verbose(1,The matched grammar is ${SPEECH_GRAMMAR(0)})
exten => s,12,SpeechDeactivateGrammar(digit)
exten => s,13,SpeechUnloadGrammar(digit)
exten => s,14,SpeechDestroy()
exten => s,15,Hangup()

This requires many features typical of the media engine to be implemented in the AGI application. An alternative is to use MRCPRecog(), which is synchronous, and has the following options:

p - profile to use in mrcp.conf
i - digits to allow the ASR to be interrupted with (set to none for DTMF grammars to allow DTMF to be sent to the MRCP server, otherwise if "any" or other digits specified, ASR will be interrupted and the digit will be returned in dialplan)
f - filename on play (if empty or not specified, no file is played)
t - Recognition timeout in milliseconds
b - bargein value (no barge-in=0, ASR engine barge-in=1, Asterisk barge-in=2)
ct - confidence threshold (0.0 - 1.0)
sl - sensitivity level (0.0 - 1.0)
sva - speed vs accuracy (0.0 - 1.0)
nb - n-best list length
nit - no input timeout
sit - start input timers (true/false)
sct - speech complete timeout
sint - speech incomplete timeout
dit - dtmf interdigit timeout
dtt - dtmf terminate timout
dttc - dtmf terminate characters
sw - save waveform (true/false)
nac - new audio channel (true/false)
spl - speech language (en-US/en-GB/etc.)
rm - recognition mode
hmaxd - hotword max duration
hmind - hotword min duration
cdb - clear dtmf buffer (true/false)
enm - early no match (true/false)
iwu - input waveform URI
mt - media type

Asterisk native

If no ASR engine is available, it will be necessary to use a DTMF recognition engine in Ruby which will listen for AMI DTMF events, and must implement timeouts and a terminator, while the recognizer, sensitivity and min-confidence attributes may be safely ignored.

Actions

  • Stop - supported

If the call hangs up during the execution of the component, the compoonent should send a Rayo complete hangup event.

Record

A complete Rayo record command might look like this:

<record xmlns='urn:xmpp:rayo:record:1' 
    format='mp3'
    start-beep='true'
    start-paused='false'
    max-duration='500000'
    initial-timeout='10000|none'
    final-timeout='30000|none'>
    <!-- Optional format-specific encoding hints -->
    <hint name="quality" value="4" />
    <hint name="double-pass" value="true" />
</record>

The options here are Record() and MixMonitor(), where the former is blocking. If using MixMonitor, timeouts and a start beep must be implemented in the component.

Actions

  • Pause - supported
  • Resume - supported to enable 'start-paused'
  • Stop - supported

Conference

Conferencing is likely to be replaced by the lower level Rayo mixer API.