danbarua / NEventSocket

A reactive FreeSwitch eventsocket library for Modern .Net
Mozilla Public License 2.0
74 stars 37 forks source link

multiple concurrent Inbound calls #29

Closed senpark15 closed 8 years ago

senpark15 commented 8 years ago

I am developing IVR application. I want to make multiple concurrent calls through gsm gateway. should i create multiple inbound socket instances to make concurrent calls or can i make concurrent calls through single socket instance and play audio files based on call id? could you please give any sample code?

Here is my sample code for make one call at time

using (var socket = await InboundSocket.Connect("localhost", 8021, "ClueCon"))
            {
                await socket.SubscribeEvents(EventName.ChannelHangupComplete, EventName.ChannelAnswer, EventName.Dtmf);

                List<String> mobileNos = new List<String>();
                mobileNos.Add("8940703144");
                mobileNos.Add("8754006462");

                foreach (var item in mobileNos)
                {
                    var originate = await socket.Originate("sofia/internal/" + item + "@192.168.1.10",
                    new OriginateOptions
                    {
                        CallerIdName = "HSS",
                        IgnoreEarlyMedia = true
                    });

                    if (originate.Success)
                    {
                        var uuid = originate.ChannelData.UUID;
                        var playOptions = new PlayGetDigitsOptions()
                        {
                            MinDigits = 1,
                            MaxDigits = 1,
                            MaxTries = 3,
                            TimeoutMs = 6000,
                            PromptAudioFile = IvrMenuFilePath,
                            ValidDigits = "1,2,3"
                        };
                        playOptions.BadInputAudioFile = InvalidPhraseFilePath;
                        var playGetDigitsResult = await socket.PlayGetDigits(uuid, playOptions);
                    }
                }
            }
danbarua commented 8 years ago

Yep, you can use one inbound socket to control many calls - once subscribed you will receive events for all channels on the switch.

danbarua commented 8 years ago

I've had a think about this and here's one way you can do it. I've seen a pattern where you originate a call via an inbound socket and then hand off to an outbound socket listener to do further processing when the call has been answered.

The .Originate() method wraps up the underlying events and completes the returned Task<OriginateResult> when the call has been answered. You probably don't want to do this as you want to originate calls as quickly as the InboundSocket/FreeSwitch can handle it rather than blocking and waiting for the result of the call, so we'll drop down a level to the BgApi() method and then hand off to our outbound listener. We'll handle failure scenarios in the Inbound Socket code:

using (var socket = await InboundSocket.Connect("localhost", 8021, "ClueCon"))
{
  await socket.SubscribeEvents(EventName.BackgroundJob, EventName.ChannelHangup);

  List<String> mobileNos = new List<String>({"8940703144", "8754006462"});
  Dictionary<String, HangupCause> failures = new List<String>();

  //we'll listen out for hangups that aren't NORMAL_CLEARING
  //this will be for calls that return rejections, busy codes, failures

  socket.Events.Where(x => x.EventName == EventName.ChannelHangup
                                       && x.HangupCause != HangupCause.NormalClearing)
                         .Subscribe(x =>
       {
      //i'm not sure which part of the event message will contain the info you want
      //it will be in the headers or the variables
      var dialledNumber = x.GetVariable("variable_dialed_user");
      failures.Add(dialledNumber, x.HangupCause);
      //log and deal with failure as appropriate
      }

  foreach(var item in mobileNos)
  {
      var bgJobResult = await socket.BackgroundJob(
                                            "originate "
                                            + originateOptions.ToString()
                                            + "sofia/internal/"
                                            + item 
                                            +"@123.456.789 &socket(127.0.0.1:8084 async)");

      if (!bgJobResult.Success)
      {
        //BgApi returns immediately if unable to dial, eg, incorrectly formatted dial string
        //Log and handle errors as appropriate
      }
  }

Note that we originated a call and then handed over to the application socket(127.0.0.1:8084 async) You can use an outbound listener to handle calls that were successfully answered. This way, the outbound listener part of the code onlu deals with calls that have been successfully set up and the inbound socket part of the code is only concerned with setting up calls, and will do so as fast as it can pump messages into FreeSwitch.

If you are doing any bridging at this point, you will need to use socket(127.0.0.1:8084 async full). A side effect of doing this is that you will receive events on the outbound socket for ALL channels in FreeSwitch, like an inbound socket. In order to reduce traffic, and remove the need to filter for specific events in your application code, you can have FreeSwitch do the filtering server-side.

await outboundSocket.Filter(HeaderNames.UniqueId, outboundSocket.ChannelData.UUID).ConfigureAwait(false); //filter for our unique id (in case using full socket mode)
await outboundSocket.Filter(HeaderNames.OtherLegUniqueId, outboundSocket.ChannelData.UUID).ConfigureAwait(false); //filter for channels bridging to our unique id
await outboundSocket.Filter(HeaderNames.ChannelCallUniqueId, outboundSocket.ChannelData.UUID); //filter for channels bridging to our unique id

This is how the Channel API works. Alternatively you could just use the Channel API which handles much of the complexity for you.

Alternatively, if you write your call handling logic in an XML dialplan originate destination &transfer(extension XML default), you can simply originate calls from the inbound socket and transfer to an extension in the dialplan which will handle your call logic - this will give you the best performance but you will be giving up the ability to write your call logic in C#.

danbarua commented 8 years ago

..and I've just fixed a bug in .BackgroundJob() eff8552150076a41895f41390a600fcf9d6536e8 if you are running off a fork please update to latest.

senpark15 commented 8 years ago

thank you very much for your response. I will try your suggestions and come back if i have any problem

senpark15 commented 8 years ago

I got latest code. I tried you suggestions. here is the code sample.

class Program
    {
        public static int _currentCallCount = 0;
        static void Main(string[] args)
        {
            Task.Run(() => { CheckCallCount(); });
            Task.Run(() => { MakeCall(); });

            using (var listener = new OutboundListener(8084))
            {
                listener.Connections.Subscribe(
                     async socket =>
                     {
                         await socket.Connect();

                         var uuid = socket.ChannelData.Headers[HeaderNames.UniqueId];
                         Console.WriteLine("OutboundSocket connected for channel " + uuid);

                         await socket.Play(uuid, "misc/8000/misc-learn_more_about_freeswitch_solutions.wav");
                         await socket.Play(uuid, "misc/8000/misc-freeswitch_is_state_of_the_art.wav");
                         await socket.Hangup(uuid, HangupCause.NormalClearing);
                     });

                listener.Start();

                Console.WriteLine("Press [Enter] to exit.");
                Console.ReadLine();
            }

            Console.ReadLine();
        }

        private static async Task CheckCallCount()
        {
            using (var socket = await InboundSocket.Connect("localhost", 8021, "ClueCon"))
            {
                while (true)
                {
                    var res = await socket.SendApi("show calls count");
                    Console.WriteLine("Current Calls Count " + Convert.ToInt32(res.BodyText.Split(' ')[0]));
                    _currentCallCount = Convert.ToInt32(res.BodyText.Split(' ')[0]);
                    Thread.Sleep(2000);
                }
            }
        }

        private static async Task MakeCall()
        {
            using (var socket = await InboundSocket.Connect("localhost", 8021, "ClueCon"))
            {
                await socket.SubscribeEvents(EventName.ChannelHangup);

                socket.Events.Where(x => x.EventName == EventName.ChannelHangup
                               && x.HangupCause != HangupCause.NormalClearing)
                 .Subscribe(x =>
                 {
                     Console.WriteLine("Hangup Detected : " + x.GetVariable("mobile_no"));
                 });

                List<String> mobileNos = null;

                while (true)
                {
                    // only four concurrent calls allowed
                    while (_currentCallCount == 4)
                    {
                        Thread.Sleep(5000);
                    }

                    // mobile no comes from database
                    mobileNos = new List<string>() { "8940703114", "8754006482" };
                    foreach (var mobileNo in mobileNos)
                    {
                        Console.WriteLine("Call initiating : " + mobileNo);

                        var originateOptions = new OriginateOptions
                        {
                            CallerIdName = "874561",
                            IgnoreEarlyMedia = true

                        };

                        var bgJobResult = await socket.BackgroundJob(
                                    "originate "
                                    + originateOptions.ToString()
                                    + "sofia/internal/" + mobileNo + "@192.168.1.250 &socket(127.0.0.1:8084 async)");

                        if (!bgJobResult.Success)
                        {
                            Console.WriteLine("Call Failed to initiate : " + mobileNo);
                        }
                        else
                        {
                            Console.WriteLine("Call Successfully initiated : " + mobileNo);
                        }
                    }
                    Thread.Sleep(5000);
                }
            }
        }
    }

here are the issues.

  1. background job originate command waited until call attend. once i attend the call in mobile, bgJobResult.Success is returned false, but call connected and transferred to outbound socket. bgJobResult.ErrorMessage is USER_BUSY.
  2. Once call connected, OutboundListener play the first audio file and stay there. it neither play next audio file or hangup the call. call connected forever. If i disconnect call from mobile , it throws task cancelled exception.

please let me know what i am missing or doing wrong.

danbarua commented 8 years ago

The BackgroundJob will only complete immediately if FreeSwitch cannot process the origination command - this will happen due to bad inputs/programming error. Otherwise it will await until the call is answered or fails.

OutboundListener play the first audio file and stay there

This is because we did not get a CHANNEL_EXECUTE_COMPLETE event.. the reason for this is below:

We need to wrap the origination application in single quotes: '&socket(127.0.0.1:8084 async full)' If it is not wrapped in quotes FreeSwitch parses it as socket 127.0.0.1:8084 and misses out the arguments async full. I needed to use async full because FS won't let me subscribe to events without it, which is annoying.

If i disconnect call from mobile , it throws task cancelled exception.

Yes this is expected behaviour - as mentioned in README you need to try { } catch(OperationCanceledException) { } inside your Outbound listener subscription to handle the scenario where the caller hangs up in the middle of execution.

danbarua commented 8 years ago

I will surface command and api errors as errors in logging and also adjust the originate command.

danbarua commented 8 years ago

Please see sample code in https://github.com/danbarua/NEventSocket/commit/8e43b3b9db98c63e67d5cc762a30abe792f1c204

danbarua commented 8 years ago

@senpark15 I have left some example code here: https://github.com/danbarua/NEventSocket/blob/master/src/NEventSocket.Examples/Examples/VoiceBlaster.cs

You might want to review the logic around rate-limiting etc. Please re-open this issue and continue the discussion if you have any problems.