Hi @jcolp,
Here is what I’m trying to do:
I’m creating a telephony app which is using AsterNET.ARI to interact with Asterisk v.16. Basically my app will receive incoming calls, pass it to Asterisk dial plan which I described above (Stasis), then our ARI app will dial out multiple numbers depends on our logic (one to many). Here is the step:
- Accept incoming channels
- Create new bridge then add the incoming channel to the bridge
- Create outgoing channels from the ARI app
- Add outgoing channels to the bridge then dial out those channels at the same time.
If one of those outgoing channels answers the call, I will receive the DIALSTATUS=ANSWER
in the OnDialEvent and I use this value to hangup the other channels. This scenario is working perfectly. Now the problem is if one of those outgoing channels declines the call, others will be hangup as well because the DIALSTATUS I receive in the OnDialEvent is always ANSWER
. Here is the JSON I got when one of the outgoing channels declined the call:
{"Caller":null,"Peer":{"Id":"1550921467.57","Name":"SIP/symbio-00000021","State":"Up","Caller":{"Name":"","Number":""},"Connected":{"Name":"carsales","Number":"61434466477"},"Accountcode":"","Dialplan":{"Context":"csntelephony","Exten":"","Priority":1},"Creationtime":"2019-02-23T22:31:07.237+11:00","Language":"en"},"Forward":"","Forwarded":null,"Dialstring":"","Dialstatus":"ANSWER","Application":"app","Timestamp":"2019-02-23T22:31:11.551+11:00","Type":"Dial"}
I’ve also checked the OnChannelHangupEvent but could not find anything that could help determine how the far end rejects the call. See below JSONs:
{"Cause":0,"Soft":false,"Channel":{"Id":"1550921458.56","Name":"SIP/125.213.160.7-00000020","State":"Up","Caller":{"Name":"carsales","Number":"61434466477"},"Connected":{"Name":"","Number":""},"Accountcode":"","Dialplan":{"Context":"ari-test","Exten":"61370182002","Priority":4},"Creationtime":"2019-02-23T22:30:58.111+11:00","Language":"en"},"Application":"app","Timestamp":"2019-02-23T22:31:37.754+11:00","Type":"ChannelHangupRequest"}
{"Cause":16,"Soft":true,"Channel":{"Id":"1550921458.56","Name":"SIP/125.213.160.7-00000020","State":"Up","Caller":{"Name":"carsales","Number":"61434466477"},"Connected":{"Name":"","Number":""},"Accountcode":"","Dialplan":{"Context":"ari-test","Exten":"61370182002","Priority":4},"Creationtime":"2019-02-23T22:30:58.111+11:00","Language":"en"},"Application":"app","Timestamp":"2019-02-23T22:31:37.755+11:00","Type":"ChannelHangupRequest"}
{"Cause":32,"Soft":true,"Channel":{"Id":"1550921467.57","Name":"SIP/symbio-00000021","State":"Up","Caller":{"Name":"","Number":""},"Connected":{"Name":"","Number":""},"Accountcode":"","Dialplan":{"Context":"csntelephony","Exten":"","Priority":1},"Creationtime":"2019-02-23T22:31:07.237+11:00","Language":"en"},"Application":"app","Timestamp":"2019-02-23T22:31:37.906+11:00","Type":"ChannelHangupRequest"}
ChannelId 1550921458.56
is incoming channel, and channelId 1550921467.57
is outgoing channel that declined the call.
Here is from the Asterisk CLI console:
*** System restart required ***
Last login: Fri Feb 22 10:34:49 2019 from 10.1.56.185
ubuntu@ip-10-224-73-184:~$ sudo asterisk -rvvvvvvv
Asterisk 16.1.1, Copyright (C) 1999 - 2018, Digium, Inc. and others.
Created by Mark Spencer <markster@digium.com>
Asterisk comes with ABSOLUTELY NO WARRANTY; type 'core show warranty' for detail s.
This is free software, with components licensed under the GNU General Public
License version 2 and other licenses; you are welcome to redistribute it under
certain conditions. Type 'core show license' for details.
=========================================================================
Connected to Asterisk 16.1.1 currently running on ip-10-224-73-184 (pid = 15603)
[Feb 23 23:00:13] ERROR[30958]: res_http_websocket.c:525 ws_safe_read: Error rea ding from web socket: Connection reset by peer
[Feb 23 23:00:13] WARNING[30958]: ari/ari_websockets.c:126 ast_ari_websocket_ses sion_read: WebSocket read error: Connection reset by peer
Deactivating Stasis app 'app'
== WebSocket connection from '10.3.100.1:65501' forcefully closed due to fatal write error
Activating Stasis app 'app'
== WebSocket connection from '10.3.100.1:49856' for protocol '' accepted using version '13'
== Using SIP RTP CoS mark 5
> 0x7f6a4000eda0 -- Strict RTP learning after remote address set to: 125. 213.160.11:47332
-- Executing [61370182002@csntelephony:1] Set("SIP/125.213.160.7-00000022", "agipath=VerifyCallerAndConnectAgi") in new stack
-- Executing [61370182002@csntelephony:2] Set("SIP/125.213.160.7-00000022", "CHANNEL(userfield)=61370182002_61434466477_1550923240.60") in new stack
-- Executing [61370182002@csntelephony:3] GotoIf("SIP/125.213.160.7-00000022 ", "0?cctester-legb,s,1") in new stack
-- Executing [61370182002@csntelephony:4] GotoIf("SIP/125.213.160.7-00000022 ", "0?cctester-legb,s,1") in new stack
-- Executing [61370182002@csntelephony:5] GotoIf("SIP/125.213.160.7-00000022 ", "0?cctester-legb,s,1") in new stack
-- Executing [61370182002@csntelephony:6] Goto("SIP/125.213.160.7-00000022", "ari-test,61370182002,start") in new stack
-- Goto (ari-test,61370182002,1)
-- Executing [61370182002@ari-test:1] Verbose("SIP/125.213.160.7-00000022", "1, Inbound call: From 61434466477 to 61370182002|<sip:61370182002@13.55.19.101; user=phone>") in new stack
Inbound call: From 61434466477 to 61370182002|<sip:61370182002@13.55.19.101;us er=phone>
-- Executing [61370182002@ari-test:2] Wait("SIP/125.213.160.7-00000022", "1" ) in new stack
-- Executing [61370182002@ari-test:3] Set("SIP/125.213.160.7-00000022", "CAL LERID(name)=carsales") in new stack
-- Executing [61370182002@ari-test:4] Stasis("SIP/125.213.160.7-00000022", " app,61434466477,61370182002,<sip:61370182002@13.55.19.101;user=phone>") in new s tack
-- Channel SIP/125.213.160.7-00000022 joined 'simple_bridge' stasis-bridge < b45ea910-7628-438b-9124-00162a90ba34>
> 0x7f6a4000eda0 -- Strict RTP switching to RTP target address 125.213.16 0.11:47332 as source
-- <SIP/125.213.160.7-00000022> Playing 'agent-pass.gsm' (language 'en')
> 0x7f6a4000eda0 -- Strict RTP learning complete - Locking on source addr ess 125.213.160.11:47332
[Feb 23 23:00:46] WARNING[4940][C-00000014]: app.c:938 dtmf_stream: Illegal DTMF character 't' in string. (0-9*#aAbBcCdD allowed)
[Feb 23 23:00:46] WARNING[4940][C-00000014]: app.c:938 dtmf_stream: Illegal DTMF character 'm' in string. (0-9*#aAbBcCdD allowed)
-- <SIP/125.213.160.7-00000022> Playing 'priv-introsaved.gsm' (language 'en' )
== Using SIP RTP CoS mark 5
-- Channel SIP/symbio-00000023 joined 'simple_bridge' stasis-bridge <b45ea91 0-7628-438b-9124-00162a90ba34>
> 0x7f6a540058c0 -- Strict RTP learning after remote address set to: 125. 213.162.242:13814
> 0x7f6a540058c0 -- Strict RTP switching to RTP target address 125.213.16 2.242:13814 as source
> 0x7f6a540058c0 -- Strict RTP learning complete - Locking on source address 125.213.162.242:13814
-- Channel SIP/symbio-00000023 left 'simple_bridge' stasis-bridge <b45ea910-7628-438b-9124-00162a90ba34>
-- Channel SIP/symbio-00000023 joined 'simple_bridge' stasis-bridge <b45ea910-7628-438b-9124-00162a90ba34>
-- Channel Recorder/ARI-0000000d;2 joined 'simple_bridge' stasis-bridge <b45ea910-7628-438b-9124-00162a90ba34>
> Bridge b45ea910-7628-438b-9124-00162a90ba34: switching from simple_bridge technology to softmix
-- x=0, open writing: /var/spool/asterisk/recording/7bd3b80d-dc25-412e-92a2-497e7e281c8c format: WAV, 0x7f6a58012ed0
-- Channel SIP/125.213.160.7-00000022 left 'softmix' stasis-bridge <b45ea910-7628-438b-9124-00162a90ba34>
> Bridge b45ea910-7628-438b-9124-00162a90ba34: switching from softmix technology to simple_bridge
-- Message ended by control
-- Channel Recorder/ARI-0000000d;2 left 'simple_bridge' stasis-bridge <b45ea910-7628-438b-9124-00162a90ba34>
-- Channel SIP/symbio-00000023 left 'simple_bridge' stasis-bridge <b45ea910-7628-438b-9124-00162a90ba34>
ip-10-224-73-184*CLI>
Again my question is how do I know get correct DIALSTATUS when the far end declines the call. Below is my code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using AsterNET.ARI;
using AsterNET.ARI.Models;
using Newtonsoft.Json;
using SimpleTestApplication.Models;
using SimpleTestApplication.Services;
namespace SimpleTestApplication
{
public class AriCallManager
{
private readonly AriClient _ariClient;
private readonly ICallContextService _callContextService;
private readonly IAsteriskSettings _asteriskSettings;
private readonly List<CallContext> _contextCalls = new List<CallContext>();
public AriCallManager(AriClient ariClient, ICallContextService callContextService, IAsteriskSettings asteriskSettings)
{
_ariClient = ariClient;
_callContextService = callContextService;
_asteriskSettings = asteriskSettings;
}
public void Start()
{
_ariClient.OnStasisStartEvent += OnStasisStartEvent;
_ariClient.OnDialEvent += OnDialEvent;
_ariClient.OnChannelStateChangeEvent += OnChannelStateChangeEvent;
_ariClient.OnChannelHangupRequestEvent += OnChannelHangupRequestEvent;
_ariClient.OnChannelDtmfReceivedEvent += OnChannelDtmfReceivedEvent;
_ariClient.Connect();
}
private void OnChannelDtmfReceivedEvent(IAriClient sender, ChannelDtmfReceivedEvent e)
{
_callContextService.HandleDtmf(sender, e);
}
private void OnChannelHangupRequestEvent(IAriClient sender, ChannelHangupRequestEvent e)
{
Console.WriteLine($"OnChannelHangupRequestEvent: {JsonConvert.SerializeObject(e)}");
_callContextService.Hangup(sender,e);
}
private void OnDialEvent(IAriClient sender, DialEvent e)
{
Console.WriteLine($"OnDialEvent: {JsonConvert.SerializeObject(e)}");
_callContextService.HandleMultipleDialing(sender, e);
}
private void OnStasisStartEvent(IAriClient sender, StasisStartEvent e)
{
_callContextService.AcceptIncomingCall(sender, e);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using AsterNET.ARI;
using AsterNET.ARI.Models;
using Newtonsoft.Json;
using SimpleTestApplication.Ioc;
using SimpleTestApplication.Models;
namespace SimpleTestApplication.Services
{
class CallContextService : ICallContextService
{
private readonly IAsteriskSettings _asteriskSettings;
private readonly IDialService _dialService;
private readonly INumberRouteService _numberRouteService;
private readonly IHangupService _hangupService;
private readonly IAriCallFactory _ariCallFactory;
private readonly IRecordingService _recordingService;
private readonly IDtmfService _dtmfService;
public CallContextService(IAsteriskSettings asteriskSettings,
IDialService dialService, INumberRouteService numberRouteService, IHangupService hangupService, IAriCallFactory ariCallFactory,
IRecordingService recordingService, IDtmfService dtmfService)
{
_asteriskSettings = asteriskSettings;
_dialService = dialService;
_numberRouteService = numberRouteService;
_hangupService = hangupService;
_ariCallFactory = ariCallFactory;
_recordingService = recordingService;
_dtmfService = dtmfService;
}
public void AcceptIncomingCall(IAriClient ariClient, StasisStartEvent e)
{
var ariCallManager = _ariCallFactory.Instance(ariClient);
if (e.Channel.State == "Ring")
{
if (ariCallManager.GetContextCallByChannelId(e.Channel.Id) == null)
{
var bridgeId = Guid.NewGuid().ToString();
var contextChannel = new ChannelContext()
{
Id = e.Channel.Id,
CallerId = e.Channel.Caller.Number,
CallerName = e.Channel.Caller.Name,
Role = "Caller",
Variables = new Dictionary<string, string>()
{
{"BridgeId", bridgeId},
{"Role", "Caller"},
{"CallerId", e.Channel.Caller.Number},
{"CallerName", e.Channel.Caller.Name}
},
VirtualNumber = e.Channel.Dialplan.Exten,
State = e.Channel.State
};
var contextCall = new CallContext()
{
Bridge = new BridgeContext()
{
Id = bridgeId,
Channels = new List<ChannelContext>()
{
contextChannel
}
}
};
ariCallManager.AddContextCall(contextCall);
ariClient.Channels.Answer(e.Channel.Id);
contextChannel.State = e.Channel.State;
contextChannel.StartCallDuration();
ariClient.Bridges.Create("mixing, dtmf_events, proxy_media", bridgeId, _asteriskSettings.StasisAppName());
ariClient.Bridges.AddChannel(bridgeId,e.Channel.Id);
ariClient.Channels.Play(e.Channel.Id, "sound:agent-pass");
ariClient.Channels.SendDTMF(e.Channel.Id, "dtmf");
Console.WriteLine($"ChannelContext: {JsonConvert.SerializeObject(contextChannel)}");
}
}
}
public void Hangup(IAriClient ariClient, ChannelHangupRequestEvent e)
{
_hangupService.Hangup(ariClient, e.Channel.Id);
}
public void HandleMultipleDialing(IAriClient ariClient, DialEvent e)
{
if (e.Dialstatus == "ANSWER")
{
var role = ariClient.Channels.GetChannelVar(e.Peer.Id, "Role");
var bridge = ariClient.Channels.GetChannelVar(e.Peer.Id, "BridgeId");
if (role.Value == "Callee")
{
ariClient.Channels.Answer(e.Peer.Id);
ariClient.Bridges.AddChannel(bridge.Value, e.Peer.Id);
}
_dialService.HandleMultipleDialing(ariClient, e);
_recordingService.StartRecording(ariClient, e);
}
}
public void HandleDtmf(IAriClient ariClient, ChannelDtmfReceivedEvent e)
{
var ariCallManager = _ariCallFactory.Instance(ariClient);
var dtmfResult = _dtmfService.HandleDtmf(ariClient, e.Channel.Id, e.Digit);
if (dtmfResult == DtmfEnum.Invalid)
{
ariClient.Channels.Play(e.Channel.Id, "sound:auth-incorrect");
}
else if (dtmfResult == DtmfEnum.Valid)
{
ariClient.Channels.Play(e.Channel.Id, "sound:priv-introsaved");
var contextCall = ariCallManager.GetContextCallByChannelId(e.Channel.Id);
_dialService.Dial(ariClient, _numberRouteService.GetDestinationNumbers(e.Channel.Dialplan.Exten), contextCall);
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using AsterNET.ARI;
using AsterNET.ARI.Models;
using Newtonsoft.Json;
using SimpleTestApplication.Ioc;
using SimpleTestApplication.Models;
namespace SimpleTestApplication.Services
{
class DialService : IDialService
{
private readonly IAsteriskSettings _asteriskSettings;
private readonly IHangupService _hangupService;
private readonly IAriCallFactory _ariCallFactory;
public DialService(IAsteriskSettings asteriskSettings, IHangupService hangupService, IAriCallFactory ariCallFactory)
{
_asteriskSettings = asteriskSettings;
_hangupService = hangupService;
_ariCallFactory = ariCallFactory;
}
public void Dial(IAriClient ariClient, List<string> numbers, CallContext callContext)
{
var callerChan = callContext.Bridge.Channels.FirstOrDefault(x => x.Role == "Caller");
if (callerChan == null)
{
throw new ArgumentException("Invalid context call: caller/originator channel not found.");
}
foreach (var number in numbers)
{
if (!string.IsNullOrEmpty(number))
{
var chan = ariClient.Channels.Create($"SIP/symbio/{number}", _asteriskSettings.StasisAppName(), null, null, null, callerChan.Id);
ariClient.Channels.SetChannelVar(chan.Id, "BridgeId", callContext.Bridge.Id);
ariClient.Channels.SetChannelVar(chan.Id, "Role", "Callee");
ariClient.Channels.SetChannelVar(chan.Id, "CallerId", callerChan.CallerId);
ariClient.Channels.SetChannelVar(chan.Id, "CallerName", callerChan.CallerName);
ariClient.Channels.SetChannelVar(chan.Id, "CalleeId", number);
ariClient.Bridges.AddChannel(callContext.Bridge.Id, chan.Id);
ariClient.Channels.Dial(chan.Id, callerChan.CallerName, 30);
callContext.Bridge.Channels.Add(new ChannelContext()
{
Id = chan.Id,
Role = "Callee",
Variables = new Dictionary<string, string>()
{
{"BridgeId", callContext.Bridge.Id},
{"Role", "Callee"},
{"CalleeId", number},
{"CallerId", callerChan.CallerId},
{"CallerName", callerChan.CallerName}
},
State = chan.State
});
}
}
}
public void HandleMultipleDialing(IAriClient ariClient, DialEvent e)
{
var ariCallManager = _ariCallFactory.Instance(ariClient);
var ctx = ariCallManager.GetContextCallByChannelId(e.Peer.Id);
if (ctx != null)
{
Console.WriteLine($"HandleMultipleDialing: {JsonConvert.SerializeObject(e)}");
var calleeChan = ctx.Bridge.Channels.FirstOrDefault(x => x.Id == e.Peer.Id);
if (calleeChan != null)
{
calleeChan.State = e.Peer.State;
}
Console.WriteLine($"Callee Chan: {JsonConvert.SerializeObject(calleeChan)}");
var hangUpChannels = new List<string>();
foreach (var chan in ctx.Bridge.Channels)
{
if (chan.Role == "Callee" && chan.Id != e.Peer.Id)
{
hangUpChannels.Add(chan.Id);
}
}
foreach (var hangUpChannel in hangUpChannels)
{
_hangupService.Hangup(ariClient, hangUpChannel);
}
}
}
}
}