Creating Custom Lync 2010 On-Hold Features with UCMA 3.0

Summary:   This article describes how to use Microsoft Unified Communications Managed API (UCMA) 3.0 to create customized on-hold response features for Microsoft Lync 2010 calls.

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

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

Contents

  • Custom On-Hold Response Feature

  • Prerequisites

  • Creating a Hold Provider

  • Looping Hold Music

  • Playing Messages at Intervals

  • Giving Callers Alternatives to Holding

  • Complete Code Listing

  • Conclusion

  • Additional Resources

Custom On-Hold Response Feature

Most telephone systems, when a caller is put on hold or is waiting in a queue, play music so that the caller knows he or she has not been disconnected. Microsoft Lync 2010 allows for hold music to be played to the party on the other end when a Lync user puts a call on hold. In building bots, IVRs, auto-attendants, or other Microsoft Unified Communications Managed API (UCMA) 3.0 applications, it is often necessary to have a caller wait for some action to occur or for a human representative to become available. This article describes how developers can use UCMA 3.0 to create customized hold response features, which, in addition to playing music, can also play messages at set intervals or offer the caller alternatives to waiting on hold.

Prerequisites

This article assumes that you have a basic familiarity with UCMA 3.0 and understand how to write an application which can register an endpoint with Microsoft Lync Server 2010 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 use the Lync Server Management Shell to provision a trusted application pool, trusted application, and a trusted application endpoint. For more information, see Activating a UCMA 3.0 Core Trusted Application.

Creating a Hold Provider

Because many of the resources that are used to play hold music and messages can be shared between calls, it is usually worthwhile to create a separate class which contains all of the code related to these features. In the example code for this article, this class is named HoldMusicProvider. The HoldMusicProvider class has an Attach method, which takes an AudioVideoCall object as a parameter and starts to play music and messages to that call.

Playing audio to a call in UCMA requires an instance of the Player class. Player attaches to an instance of AudioVideoFlow, which handles the media flow for audio calls. The audio source for the Player object is represented by an instance of MediaSource. The MediaSource object usually represents a Windows Media Audio (WMA) file.

One instance of Player is needed for each audio file, but it can be shared across multiple AudioVideoFlow objects, and therefore across multiple calls. So, the hold music provider can have one instance of Player for hold music, and another for the hold message. If there are multiple hold messages, additional instances might be necessary.

internal class HoldMusicProvider
{
    Player _holdMusicPlayer = new Player();
    Player _holdMessagePlayer = new Player();

Before the hold music provider can function, these two Player objects must be set up. To prepare a Player object to play audio for a call:

  1. Create a media source.

  2. Prepare the media source by using the BeginPrepareSource method.

  3. Create a new instance of Player.

  4. Set the source for the Player instance using SetSource.

  5. If necessary, set the mode of the Player instance using SetMode.

Creating the Media Source

To play audio, an instance of Player should include a MediaSource object. In UCMA 3.0, the only available subclass of MediaSource is WmaFileSource, which uses a WMA file.

When creating a new instance of WmaFileSource, supply the path to the WMA file as the only parameter to the constructor.

WmaFileSource mediaSource = new WmaFileSource(path);

Preparing the Media Source

Before the media source can be used for a Player object, you must prepare the media source by using the BeginPrepareSource method and the corresponding EndPrepareSource.

The first parameter for BeginPrepareSource must be a value from the MediaSourceOpenMode enumeration. There are two possible values: Buffered, and Unbuffered. In buffered mode, the content of the file is cached to improve performance. In unbuffered mode, it is read from the disk every time that it is needed. It is generally best to use buffered mode unless the file will be changing frequently and must be reread by the application.

try
{
    // Prepare the hold music source.
    // Use buffered mode for better performance since we
    // do not expect the music file to change.
    mediaSource.BeginPrepareSource(MediaSourceOpenMode.Buffered,
        prepareAsyncResult =>
        {
            try
            {
                mediaSource.EndPrepareSource(prepareAsyncResult);

                Console.WriteLine("Hold music file source prepared.");

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

Creating the Player

Instantiating a Player object is very simple, as the following code shows. The constructor requires no parameters.

Player player = new Player();

Setting the Source for the Player

With both the MediaSource object and Player object that was created, you can tell the Player object to use the instance of MediaSource by calling SetSource, as shown in the following code.

// Assign the source to the Player object.
player.SetSource(mediaSource);

It is worth noting that while Player can only use a single instance of MediaSource, it is possible for multiple Player objects to share a MediaSource instance. For example, if you wanted each call to have a separate Player object for the hold message, perhaps in order to play the message at different times for different calls, you could prepare a single MediaSource object and set it as the source for several different instances of Player.

Setting the Mode for the Player

The Player’s SetMode method lets you determine how a Player object will behave when there are no media flows attached. In Automatic mode, the default, Player will stop automatically when the last flow is detached. In Manual mode, on the other hand, Player can continue to play its audio even if no flows are attached.

The latter makes sense for our hold music provider. By using its Player objects in Manual mode, it can loop through the hold music and messages without having to worry about whether any calls are attached.

The following code shows how to set the mode of a Player object.

// Use manual mode so the Player object will continue
// playing even when no flows are attached.
player.SetMode(PlayerMode.Manual);

The completed Initialize and SetUpPlayer methods for the hold music provider appear in the next example. SetUpPlayer takes a reference to a variable that contains a Player object, and a file path. It then prepares the media source and sets up the Player object. Initialize merely calls SetUpPlayer two times, for the hold music player and the message player.

internal void Initialize()
{
    SetUpPlayer(ref _holdMusicPlayer, _musicWmaFilePath);
    SetUpPlayer(ref _holdMessagePlayer, _messageWmaFilePath);
}

private void SetUpPlayer(ref Player playerToCreate, string path)
{
    WmaFileSource mediaSource = new WmaFileSource(path);
    Player player = new Player();

    try
    {
        // Prepare the hold music source.
        // Use buffered mode for better performance since we
        // do not expect the music file to change.
        mediaSource.BeginPrepareSource(MediaSourceOpenMode.Buffered,
            prepareAsyncResult =>
            {
                try
                {
                    mediaSource.EndPrepareSource(prepareAsyncResult);

                    Console.WriteLine("Hold music file source prepared.");

                    // Assign the source to the Player object.
                    player.SetSource(mediaSource);

                    // Use manual mode so the Player object will continue
                    // playing even when no flows are attached.
                    player.SetMode(PlayerMode.Manual);

                    Interlocked.Increment(ref _mediaSourcesPrepared);

                    if (_mediaSourcesPrepared == 2)
                    {
                        StartMusicPlayer();
                    }
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }

    playerToCreate = player;
}

Figure 1. Relationship between the audio flows, the Player instance, and the media source in the sample application

Application hierarchy

Looping Hold Music

Player is designed to play back its audio one time, and then stop. It has no built-in facility for looping playback. However, it is easy to make it loop by taking advantage of the StateChanged event.

The next example shows a method that calls Player.Start() to start playback, after registering an event handler for the Player.StateChanged event. The event handler merely starts playback again when the Player object’s state changes to Stopped.

internal void StartMusicPlayer()
{
    // Subscribe to Player state changes
    // in order to restart the Player object
    // when it stops.
    _holdMusicPlayer.StateChanged += OnHoldMusicPlayerStateChanged;

    // Start playing.
    _holdMusicPlayer.Start();

    Console.WriteLine("Player started.");

    ...
}

private void OnHoldMusicPlayerStateChanged(object sender,
    PlayerStateChangedEventArgs e)
{
    // Whenever the Player object stops because 
    // it has reached the end of the music file, 
    // start it again.
    if (e.State == PlayerState.Stopped)
    {
        _holdMusicPlayer.Start();
    }
}

Important

Unregister this event if you actually want to stop playback completely. Otherwise, the Player object will be restarted again when you call Player.Stop().

Playing Messages at Intervals

A common feature of on-hold response feature in contact centers or other customer service lines is a message that plays at set intervals to thank the caller for waiting or suggest alternatives to remaining on hold.

It is fairly easy to create this same response feature in a UCMA application. It is generally easiest to use a separate Player object to play the message (and additional objects if there are multiple messages). This is the approach taken by the sample hold music provider. After starting the hold music player, the hold music provider starts a timer which expires after 20 seconds.

When the timer callback is invoked, the hold music provider detaches all of the calls from the hold music player and attaches them to the message player. Then it calls Start on the message player.

Meanwhile, it also registers an event handler for the StateChanged event on the message player. When the message has finished playing, it detaches the calls from the message player and reattaches them to the hold music player. It then resets the timer to expire in another 20 seconds. Figure 2 shows this process.

Figure 2. Player message hierarchy

Player message hierarchy

The timer callback and the event handler for the StateChanged event appears in the next example.

private void OnTimerInterval(object state)
{
    _messageTimer.Change(Timeout.Infinite, Timeout.Infinite);

    List<AudioVideoCall> calls;

    // Manipulate the calls within a lock
    // so that race conditions are not present.
    lock (_syncObject)
    {
        calls = _attachedCalls.ToList();

        calls.ForEach(c =>
        {
            _holdMusicPlayer.DetachFlow(c.Flow);
            _holdMessagePlayer.AttachFlow(c.Flow);
        });
    }

    _holdMessagePlayer.StateChanged += OnHoldMessagePlayerStateChanged;
    _holdMessagePlayer.Start();
}

private void OnHoldMessagePlayerStateChanged(object sender, 
    PlayerStateChangedEventArgs e)
{
    if (e.State == PlayerState.Stopped)
    {
        _holdMessagePlayer.StateChanged -= OnHoldMessagePlayerStateChanged;

        // Manipulate the calls within a lock
        // so that race conditions are not present.
        lock (_syncObject)
        {
            List<AudioVideoFlow> flows = _holdMessagePlayer.AudioVideoFlows.ToList();

            flows.ForEach(f =>
            {
                _holdMessagePlayer.DetachFlow(f);
                _holdMusicPlayer.AttachFlow(f);
            });
        }

        _messageTimer.Change(20000, Timeout.Infinite);
    }
}

Giving Callers Alternatives to Holding

As customers accustomed to instant feedback become increasingly impatient with long hold times, it has become popular for customer service lines to give callers one or more alternatives to waiting on hold. These may include receiving a callback later, being transferred to voice mail, or being forwarded to a different queue.

The sample application shows how to do this by letting callers to push the pound key to be transferred to another SIP URI which is contained in the configuration file.

In order to recognize when a caller has pushed the pound key, the application uses the ToneController class. ToneController is responsible for both generating and recognizing dual-tone multi-frequency (DTMF) tones on audio calls. When the application puts a call on hold, it creates an instance of ToneController and subscribes to the ToneReceived event. If the tone that the caller has dialed is the pound tone, it transfers the call.

Use the following code to listen for DTMF tones and transfer the call when the pound tone is received.

private void StartListeningForTones()
{
    // Create a new ToneController instance
    // and begin listening for tones on the call.
    _toneController = new ToneController();
    _toneController.ToneReceived += OnToneReceived;
    _toneController.AttachFlow(_call.Flow);
    Console.WriteLine("Tone controller attached.");
}

private void OnToneReceived(object sender, ToneControllerEventArgs e)
{
    // Lock to ensure that you do not process two tones at once
    lock (_syncObject)
    {
        // If we receive the pound tone, 
        // transfer the call.
        if (e.Tone == (int)ToneId.Pound)
        {
            _toneController.DetachFlow();

            Console.WriteLine("Pound tone received; transferring call.");
            TransferCallToSipUri(_recipientSipUri);
        }
    }
}

private void TransferCallToSipUri(string _recipientSipUri)
{
    // Perform a "blind transfer" to the SIP URI
    // specified in configuration.
    // The hold music player will be detached 
    // automatically because the original call 
    // will be terminated.
    try
    {
        _call.BeginTransfer(_recipientSipUri, 
            new CallTransferOptions(CallTransferType.Unattended),
            transferAsyncResult =>
            {
                try
                {
                    _call.EndTransfer(transferAsyncResult);

                    Console.WriteLine("Transferred call to {0}", _recipientSipUri);
                }
                catch (RealTimeException ex)
                {
                    Console.WriteLine(ex);
                }
            },
            null);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex);
    }
}

Complete Code Listing

The code for the example hold music provider class and the CustomOnHoldCallSession class appear in this section.

Hold music provider class

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

namespace CustomOnHold
{
    internal class HoldMusicProvider
    {
        internal event EventHandler<EventArgs> HoldMusicProviderReady;

        Player _holdMusicPlayer = new Player();
        Player _holdMessagePlayer = new Player();

        Timer _messageTimer;

        string _musicWmaFilePath;
        string _messageWmaFilePath;

        int _mediaSourcesPrepared = 0;

        readonly object _syncObject = new object();

        readonly List<AudioVideoCall> _attachedCalls = 
            new List<AudioVideoCall>();

        internal HoldMusicProvider(string musicWmaFilePath, string messageWmaFilePath)
        {
            _musicWmaFilePath = musicWmaFilePath;
            _messageWmaFilePath = messageWmaFilePath;
        }

        internal void Initialize()
        {
            SetUpPlayer(ref _holdMusicPlayer, _musicWmaFilePath);
            SetUpPlayer(ref _holdMessagePlayer, _messageWmaFilePath);
        }

        private void SetUpPlayer(ref Player playerToCreate, string path)
        {
            WmaFileSource mediaSource = new WmaFileSource(path);
            Player player = new Player();

            try
            {
                // Prepare the hold music source.
                // Use buffered mode for better performance since the
                // music file is static.
                mediaSource.BeginPrepareSource(MediaSourceOpenMode.Buffered,
                    prepareAsyncResult =>
                    {
                        try
                        {
                            mediaSource.EndPrepareSource(prepareAsyncResult);

                            Console.WriteLine("Hold music file source prepared.");

                            // Assign the source to the Player object.
                            player.SetSource(mediaSource);

                            // Use manual mode so the Player object will continue
                            // playing even when no flows are attached.
                            player.SetMode(PlayerMode.Manual);

                            Interlocked.Increment(ref _mediaSourcesPrepared);

                            if (_mediaSourcesPrepared == 2)
                            {
                                StartMusicPlayer();
                            }
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }

            playerToCreate = player;
        }

        internal void StartMusicPlayer()
        {
            // Subscribe to Player state changes
            // in order to restart the Player object
            // when it stops.
            _holdMusicPlayer.StateChanged += OnHoldMusicPlayerStateChanged;

            // Start playing.
            _holdMusicPlayer.Start();

            Console.WriteLine("Player started.");

            _messageTimer = new Timer(OnTimerInterval, null, 20000, Timeout.Infinite);

            if (HoldMusicProviderReady != null)
            {
                HoldMusicProviderReady(this,
                    new EventArgs());
            }
        }

        internal void ShutDown()
        {
            // Remove the event before stopping the player so it 
            // does not automatically start again.
            _holdMusicPlayer.StateChanged -= OnHoldMusicPlayerStateChanged;
            _holdMusicPlayer.Stop();

            // Clean up the media source.
            MediaSource source = _holdMusicPlayer.Source;
            _holdMusicPlayer.RemoveSource();
            source.Close();
        }

        private void OnHoldMusicPlayerStateChanged(object sender,
            PlayerStateChangedEventArgs e)
        {
            // Whenever the Player object stops because 
            // it has reached the end of the music file, 
            // start it again.
            if (e.State == PlayerState.Stopped)
            {
                _holdMusicPlayer.Start();
            }
        }

        private void OnTimerInterval(object state)
        {
            _messageTimer.Change(Timeout.Infinite, Timeout.Infinite);

            List<AudioVideoCall> calls;

            // Manipulate the calls within a lock
            // to ensure that race conditions do not appear.
            lock (_syncObject)
            {
                calls = _attachedCalls.ToList();

                calls.ForEach(c =>
                {
                    _holdMusicPlayer.DetachFlow(c.Flow);
                    _holdMessagePlayer.AttachFlow(c.Flow);
                });
            }

            _holdMessagePlayer.StateChanged += OnHoldMessagePlayerStateChanged;
            _holdMessagePlayer.Start();
        }

        private void OnHoldMessagePlayerStateChanged(object sender, 
            PlayerStateChangedEventArgs e)
        {
            if (e.State == PlayerState.Stopped)
            {
                _holdMessagePlayer.StateChanged -= OnHoldMessagePlayerStateChanged;

                // Manipulate the calls within a lock
                // to ensure that race conditions do not appear.
                lock (_syncObject)
                {
                    List<AudioVideoFlow> flows = _holdMessagePlayer.AudioVideoFlows.ToList();

                    flows.ForEach(f =>
                    {
                        _holdMessagePlayer.DetachFlow(f);
                        _holdMusicPlayer.AttachFlow(f);
                    });
                }

                _messageTimer.Change(20000, Timeout.Infinite);
            }
        }

        internal void Attach(AudioVideoCall call)
        {
            lock (_syncObject)
            {
                _attachedCalls.Add(call);
                _holdMusicPlayer.AttachFlow(call.Flow);
                call.StateChanged += new EventHandler<Microsoft.Rtc.Collaboration.CallStateChangedEventArgs>(
                    OnCallStateChanged);
            }
        }

        internal void Detach(AudioVideoCall call)
        {
            call.StateChanged -= OnCallStateChanged;

            lock (_syncObject)
            {
                _attachedCalls.Remove(call);

                if (_holdMessagePlayer.AudioVideoFlows.Contains(call.Flow))
                {
                    _holdMessagePlayer.DetachFlow(call.Flow);
                }
                if (_holdMusicPlayer.AudioVideoFlows.Contains(call.Flow))
                {
                    _holdMusicPlayer.DetachFlow(call.Flow);
                }
            }
        }

        private void OnCallStateChanged(object sender, 
            Microsoft.Rtc.Collaboration.CallStateChangedEventArgs e)
        {
            if (e.State == Microsoft.Rtc.Collaboration.CallState.Terminated)
            {
                // If a call terminates, detach it from any players 
                // it is attached to.

                Detach(sender as AudioVideoCall);
            }
        }
    }
}



The complete code for the CustomOnHoldCallSession class appears in this section.

CustomOnHoldCallSession class

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 CustomOnHold
{
    internal class CustomOnHoldCallSession
    {
        AudioVideoCall _call;
        HoldMusicProvider _holdMusicProvider;
        ToneController _toneController;

        readonly object _syncObject = new object();

        string _recipientSipUri;

        internal CustomOnHoldCallSession(AudioVideoCall call, string recipientSipUri,
            HoldMusicProvider holdMusicProvider)
        {
            _call = call;
            _recipientSipUri = recipientSipUri;
            _holdMusicProvider = holdMusicProvider;
        }

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

            try
            {
                _call.StateChanged += 
                    new EventHandler<CallStateChangedEventArgs>(OnCallStateChanged);

                // Accept the call.
                _call.BeginAccept(
                    acceptAsyncResult =>
                    {
                        try
                        {
                            _call.EndAccept(acceptAsyncResult);

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

        void OnCallStateChanged(object sender, 
            CallStateChangedEventArgs e)
        {
            if (e.State == CallState.Terminated)
            {
                if (_toneController != null)
                {
                    _toneController.DetachFlow();
                }

                _call.StateChanged -= OnCallStateChanged;
            }
        }

        private void StartHoldMusic()
        {
            _holdMusicProvider.Attach(_call);
            Console.WriteLine("Hold music provider attached.");
        }

        private void StartListeningForTones()
        {
            // Create a new ToneController instance
            // and begin listening for tones on the call.
            _toneController = new ToneController();
            _toneController.ToneReceived += OnToneReceived;
            _toneController.AttachFlow(_call.Flow);
            Console.WriteLine("Tone controller attached.");
        }

        private void OnToneReceived(object sender, ToneControllerEventArgs e)
        {
            // Lock to ensure that you do not process two tones at once
            lock (_syncObject)
            {
                // If we receive the pound tone, 
                // transfer the call.
                if (e.Tone == (int)ToneId.Pound)
                {
                    _toneController.DetachFlow();

                    Console.WriteLine("Pound tone received; transferring call.");
                    TransferCallToSipUri(_recipientSipUri);
                }
            }
        }

        private void TransferCallToSipUri(string _recipientSipUri)
        {
            // Perform a "blind transfer" to the SIP URI
            // specified in configuration.
            // The hold music player will be detached 
            // automatically because the original call 
            // will be terminated.
            try
            {
                _call.BeginTransfer(_recipientSipUri, 
                    new CallTransferOptions(CallTransferType.Unattended),
                    transferAsyncResult =>
                    {
                        try
                        {
                            _call.EndTransfer(transferAsyncResult);

                            Console.WriteLine("Transferred call to {0}", _recipientSipUri);
                        }
                        catch (RealTimeException ex)
                        {
                            Console.WriteLine(ex);
                        }
                    },
                    null);
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}

Conclusion

UCMA applications which serve lots of voice callers often have to provide hold music while callers are waiting. This article discussed how to provide various customized hold response features to callers, including looping music, regular messages, and alternatives to waiting on hold.

Additional Resources

For more information, see the following resources:

About the Author

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