Bridge Communications

Friday, May 29, 2015

Understanding UCMA Back to Back Calls Part III

This is the final part of this series on the Back2Back call class.  Last week we walked through setting a context and self transferring the call in order to get it in the proper state.  This week we'll continue our look as our ConferenceCallSession class.

Now that call has been self transfered, I want our tech support app to use conferences to handle the rest.  Let's start with the HandleSelfTransfer code.

        internal void HandleSelfTransfer(AudioVideoCall call)
        {
            // The incoming call will serve as the first call leg.
            AudioVideoCall incomingCallLeg = call;

            // Create a second call leg for the call into the conference.
            Conversation conferenceCallLegConversation =
                new Conversation(call.Conversation.Endpoint);

            // Use a fake SIP URI when joining the conference.
            //conferenceCallLegConversation.Impersonate("sip:support@bridgeoc.com", "sip:support@bridgeoc.com", "Bridge OC Support");

            string realcallNum = "";
            string realcallUri = "";
            string realcallName = "";

            try
            {
                realcallUri = incomingCallLeg.Conversation.RemoteParticipants[0].Uri;
                realcallName = incomingCallLeg.Conversation.RemoteParticipants[0].DisplayName;
                realcallNum = incomingCallLeg.Conversation.RemoteParticipants[0].PhoneUri;

                conferenceCallLegConversation.Impersonate(realcallUri, realcallNum, realcallName);

    
            }
            catch { }

            // Join the conference as a normal participant.
            ConferenceJoinOptions joinOptions = new ConferenceJoinOptions()
            {
                JoinMode = JoinMode.Default
            };

            ConferenceJoinOptions trustedOptions = new ConferenceJoinOptions()
            {
                JoinMode = JoinMode.TrustedParticipant
            };

            // Join the ad hoc conference, creating the SIP dialog
            // with the new conference focus.
            try
            {
                conferenceCallLegConversation.ConferenceSession.BeginJoin(
                    joinOptions,
                    ar =>
                    {
                        try
                        {
                            conferenceCallLegConversation.ConferenceSession.EndJoin(ar);

                            ConferenceRecorder crec = new ConferenceRecorder(conferenceCallLegConversation.ConferenceSession.ConferenceUri, _logger,  _appEndpoint, _agentSipUri);
                            crec.Start();

                            // Create an AudioVideoCall for the call to the A/V MCU.
                            AudioVideoCall conferenceCallLeg =
                                new AudioVideoCall(conferenceCallLegConversation);

                            InitiateBackToBackCall(incomingCallLeg, conferenceCallLeg);

                        }
                        catch (RealTimeException ex)
                        {
                            _logger.Log("Failed joining ad hoc conference.", ex);
                        }
                    },
                    null
                );
            }
            catch (InvalidOperationException ex)
            {
                _logger.Log("Failed joining ad hoc conference.", ex);
            }
        }

Let's look at the code for InitateBackToBackCall which does the impersonation of our support alias for the caller to see.

        private void InitiateBackToBackCall(AudioVideoCall incomingCallLeg, AudioVideoCall conferenceCallLeg)
        {
            // Create the settings for the back to back call.
            // There's no need to provide a SIP URI for the outgoing call
            // since the conversation is already associated with the conference.
            BackToBackCallSettings settings1 = new BackToBackCallSettings(incomingCallLeg);
            BackToBackCallSettings settings2 = new BackToBackCallSettings(conferenceCallLeg);

            incomingCallLeg.Conversation.Impersonate("sip:support@bridgeoc.com", "sip:support@bridgeoc.com", "Bridge OC Support");


            
            // Create and establish the back to back call.
            BackToBackCall b2bCall = new BackToBackCall(settings1, settings2);

            incomingCallLeg.StateChanged += conferenceCallLeg_StateChanged;
            conferenceCallLeg.StateChanged += conferenceCallLeg_StateChanged;

            try
            {
                _logger.Log("Establishing back to back call...");

                b2bCall.BeginEstablish(
                    ar =>
                    {
                        try
                        {
                            b2bCall.EndEstablish(ar);

                            _logger.Log("Established back to back call.");

                           
                            try
                            {
                                _logger.Log("Sending Conference Invites");


                                foreach (string u in _agentSipUri.Split(','))
                                {
                                    InviteToConf(conferenceCallLeg.Conversation, u);
                                }

                            }
                            catch { }

                        }
                        catch (RealTimeException ex)
                        {
                            _logger.Log("Failed establishing a back to back call.", ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ioex)
            {
                _logger.Log("Failed initiating a back to back call.", ioex);
            }

            Task.Factory.StartNew(() =>
            {
                Thread.Sleep(15000);
                allAttached = true;
            });
        }

In this example _agentSipUri is a List<string> object containing the URIs of all the users in the tech support group.  They all get invited which seems like overkill, but since the caller won't hear or see them join, they can easily silently drop if someone else is already helping the customer.  The other thing you no doubt say was the call to a ConferenceRecorder class.  We use this class to record the audio of the conference to local file.

Here is the full class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Rtc.Collaboration;
using Microsoft.Rtc.Signaling;
using Microsoft.Rtc.Collaboration.AudioVideo;
using System.Threading;

namespace B2B1
{
    internal class ConferenceRecorder
    {
        string _agentSipUri;
        Guid _guid = new Guid();
        Recorder _recorder = new Recorder();
        ApplicationEndpoint _appEndpoint;
        bool allAttached = false;
        string _conferenceURI;
        ILogger _logger;
        AudioVideoCall tAV;
        List<string> localParticipants = new List<string>();

        internal ConferenceRecorder(string confURI, ILogger logger, ApplicationEndpoint _apendpoint, string agenturi)
        {
            _conferenceURI = confURI;
            _logger = logger;
            _appEndpoint = _apendpoint;
            _agentSipUri = agenturi;
        }

        internal void Start()
        {
            _guid = Guid.NewGuid();
            _recorder.SetSink(new WmaFileSink(@"C:\temp\" + _guid.ToString() + ".wma"));

            ConferenceJoinOptions trustedOptions = new ConferenceJoinOptions()
            {
                JoinMode = JoinMode.TrustedParticipant
            };


            try
            {
                Conversation tConv = new Conversation(_appEndpoint);
                tAV = new AudioVideoCall(tConv);
                tConv.ConferenceSession.BeginJoin(_conferenceURI, trustedOptions, myt =>
                {
                    tConv.ConferenceSession.EndJoin(myt);

                    tConv.ConferenceSession.ParticipantEndpointAttendanceChanged += ConferenceSession_ParticipantEndpointAttendanceChanged;
                    tConv.ConferenceSession.StateChanged += ConferenceSession_StateChanged;

                    tAV.BeginEstablish(mytc =>
                    {
                        tAV.EndEstablish(mytc);

                        while (tAV.Flow == null)
                        {
                            Thread.Sleep(10);
                        }

                        _recorder.AttachFlow(tAV.Flow);
                        _recorder.Start();
                        _logger.Log("Attaching Recorder");


                    }, null);
                }, null);
            }
            catch { }
        }

        void ConferenceSession_StateChanged(object sender, StateChangedEventArgs<ConferenceSessionState> e)
        {
            _logger.Log("Conf State was " + e.PreviousState.ToString() + ", Now " + e.State.ToString());
        }

        void ConferenceSession_ParticipantEndpointAttendanceChanged(object sender, ParticipantEndpointAttendanceChangedEventArgs<ConferenceParticipantEndpointProperties> e)
        {
            _logger.Log("Conference Join or Leave");

            try
            {
                foreach (KeyValuePair<ParticipantEndpoint, ConferenceParticipantEndpointProperties> pair in e.Joined)
                {
                    localParticipants.Add(pair.Key.Participant.Uri);

                    if (_agentSipUri.ToUpper().Contains(pair.Key.Participant.Uri.ToUpper()))
                    {
                        allAttached = true;
                    }

                    _logger.Log(string.Format("{0} joined the conference.", pair.Key.Participant.Uri));
                }
            }
            catch { }

            try
            {
                foreach (KeyValuePair<ParticipantEndpoint, ConferenceParticipantEndpointProperties> pair in e.Left)
                {
                    localParticipants.Remove(pair.Key.Participant.Uri);

                    _logger.Log(string.Format("{0} left the conference.", pair.Key.Participant.Uri));

                    if (localParticipants.Count == 1 && !_agentSipUri.ToUpper().Contains(localParticipants[0].ToUpper()))
                    {
                        _logger.Log("Stopping Recorder");
                        _recorder.Stop();
                        _recorder.RemoveSink();

                        tAV.BeginTerminate(myterm =>
                        {
                            tAV.EndTerminate(myterm);
                        }, null);

                    }
                }
            }
            catch { }

        }

    }
}

That will end my 3 part series showing you how to use the back2back class in conjunction with conferences and recording to make a nice mini tech support call center application in UCMA.


Doug Routledge, C# Lync, Skype for Business, SQL, Exchange, UC Developer  BridgeOC
Twitter - @droutledge @ndbridge



8 comments:

  1. Doug,

    firs of all thaks for all your knowledge sharing. Is there way to record Lync clients ins ilent mode for inbound and outbpund calls "a la Cisco" with something like Ciscos built-in-bridge? something that could be done in a centralized way without giving the end uder the option to drop the recording process?

    Regards,

    José Sanchez

    ReplyDelete
  2. If you think of your UCMA app as the sip "built-in-bridge" you can. A good place to start is MSPL, this script language let's you control sip routing, and get all incoming calls to your app, where you can record them and conference in the original called user. It is written by Michael Greenlee who also has a book on the subject that goes into great detail. http://blog.greenl.ee/2011/07/08/extending-lync-server-routing-with-mspl-part-1/

    ReplyDelete
    Replies
    1. Doug,

      thanks for the advice!

      Regards,

      Jose

      Delete
  3. Can you show an example of recording every call, IM and conference. What I am looking for is a UMCA app that takes every single call External, internal, Peer to peer and records them to a file and also gives the ability to listen in if required.

    ReplyDelete
  4. Can you show an example of recording every call, IM and conference. What I am looking for is a UMCA app that takes every single call External, internal, Peer to peer and records them to a file and also gives the ability to listen in if required.

    ReplyDelete
    Replies
    1. IMs can all be saved to sql in the archiving settings, they go typically in the same lcsCDR database as the call history. If you want to record every single call, you need to redirect everything to your app with an MSPL script, and then you are in charge of all call flow, and have to get the calls to right people. An example of this would be quite complicated, basically 1/2 a fully functional contact center application. A good place to start would be to read this book. https://www.amazon.com/dp/0470939036/ref=cm_sw_r_cp_dp_T1_DFJuzb2MRNY73

      Delete
    2. Thanks i have now enabled archiving for the IM and have all IMs recorded to the database. im still stuck with how ucma works. how do you direct all calls to UCMA? we have 3 skype FE servers and 3 Edge servers and 3 Mediation servers. we also have a trust app server with UCMA installed. We have created a trusted application end point.

      Delete
    3. I have now figured out how to direct calls to UCMA using mspl script. all I need now is to understand how to create the UCMA part.

      Delete

Any spam comments will be deleted and your user account will be disabled.