Export (0) Print
Expand All
Expand Minimize

Reproducing Complex PBX Features Using Lync 2010 Custom Audio Routing

Summary:   This article describes how to use Microsoft Unified Communications Managed API (UCMA) 3.0 to implement advanced Private Branch Exchange (PBX) features.

Applies to:   Microsoft Unified Communications Managed API (UCMA) 3.0 | Microsoft Unified Communications Managed API (UCMA) 3.0 Core SDK | Microsoft Lync Server 2010

Published:   October 2011 | Provided by:   Michael Greenlee, Clarity Consulting, Inc. | About the Author

Contents

With the release of Microsoft Lync Server 2010, which includes various features that historically have been the purview of a traditional PBX, there are significant technological and cost advantages for organizations that disband their legacy PBX system completely and replace it with the Microsoft unified communications platform.

A PBX, or private branch exchange, is a telephone exchange that is operated by a business or organization for internal use, instead of by a telephone company for general use. Until recently, PBX systems have been typically used by organizations that grow beyond a certain size. Systems vary in capacity and functionality, and a larger PBX often has a wide selection of features that can be used for advanced call handling, or for contact center and help desk calls.

Lync Server 2010 includes many features that are typical in PBX systems. Even better, almost any specialized feature which a PBX might offer can be built for Lync Server using the SDKs that Microsoft has made freely available for Microsoft Lync 2010 custom development. These SDKs let developers create applications which customize Lync 2010 using managed code in familiar .NET languages

This article describes how to use Microsoft Unified Communications Managed API (UCMA) 3.0 to implement advanced PBX features with a specific example: silent monitoring by supervisors.

This article assumes that you have a basic familiarity with UCMA 3.0 and understand how to write an application that can register an endpoint with Lync Server and answer calls. For more information about UCMA development, see Additional Resources.

In addition, before you use the sample code in this article, you should provision a trusted application pool, trusted application, and trusted application endpoint with the Lync Server Management Shell. For more information about the provisioning process, see Additional Resources. Also, the Microsoft Unified Communications Managed API (UCMA) 3.0 Core SDK must be installed in your development environment. To install the SDK, go to http://www.microsoft.com/download/en/details.aspx?id=10566.

With the Microsoft Lync Server 2010 conferencing features that you can schedule a conference for a specific time with a predefined list of participants. However, because Microsoft Lync 2010 conferences can also be created impromptu, when they are needed, organizations that deploy Lync often find that it becomes very convenient for people to arrange small, multi-person conversations without advance notice.

At the same time, Lync conferences are very useful for managing calls that require special services such as recording, silent monitoring, and billable time tracking. In these cases, a server application can provide the services by joining as a third participant. Furthermore, if other people have to join the conversation later, they can be brought in without any more changes to how the call is managed.

In order for a UCMA application to turn a call into a conference in this manner, however, it must be involved from the beginning. All of the messages that are passed back and forth between the two call participants in setting up the call must be routed through the UCMA application. This is because there is no way for a UCMA application to “intervene” in a two-party call where it is not a participant.

The best way around this problem is to ensure that calls go to the application first. This is easiest in an environment like a call center or help desk where people call a single number without knowing which individual they will be connected with.

On receiving a call, the application can create a new impromptu conference and connect the caller with the conference using a BackToBackCall object. The BackToBackCall class creates a kind of proxy arrangement where the application relays Session Initiation Protocol (SIP) messages from the caller to the conference. These messages control the initiation and the state of the call. Meanwhile, media goes directly from the caller into the conference.

In Figure 1, the back to back call is made up of two individual calls, which are the call “legs.” The call between the application and the original caller is called the “front end leg,” while the call between the application and the conference is the “back end leg.”

Figure 1. BackToBackCall class proxy arrangement

BackToBackCall class proxy arrangement

Besides giving the application more control, this setup also disguises the conference from the caller, because from the caller’s perspective he or she is still communicating directly with the application.

NoteNote

For more information about the BackToBackCall class, see Additional Resources.

The first step in creating almost any UCMA application is to create and establish a collaboration platform and one or more endpoints. For more information, see Unified Communications Managed API 3.0 Core SDK Documentation.

After establishing an endpoint, the following example shows how a UCMA application that has to handle calls can register an event handler for incoming calls.

// Register to be notified of incoming calls
_endpoint.RegisterForIncomingCall<AudioVideoCall>(
    OnAudioVideoCallReceived);

The kind of call is specified as a type parameter, and the event handler delegate is provided as the first parameter for the method.

Next, you must create the event handler itself. The next code example shows an application handling an incoming call, setting up an impromptu conference, and calling another method to set up the back to back call. The Call object is stored in an instance variable called _frontEndCallLeg. To create the impromptu conference, the application merely calls the ConferenceSession.BeginJoin method without specifying a conference URI. UCMA handles the creation of the conference.

There are some points to note here. First, notice that the application creates a new Conversation object that is used to join the conference, instead of joining the conference using the incoming call. This second Conversation object will be for the back-end call leg.

Also, the application uses the Impersonate method on the new Conversation object to impersonate the SIP URI of the original caller. If the application did not do this, other people in the conference would see the application as a participant instead of the person who called.

internal void HandleIncomingCall()
{
    Console.WriteLine("Incoming call.");

    // Create a new conversation for the back-end leg
    // of the B2B call (which will connect to the conference).
    LocalEndpoint localEndpoint = 
        _frontEndCallLeg.Conversation.Endpoint;
    Conversation backEndConversation = 
        new Conversation(localEndpoint);

    // Impersonate the caller so that the caller, rather than
    // the application, will appear to be participating in
    // the conference.
    string callerSipUri = 
        _frontEndCallLeg.RemoteEndpoint.Participant.Uri;
    backEndConversation.Impersonate(callerSipUri, null, null);

    try
    {
        // Join the conference.
        backEndConversation.ConferenceSession.BeginJoin(
            default(ConferenceJoinOptions), 
            joinAsyncResult => {
                try 
                {
                    backEndConversation.ConferenceSession.EndJoin(
                        joinAsyncResult);
                    Console.WriteLine("Joined conference.");

                    _backEndCallLeg = new AudioVideoCall(backEndConversation);

                    CreateBackToBack();
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



The code for the CreateBackToBack method appears in the next example. It takes the front-end call leg (the original incoming call) and the back-end call leg (just created by the application) and establishes a back to back call using the two legs.

private void CreateBackToBack()
{
    // Create a back to back call between the caller
    // and the conference. This is so the caller
    // will not see that he/she is connected to a 
    // conference.
    BackToBackCallSettings frontEndCallLegSettings = 
        new BackToBackCallSettings(_frontEndCallLeg);
    BackToBackCallSettings backEndCallLegSettings = 
        new BackToBackCallSettings(_backEndCallLeg);

    _b2bCall = new BackToBackCall(frontEndCallLegSettings, 
        backEndCallLegSettings);

    try
    {
        // Establish the back to back call.
        _b2bCall.BeginEstablish(
            establishAsyncResult =>
            {
                try
                {
                    _b2bCall.EndEstablish(
                        establishAsyncResult);
                    Console.WriteLine("Back to back call established.");

                    InviteRecipientToConference();
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}


Once the back to back call is created, the application brings into the conference the person who is meant to receive the call. Depending on the context, this might be a contact center agent, a help desk staff member, or some other individual.

The InviteRecipientToConference method appears in the next example.

private void InviteRecipientToConference()
{
    ConferenceInvitation invitation = 
        new ConferenceInvitation(_backEndCallLeg.Conversation);

    try
    {
        // Invite the recipient to the conference
        // using a conference invitation.
        invitation.BeginDeliver(
            _recipientSipUri,
            deliverAsyncResult =>
            {
                try
                {
                    invitation.EndDeliver(deliverAsyncResult);

                    Console.WriteLine("Invited recipient to the conference.");

                    InviteSupervisor();
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



Once these operations are complete, you will have a call that resembles a perfectly ordinary two-party call to the caller, but is managed as a conference on the back end. This arrangement will make it very easy to track, record, or manipulate the call for the PBX-like behaviors described earlier.

With a call that is connected to a conference via BackToBackCall, as described earlier, it is fairly easy to implement a silent monitoring feature which achieves the following tasks.

  • Invite a supervisor to the call.

  • Allow the supervisor, when he or she has joined, to hear all audio on the call.

  • Prevent any other participants from hearing the supervisor.

  • Avoid showing the supervisor in the conference roster.

In order to manipulate the routing of audio so that the supervisor can hear without being heard, you can create manual routes for the audio/video MCU. To these routes and hide the supervisor in the roster, you should have the supervisor join the conference as a trusted participant. Finally, for your application to complete each of these things (creating manual audio routes and bringing in the supervisor as a trusted participant), the supervisor must be connected to the conference through a second BackToBackCall object. This section describes the steps that are required.

Invite the Supervisor

In a fully functional monitoring application, it would presumably be possible for the supervisor to choose to join any active call by using a graphical user interface. In this example, for simplicity, the supervisor is invited to join shortly after the call is connected.

The method that invites the supervisor into the conference appears in the next code example. It does not use the ConferenceInvitation class. Instead, it places a new outbound call to the supervisor. This is because the goal is to connect the supervisor to the conference using BackToBackCall. Using a ConferenceInvitation object to invite the supervisor would result in a call from the supervisor to the conference that would be outside the control of your UCMA application

private void InviteSupervisor()
{
    LocalEndpoint localEndpoint = 
        _backEndCallLeg.Conversation.Endpoint;

    // Create a new audio call to call the supervisor.
    Conversation supervisorConversation = 
        new Conversation(localEndpoint);
    AudioVideoCall supervisorCall = 
        new AudioVideoCall(supervisorConversation);

    try
    {
        // Place an outbound call to the supervisor.
        supervisorCall.BeginEstablish(_supervisorSipUri, 
            default(CallEstablishOptions),
            establishAsyncResult =>
        {
            try
            {
                supervisorCall.EndEstablish(establishAsyncResult);

                // Wait for a couple of seconds
                // before transferring.
                Thread.Sleep(2000);

                SelfTransferSupervisorCall(supervisorCall);
            }
            catch (RealTimeException ex)
            {
                Console.WriteLine(ex);
            }
        },
        null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



Connect the Supervisor to the Conference

To set up a back to back call, you start with the following scenario:

  • An incoming call that has not yet been accepted.

  • An outgoing call that has not yet been established.

At this stage, however, this scenario is present:

  • An outgoing call that has already been established (to the supervisor).

  • An outgoing call that has not yet been established (to the conference).

So, you must transform the call to the supervisor into an incoming call in order to have the correct ingredients for a back to back call. The easiest way to do this is to use the self-transfer method. The following example shows the self-transfer code.

avCall.BeginTransfer(avCall, OnTransferCompleted, null);

The call that you are transferring is also the first parameter of the BeginTransfer method. The result of this operation is that the remote party (in this case, the supervisor) places a new call to the application, with additional information in the message that contains the ID of the call that it is replacing. The application can examine this bit of context to link the new incoming call back to the one that it placed to the supervisor so that it knows what to do next.

To make it easy for the application to maintain continuity, the object that is managing this call puts a reference itself into the ApplicationContext property of the call before it performs the self-transfer. As you will see shortly, this property will be available when the new incoming call arrives at the application.

The following code shows how to self-transfer the call.

private void SelfTransferSupervisorCall(AudioVideoCall supervisorCall)
{
    // Put this instance of SupervisorJoinCallSession
    // into the context property for retrieval when
    // the application receives the self-transferred call.
    supervisorCall.Conversation.ApplicationContext =
        this;

    try
    {
        // Perform a self-transfer.
        supervisorCall.BeginTransfer(
            supervisorCall,
            transferAsyncResult =>
            {
                try
                {
                    supervisorCall.EndTransfer(transferAsyncResult);
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}


Because this call will now arrive at the application as a new incoming call, the application must differentiate between incoming calls that are from self-transfers and ordinary incoming calls from a person. The incoming call event handler in the next example does this by looking at the CallToBeReplaced property on the event arguments. If this property is null, the incoming call is an ordinary incoming call.

private void OnAudioVideoCallReceived(object sender, 
    CallReceivedEventArgs<AudioVideoCall> e)
{
    if (e.CallToBeReplaced == null)
    {
        // Initial call (not a self-transfer).
        SupervisorJoinCallSession session =
            new SupervisorJoinCallSession(e.Call, 
                _recipientSipUri, _supervisorSipUri);
        session.HandleIncomingCall();
    }
    else
    {
        // Self-transfer for a supervisor join. Get
        // the session object out of the ApplicationContext
        // property on the replaced call (where it
        // was positioned before doing the self-transfer).
        SupervisorJoinCallSession session = 
            e.CallToBeReplaced.Conversation.ApplicationContext 
            as SupervisorJoinCallSession;
        session.HandleIncomingSelfTransfer(e.Call);
    }
}
 


If the call is a self-transfer, the application retrieves the session object that is managing the call by looking at the ApplicationContext property from the old call, and turns control back over to that session object. The following code example shows the method that is executed next. It creates a back-end call leg for the new back to back call, joins the back-end call leg to the conference, and, finally, creates the back to back call. Notice that the code uses an instance of ConferenceJoinOptions to specify that this should be a trusted conference join.

internal void HandleIncomingSelfTransfer(AudioVideoCall call)
{
    string conferenceUri = _backEndCallLeg.Conversation.ConferenceSession.ConferenceUri;

    // Create a new conversation for the
    // back-end call leg.
    LocalEndpoint localEndpoint = 
        _backEndCallLeg.Conversation.Endpoint;
    Conversation conferenceConversation = 
        new Conversation(localEndpoint);

    // Prepare to join the conference as a trusted
    // participant so as to be allowed to
    // manipulate audio routing.
    ConferenceJoinOptions joinOptions = 
        new ConferenceJoinOptions();
    joinOptions.JoinMode = JoinMode.TrustedParticipant;

    try
    {
        // Join the conference.
        conferenceConversation.ConferenceSession.BeginJoin(conferenceUri,
            joinOptions, joinAsyncResult =>
            {
                try
                {
                    conferenceConversation.ConferenceSession.EndJoin(joinAsyncResult);

                    CreateSupervisorB2BCall(call, conferenceConversation);
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



Before actually establishing the back to back call, you must specify one more option that is called RemoveFromDefaultRouting, which is a property on AudioVideoCallEstablishOptions.AudioVideoMcuDialInOptions. When this instance of AudioVideoCallEstablishOptions is associated with the back-end call leg, it causes the supervisor to be kept out of the default audio mix when it joins the conference. To have any audio flow between the supervisor and the other conference participants, you must set up manual audio routes.

NoteNote

It is also possible to remove a participant from the default audio mix after that participant has joined the conference. You can do this with the AudioVideoMcuSession.BeginRemoveFromDefaultRouting method, as described later in this article. However, the approach shown here is simpler when the participant is meant to be kept out of the default mix from the beginning, and it removes the risk that other participants will be able to hear the new participant’s audio for a brief period.

The following code shows how the back to back call is created and established.

private void CreateSupervisorB2BCall(AudioVideoCall incomingCall, 
    Conversation conferenceConversation)
{
    // Create a new audio call on the back-end call leg.
    AudioVideoCall callToConference = 
        new AudioVideoCall(conferenceConversation);

    // Set up the call to be automatically removed
    // from the default audio mix in the A/V MCU.
    AudioVideoCallEstablishOptions establishOptions = 
        new AudioVideoCallEstablishOptions();
    establishOptions.AudioVideoMcuDialInOptions.
        RemoveFromDefaultRouting = true;

    // Create a back to back call between the supervisor
    // and the conference.
    BackToBackCallSettings frontEndB2BSettings = 
        new BackToBackCallSettings(incomingCall);
    BackToBackCallSettings backEndB2BSettings = 
        new BackToBackCallSettings(callToConference);

    backEndB2BSettings.CallEstablishOptions = establishOptions;

    BackToBackCall supervisorB2BCall = 
        new BackToBackCall(frontEndB2BSettings, backEndB2BSettings);

    try
    {
        // Establish the B2B call.
        supervisorB2BCall.BeginEstablish(
            establishAsyncResult =>
            {
                try
                {
                    supervisorB2BCall.EndEstablish(establishAsyncResult);

                    CreateManualAudioRoutes(callToConference);
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



The final step is to create manual audio routes. The AudioVideoCall class has a property, AudioVideoMcuRouting, which holds an object that manages manual audio routing. The method to modify these manual audio routes is called AudioVideoMcuRouting.BeginUpdateAudioRoutes.

There are two kinds of audio routes: incoming routes and outgoing routes. From any given AudioVideoCall, you can only control audio going from the local participant of that call to another conference participant, or to the local participant of that call from another participant. Figure 2 shows this slightly confusing concept. So, the kind of route you use depends on the direction you want the audio to go.

Figure 2. Incoming and outgoing audio routes

Incoming and outgoing audio routes

The next code example shows how to create the audio routes that allow the supervisor to hear other participants but not be heard.

private void CreateManualAudioRoutes(AudioVideoCall callToConference)
{
    AudioVideoMcuSession avMcu = 
        _backEndCallLeg.Conversation.ConferenceSession.AudioVideoMcuSession;

    // Get the ParticipantEndpoint objects for the caller
    // and recipient.
    ParticipantEndpoint callerParticipantEndpoint = 
        avMcu.GetRemoteParticipantEndpoints().Single(p => 
            p.Participant.Uri == _backEndCallLeg.Conversation.Endpoint.OwnerUri);
    ParticipantEndpoint recipientParticipantEndpoint = 
        avMcu.GetRemoteParticipantEndpoints().Single(p => 
            p.Participant.Uri == _recipientSipUri);

    // Create incoming audio routes from the caller
    // and recipient to the supervisor.
    IncomingAudioRoute callerToSupervisorAudioRoute = 
        new IncomingAudioRoute(callerParticipantEndpoint);
    IncomingAudioRoute recipientToSupervisorAudioRoute = 
        new IncomingAudioRoute(recipientParticipantEndpoint);

    List<IncomingAudioRoute> routes = new List<IncomingAudioRoute>() { 
        callerToSupervisorAudioRoute, recipientToSupervisorAudioRoute };

    try
    {
        // Update the MCU audio routing with the new incoming routes.
        callToConference.AudioVideoMcuRouting.BeginUpdateAudioRoutes(
            null, routes,
            updateRoutesAsyncResult =>
            {
                try
                {
                    callToConference.AudioVideoMcuRouting.EndUpdateAudioRoutes(
                        updateRoutesAsyncResult);
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



The first step is to retrieve ParticipantEndpoint objects that represent the two remote conference participants whom the supervisor should be able to hear. In this case, those participants are identified by the SIP URI. Next, the application creates two IncomingAudioRoute objects: one to represent the flow of audio from the caller to the supervisor, and another to represent the flow of audio from the recipient to the supervisor. Figure 3 shows the resulting audio routes.

Figure 3. Audio flow from application to endpoints

Audio flow from application to endpoints

Finally, the application calls BeginUpdateAudioRoutes and passes in the two routes as a list. The first parameter, which is null in this case, would hold OutgoingAudioRoute objects if they are present.

For more information about the sample application code, see Code Examples.

The pattern discussed in the previous section is versatile and can be useful for other purposes, for example:

  • Consultative transfers.

  • Supervisor coaching, also known as “whisper mode.”

  • DTMF-enabled menus for a single participant.

To perform a consultative transfer, a help desk agent might want to be able to invite a supervisor to the call, have a brief conversation that the caller cannot hear, and then turn the call over to the supervisor.

The starting point, again, is a call that was connected to a conference on the back end through a back to back call. In this case, you should temporarily remove the caller from the default audio mix. This way, the caller will not hear the two other participants having their conversation, nor will they hear the caller.

The next code example shows how to remove the caller from the default audio mix by using the AudioVideoMcuSession.BeginRemoveFromDefaultRouting method.

private void RemoveCallerFromDefaultAudioMix()
{
    AudioVideoMcuSession avMcu =
        _backEndCallLeg.Conversation.ConferenceSession.AudioVideoMcuSession;

    // Get the ParticipantEndpoint object for the caller.
    ParticipantEndpoint callerParticipantEndpoint = 
        avMcu.GetRemoteParticipantEndpoints().Single(p => 
            p.Participant.Uri == _backEndCallLeg.Conversation.Endpoint.OwnerUri);

    try
    {
        // Remove the caller from the default audio mix.
        // He or she will not hear or be heard by any
        // other participants.
        avMcu.BeginRemoveFromDefaultRouting(callerParticipantEndpoint,
            removeAsyncResult =>
            {
                try
                {
                    avMcu.EndRemoveFromDefaultRouting(removeAsyncResult);
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



Once you do this, you can invite the supervisor to the conference by using an instance of ConversationInvitation. You can use a back to back call if you prefer, but there is no need for that in this case, because the supervisor can be visible and, after you remove the caller from the default audio mix, you do not have to do anything else with manual audio routes.

After the consultation has occurred, the next example shows how the caller can be added back into the default audio mix.

private void AddCallerToDefaultAudioMix()
{
    AudioVideoMcuSession avMcu =
        _backEndCallLeg.Conversation.ConferenceSession.AudioVideoMcuSession;

    // Get the ParticipantEndpoint object for the caller.
    ParticipantEndpoint callerParticipantEndpoint =
        avMcu.GetRemoteParticipantEndpoints().Single(p =>
            p.Participant.Uri == _backEndCallLeg.Conversation.Endpoint.OwnerUri);

    try
    {
        // Remove the caller from the default audio mix.
        // He or she will not hear or be heard by any
        // other participants.
        avMcu.BeginAddToDefaultRouting(callerParticipantEndpoint,
            addAsyncResult =>
            {
                try
                {
                    avMcu.EndAddToDefaultRouting(addAsyncResult);
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}



NoteNote

As you can see, the code for these two operations is similar. In an ordinary application, the code to get the ParticipantEndpoint object could be moved into a separate method to eliminate the duplicated code.

This section includes SupervisorJoinSample.cs and SupervisorJoinCallSession.cs.

Code Listing 1: SupervisorJoinSample.cs

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

namespace SupervisorJoin
{
    internal class SupervisorJoinSample
    {
        CollaborationPlatform _platform;
        ApplicationEndpoint _endpoint;

        string _applicationId;
        string _recipientSipUri;
        string _supervisorSipUri;

        ManualResetEvent _startupWaitHandle = 
            new ManualResetEvent(false);

        internal void Start()
        {
            _applicationId = ConfigurationManager.AppSettings["applicationId"];
            _recipientSipUri = ConfigurationManager.AppSettings["recipientSipUri"];
            _supervisorSipUri = ConfigurationManager.AppSettings["supervisorSipUri"];

            // Use auto-provisioning to load trusted application settings
            ProvisionedApplicationPlatformSettings platformSettings = 
                new ProvisionedApplicationPlatformSettings("SupervisorJoin",
                _applicationId);

            _platform = new CollaborationPlatform(platformSettings);

            try
            {
                // Register for information on trusted application endpoints
                _platform.RegisterForApplicationEndpointSettings(
                    OnApplicationEndpointDiscovered);

                // Start up the collaboration platform
                _platform.BeginStartup(
                    startupAsyncResult =>
                    {
                        try
                        {
                            _platform.EndStartup(startupAsyncResult);
                            Console.WriteLine("Platform started.");
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    }, null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        void OnApplicationEndpointDiscovered(object sender, 
            ApplicationEndpointSettingsDiscoveredEventArgs e)
        {
            _endpoint = new ApplicationEndpoint(_platform, 
                e.ApplicationEndpointSettings);

            Console.WriteLine("Endpoint {0} discovered.", 
                e.ApplicationEndpointSettings.OwnerUri);

            // Register to be notified of incoming calls
            _endpoint.RegisterForIncomingCall<AudioVideoCall>(
                OnAudioVideoCallReceived);

            // Establish the application endpoint
            _endpoint.BeginEstablish(establishAsyncResult =>
            {
                try
                {
                    _endpoint.EndEstablish(establishAsyncResult);
                    Console.WriteLine("Endpoint established.");

                    _startupWaitHandle.Set();
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
        }

        internal void WaitForStartup()
        {
            _startupWaitHandle.WaitOne();
        }

        internal void Stop()
        {
            try
            {
                _endpoint.BeginTerminate(
                    terminateAsyncResult =>
                    {
                        try
                        {
                            _endpoint.EndTerminate(terminateAsyncResult);
                            Console.WriteLine("Terminated endpoint.");

                            ShutDownPlatform();
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    }, null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void ShutDownPlatform()
        {
            try
            {
                _platform.BeginShutdown(shutdownAsyncResult =>
                {
                    try
                    {
                        _platform.EndShutdown(shutdownAsyncResult);
                        Console.WriteLine("Shut down platform.");
                    }
                    catch (RealTimeException ex)
                    {
                        Console.WriteLine(ex);
                    }
                },
                null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void OnAudioVideoCallReceived(object sender, 
            CallReceivedEventArgs<AudioVideoCall> e)
        {
            if (e.CallToBeReplaced == null)
            {
                // Initial call (not a self-transfer).
                SupervisorJoinCallSession session =
                    new SupervisorJoinCallSession(e.Call, 
                        _recipientSipUri, _supervisorSipUri);
                session.HandleIncomingCall();
            }
            else
            {
                // Self-transfer for a supervisor join. Get
                // the session object out of the ApplicationContext
                // property on the replaced call (where we
                // put it before doing the self-transfer).
                SupervisorJoinCallSession session = 
                    e.CallToBeReplaced.Conversation.ApplicationContext 
                    as SupervisorJoinCallSession;
                session.HandleIncomingSelfTransfer(e.Call);
            }
        }
    }
}



Code Listing 2: SupervisorJoinCallSession.cs

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

namespace SupervisorJoin
{
    internal class SupervisorJoinCallSession
    {
        AudioVideoCall _frontEndCallLeg;
        AudioVideoCall _backEndCallLeg;
        BackToBackCall _b2bCall;

        string _recipientSipUri;
        string _supervisorSipUri;

        internal SupervisorJoinCallSession(AudioVideoCall call, string recipientSipUri, string supervisorSipUri)
        {
            _frontEndCallLeg = call;
            _recipientSipUri = recipientSipUri;
            _supervisorSipUri = supervisorSipUri;
        }

        internal void HandleIncomingCall()
        {
            Console.WriteLine("Incoming call.");

            // Create a new conversation for the back-end leg
            // of the B2B call (which will connect to the conference).
            LocalEndpoint localEndpoint = 
                _frontEndCallLeg.Conversation.Endpoint;
            Conversation backEndConversation = 
                new Conversation(localEndpoint);

            // Impersonate the caller so that the caller, rather than
            // the application, will appear to be participating in
            // the conference.
            string callerSipUri = 
                _frontEndCallLeg.RemoteEndpoint.Participant.Uri;
            backEndConversation.Impersonate(callerSipUri, null, null);

            try
            {
                // Join the conference.
                backEndConversation.ConferenceSession.BeginJoin(
                    default(ConferenceJoinOptions), 
                    joinAsyncResult => {
                        try 
                        {
                            backEndConversation.ConferenceSession.EndJoin(
                                joinAsyncResult);
                            Console.WriteLine("Joined conference.");

                            _backEndCallLeg = new AudioVideoCall(backEndConversation);

                            CreateBackToBack();
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void CreateBackToBack()
        {
            // Create a back to back call between the caller
            // and the conference. This is so the caller
            // will not see that he/she is connected to a 
            // conference.
            BackToBackCallSettings frontEndCallLegSettings = 
                new BackToBackCallSettings(_frontEndCallLeg);
            BackToBackCallSettings backEndCallLegSettings = 
                new BackToBackCallSettings(_backEndCallLeg);

            _b2bCall = new BackToBackCall(frontEndCallLegSettings, 
                backEndCallLegSettings);

            try
            {
                // Establish the back to back call.
                _b2bCall.BeginEstablish(
                    establishAsyncResult =>
                    {
                        try
                        {
                            _b2bCall.EndEstablish(
                                establishAsyncResult);
                            Console.WriteLine("Back to back call established.");

                            InviteRecipientToConference();
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void InviteRecipientToConference()
        {
            ConferenceInvitation invitation = 
                new ConferenceInvitation(_backEndCallLeg.Conversation);

            try
            {
                // Invite the recipient to the conference
                // using a conference invitation.
                invitation.BeginDeliver(
                    _recipientSipUri,
                    deliverAsyncResult =>
                    {
                        try
                        {
                            invitation.EndDeliver(deliverAsyncResult);

                            Console.WriteLine("Invited recipient to the conference.");

                            InviteSupervisor();
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void InviteSupervisor()
        {
            LocalEndpoint localEndpoint = 
                _backEndCallLeg.Conversation.Endpoint;

            // Create a new audio call to call the supervisor.
            Conversation supervisorConversation = 
                new Conversation(localEndpoint);
            AudioVideoCall supervisorCall = 
                new AudioVideoCall(supervisorConversation);

            try
            {
                // Place an outbound call to the supervisor.
                supervisorCall.BeginEstablish(_supervisorSipUri, 
                    default(CallEstablishOptions),
                    establishAsyncResult =>
                {
                    try
                    {
                        supervisorCall.EndEstablish(establishAsyncResult);

                        // Wait for a couple of seconds
                        // before transferring.
                        Thread.Sleep(2000);

                        SelfTransferSupervisorCall(supervisorCall);
                    }
                    catch (RealTimeException ex)
                    {
                        Console.WriteLine(ex);
                    }
                },
                null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void SelfTransferSupervisorCall(AudioVideoCall supervisorCall)
        {
            // Put this instance of SupervisorJoinCallSession
            // into the context property for retrieval when
            // the application receives the self-transferred call.
            supervisorCall.Conversation.ApplicationContext =
                this;

            try
            {
                // Perform a self-transfer.
                supervisorCall.BeginTransfer(
                    supervisorCall,
                    transferAsyncResult =>
                    {
                        try
                        {
                            supervisorCall.EndTransfer(transferAsyncResult);
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        internal void HandleIncomingSelfTransfer(AudioVideoCall call)
        {
            string conferenceUri = _backEndCallLeg.Conversation.ConferenceSession.ConferenceUri;

            // Create a new conversation for the
            // back end call leg.
            LocalEndpoint localEndpoint = 
                _backEndCallLeg.Conversation.Endpoint;
            Conversation conferenceConversation = 
                new Conversation(localEndpoint);

            // Prepare to join the conference as a trusted
            // participant so as to be allowed to
            // manipulate audio routing.
            ConferenceJoinOptions joinOptions = 
                new ConferenceJoinOptions();
            joinOptions.JoinMode = JoinMode.TrustedParticipant;

            try
            {
                // Join the conference.
                conferenceConversation.ConferenceSession.BeginJoin(conferenceUri,
                    joinOptions, joinAsyncResult =>
                    {
                        try
                        {
                            conferenceConversation.ConferenceSession.EndJoin(joinAsyncResult);

                            CreateSupervisorB2BCall(call, conferenceConversation);
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void CreateSupervisorB2BCall(AudioVideoCall incomingCall, 
            Conversation conferenceConversation)
        {
            // Create a new audio call on the back end call leg.
            AudioVideoCall callToConference = 
                new AudioVideoCall(conferenceConversation);

            // Set up the call to be automatically removed
            // from the default audio mix in the A/V MCU.
            AudioVideoCallEstablishOptions establishOptions = 
                new AudioVideoCallEstablishOptions();
            establishOptions.AudioVideoMcuDialInOptions.
                RemoveFromDefaultRouting = true;

            // Create a back to back call between the supervisor
            // and the conference.
            BackToBackCallSettings frontEndB2BSettings = 
                new BackToBackCallSettings(incomingCall);
            BackToBackCallSettings backEndB2BSettings = 
                new BackToBackCallSettings(callToConference);

            backEndB2BSettings.CallEstablishOptions = establishOptions;

            BackToBackCall supervisorB2BCall = 
                new BackToBackCall(frontEndB2BSettings, backEndB2BSettings);

            try
            {
                // Establish the B2B call.
                supervisorB2BCall.BeginEstablish(
                    establishAsyncResult =>
                    {
                        try
                        {
                            supervisorB2BCall.EndEstablish(establishAsyncResult);

                            CreateManualAudioRoutes(callToConference);
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void CreateManualAudioRoutes(AudioVideoCall callToConference)
        {
            AudioVideoMcuSession avMcu = 
                _backEndCallLeg.Conversation.ConferenceSession.AudioVideoMcuSession;

            // Get the ParticipantEndpoint objects for the caller
            // and recipient.
            ParticipantEndpoint callerParticipantEndpoint = 
                avMcu.GetRemoteParticipantEndpoints().Single(p => 
                    p.Participant.Uri == _backEndCallLeg.Conversation.Endpoint.OwnerUri);
            ParticipantEndpoint recipientParticipantEndpoint = 
                avMcu.GetRemoteParticipantEndpoints().Single(p => 
                    p.Participant.Uri == _recipientSipUri);

            // Create incoming audio routes from the caller
            // and recipient to the supervisor.
            IncomingAudioRoute callerToSupervisorAudioRoute = 
                new IncomingAudioRoute(callerParticipantEndpoint);
            IncomingAudioRoute recipientToSupervisorAudioRoute = 
                new IncomingAudioRoute(recipientParticipantEndpoint);

            List<IncomingAudioRoute> routes = new List<IncomingAudioRoute>() { 
                callerToSupervisorAudioRoute, recipientToSupervisorAudioRoute };

            try
            {
                // Update the MCU audio routing with the new incoming routes.
                callToConference.AudioVideoMcuRouting.BeginUpdateAudioRoutes(
                    null, routes,
                    updateRoutesAsyncResult =>
                    {
                        try
                        {
                            callToConference.AudioVideoMcuRouting.EndUpdateAudioRoutes(
                                updateRoutesAsyncResult);
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void RemoveCallerFromDefaultAudioMix()
        {
            AudioVideoMcuSession avMcu =
                _backEndCallLeg.Conversation.ConferenceSession.AudioVideoMcuSession;

            // Get the ParticipantEndpoint object for the caller.
            ParticipantEndpoint callerParticipantEndpoint = 
                avMcu.GetRemoteParticipantEndpoints().Single(p => 
                    p.Participant.Uri == _backEndCallLeg.Conversation.Endpoint.OwnerUri);

            try
            {
                // Remove the caller from the default audio mix.
                // He or she will not hear or be heard by any
                // other participants.
                avMcu.BeginRemoveFromDefaultRouting(callerParticipantEndpoint,
                    removeAsyncResult =>
                    {
                        try
                        {
                            avMcu.EndRemoveFromDefaultRouting(removeAsyncResult);
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void AddCallerToDefaultAudioMix()
        {
            AudioVideoMcuSession avMcu =
                _backEndCallLeg.Conversation.ConferenceSession.AudioVideoMcuSession;

            // Get the ParticipantEndpoint object for the caller.
            ParticipantEndpoint callerParticipantEndpoint =
                avMcu.GetRemoteParticipantEndpoints().Single(p =>
                    p.Participant.Uri == _backEndCallLeg.Conversation.Endpoint.OwnerUri);

            try
            {
                // Remove the caller from the default audio mix.
                // He or she will not hear or be heard by any
                // other participants.
                avMcu.BeginAddToDefaultRouting(callerParticipantEndpoint,
                    addAsyncResult =>
                    {
                        try
                        {
                            avMcu.EndAddToDefaultRouting(addAsyncResult);
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}




Manual audio routing, one of the advanced conference control features in Microsoft Unified Communications Managed API (UCMA) 3.0, is very useful for reproducing PBX-like functionality in Microsoft Lync Server 2010 applications. In this article, you have learned how to use manual audio routes together with the BackToBackCall class to implement PBX-like features in your UCMA application.

Michael Greenlee is currently a Senior Consultant at Avanade | LinkedIn.

Show:
© 2014 Microsoft