Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to develop a call queue with periodically played informational messages in C# for improving your VoIP PBX

0.00/5 (No votes)
22 Jul 2014 1  
It explains how to create a virtual call queue extension in your C# IP PBX in order to make your Automatic Call Distribution (ACD) system more effective.

Introduction

In call centers and corporate communication systems the number of incoming calls can exceed the number of available agents/operators. The lack of human resources often results missed calls, therefore dissatisfied customers and lower revenues. As a practical solution for this problem, Automatic Call Distribution (ACD) is commonly used system for distributing incoming calls to specific resources (agents /operators). Due to the call queuing, your system will be able to receive each incoming call even if they are simultaneous calls.

Background

In this year I have started a great PBX project, and I shared my experiences in the past few months. Now I am going to continue the improvement of my VoIP PBX by explaining how to develop a virtual call queue extension to manage simultaneous inbound calls. By using call queuing, your phone system can handle the incoming calls in a queued system. The following figure illustrates what does it mean. If there aren’t any available call center agents (because all of them are in call), the incoming calls will be put at the end of the call queue. When an agent gets available, the first call in the queue will be transferred to this agent. (During the waiting, you can inform the caller about the estimated waiting time and/or you can play some music in.)

Prerequisites

The implementation of this call queue project is based on my first PBX project. (Considering that the call queue functionality is an extra feature, my original PBX is viable without this improvement of course, but call queuing can make your whole VoIP communication system more effective.) Due to this fact, the initial steps have not been explained in this article. If you still haven’t got a VoIP SIP PBX, first please study how to build one:

How to build a simple SIP PBX in C# extended with dial plan functionality:

http://www.codeproject.com/Articles/739075/How-to-build-a-simple-SIP-PBX-in-Csharp-extended-w

  • First you need an IDE that supports C#, since my VoIP PBX has been written in this programming language. I prefer Microsoft Visual Studio.
  • You need to install .NET Framework (3.5 or any newer version) as well.
  • In order to be able to build any VoIP application, it is essentialy needed to add VoIP components to the references in your IDE. Considering that I’ve been using Ozeki VoIP SIP SDK for this purpose, during this project I also used this SDK.  So my call queue solution requires Ozeki SDK installed on your PC as well.

Having done the first steps let’s see how to implement call queuing in order to improve your existing VoIP phone system!

Writing the code

In this project the following 2 classes have been used for building a virtual call queue extension: CallQueueExtension and CallQueueCallHandler

Implementing the CallQueueExtension class

Code example 1 shows those objects that are needed in the CallQueueExtension class:

IExtensionContainer extensionContainer;  
IPBXCallFactory callFactory;  
List<CallQueueCallHandler> callHandlers;  
ICallManager callManager;  
List<string> members;  
object sync;

Code example 1: Necessary objects in the CallQueueExtension

As you can see above, the constructor has a list parameter.  This list contains the telephone numbers of those employees who will receive the incoming calls (Code example 2).

public CallQueueExtension(string extensionId, IExtensionContainer extensionContainer, IPBXCallFactory callFactory, ICallManager callManager, List<string> members)  
{  
    sync = new object();  
    callHandlers = new List<CallQueueCallHandler>();  
    Account = new Account(extensionId, extensionId, "ozekiphone.com");  
    this.extensionContainer = extensionContainer;  
    this.callFactory = callFactory;  
    this.callManager = callManager;  
  
    this.members = members;  
}

Code example 2: The constructor of the CallQueueExtension

Implementing the CallQueueCallHandler class

This class is used to manage the calls. This class works with the call queues just like it deals with paralell calls, since these calls are similar to each other. This way each call will be a seperate CallQueueCallHandler object.

Now take a look at Code example 3. The OnCalled method handles the incoming calls. The createIncomingPBXCall method of callfactory can be used to create a call object for the incoming calls. The callQueueHandler variable contains the CallQueueCallHandler objects. These objects will be listed in the callHandlers list. If the list contains one call (that is there is only one incoming call in the call queue) you can call the Start() method that will start a timer.

public Ozeki.VoIP.PBX.PhoneCalls.ISIPCall OnCalled(Ozeki.VoIP.PBX.PhoneCalls.RouteInfo callInfo)  
{  
    lock (sync)  
    {  
        var call = callFactory.CreateIncomingPBXCall(this, SRTPMode.None);  
  
        var callQueueHandler = new CallQueueCallHandler(call, callManager, extensionContainer, members);  
        callQueueHandler.Completed += CallHandlerCompleted;  
        callHandlers.Add(callQueueHandler);  
  
        if (callHandlers.Count == 1)  
        {  
            callQueueHandler.Start();  
        }  
          
        return call;  
    }         
}

Code example 3: The CallQueueCallHandler object

Now let’s see what happens when a call is done. If the state of the call is Completed, the CallHandlerCompleted method will be called. If this call was the first one on the callHandlers list, that will be removed. If there are more calls on the list, the Start() method will be called for the next call (Code example 4).

void CallHandlerCompleted(object sender, System.EventArgs e)  
{  
    lock (sync)  
    {  
        var callHandler = (CallQueueCallHandler)sender;  
  
        if (callHandlers[0] == callHandler)  
        {  
            callHandlers.Remove(callHandler);  
  
            if (callHandlers.Count >= 1)  
            {  
                var nextCallHandler = callHandlers[0];  
                nextCallHandler.Start();  
            }  
        }  
        else  
        {  
            callHandlers.Remove(callHandler);  
        }  
    }  
} 

Code example 4: The CallHandlerCompleted method

The Start() method can be found in the CallQueueCallHandler class. As you can see in Code example 5, this starts the timer under the calls:

public void Start()  
        {  
            if (call.CallState.IsCallEnded())  
            {  
                OnCompleted();  
                return;  
            }  
  
            timer.Start();  
        }

Code example 5: The Start() method

Code example 6 demonstrates what happens after this. If the timer is elapsed, the timer_Elapsed method will be called. After an examination, if the timer_Elapsed method concludes that the state of the call is not InCall, the method will check which operator are in call by using the ActiveSession method of the callManager.  The telephone number of the available operators will be stored in a list. The extension container (extensionContainer.GetExtension()) can help you to select which operator in the list have logged into the system. If there is at least one available operator who are not in a call currently, the incoming call will be transferred to that operator due to the BlindTransfer method.

void timer_Elapsed(object sender, ElapsedEventArgs e)  
{  
    if (!call.CallState.IsInCall())  
        return;  
  
    var memb = new List<string>(members);  
  
    foreach (var activeSession in callManager.ActiveSessions)  
    {  
        var callee = activeSession.CalleeInfo.Owner.Account.UserName;  
        var caller = activeSession.CallerInfo.Owner.Account.UserName;  
  
        memb.Remove(callee);  
        memb.Remove(caller);  
    }  
  
    foreach (var member in memb)  
    {  
        if (extensionContainer.GetExtension(member) != null)  
        {  
            call.BlindTransfer(member);  
        }  
    }  
}  

Code example 6: The timer_Elapsed method

Implementing the periodical informational message playing

In order to make your call queue more user-friendly, you can improve it by implementing some advanced functionalities as well as periodically played informational message streaming. As a result, your callers will be informed about the currently estimated waiting time. This way - figuratively speaking - you can reduce the waiting time and keep your callers’ calm. It also encourages your callers to do not hang up the phone while they are waiting in the call queue. In this section I am going to show you how to create this feature.

I used the CallQueueHandlerContainer (Code example 7) and the CallHistory (Code example 8) classes to let the text-to-speech engine read a prewritten message for the queued callers. This message will inform them about the expected waiting time in every 15 seconds.

As you can see below, the CallQueueHandlerContainer class stores the callQueueCallHandler objects in a list (in other words: the calls) and masks all the operations can be performed (as well as Add, Count, GetIndex, Remove and Next). As a result, the rank of the call in the call queue can be easily identified. You only need to call the GetIndex method.

using System;  
using System.Collections.Generic;  
  
namespace MyPBX.CallQueue  
{  
    class CallQueueHandlerContainer  
    {  
        List<CallQueueCallHandler> callQueueCallHandlers;  
  
        public CallQueueHandlerContainer()  
        {  
            callQueueCallHandlers = new List<CallQueueCallHandler>();      
        }  
  
        public void Add(CallQueueCallHandler callHandler)  
        {  
            callQueueCallHandlers.Add(callHandler);  
        }  
  
        public int Count()  
        {  
            return callQueueCallHandlers.Count;  
        }  
  
        public int GetIndex(CallQueueCallHandler callHandler)  
        {  
            return callQueueCallHandlers.IndexOf(callHandler);  
        }  
  
        public void Remove(CallQueueCallHandler callHandler)  
        {  
            callQueueCallHandlers.Remove(callHandler);  
        }  
  
        public CallQueueCallHandler Next()  
        {  
            if (callQueueCallHandlers.Count == 0)  
                return null;  
            return callQueueCallHandlers[0];  
        }  
    }  
}

Code example 7: The CallQueueHandlerContainer class

Code example 8 presents the CallHistory class that handles the built-in system calls, and makes these data accessible. You only need to call the GetCallHistrory method.

using System.Collections.Generic;  
using Ozeki.VoIP.PBX.Services;  
using Ozeki.VoIP.PBX.PhoneCalls.Session;  
  
namespace MyPBX.CallQueue  
{  
    class CallHistory  
    {  
        static Dictionary<string, List<ISession>> callHistoryStatistics = new Dictionary<string, List<ISession>>();  
   
        public static void Init(ICallManager callManager)  
        {  
            callManager.SessionClosed += callManager_SessionClosed;  
        }  
  
        public static List<ISession> GetCallHistory(string userId)  
        {  
            if(!callHistoryStatistics.ContainsKey(userId))  
                return new List<ISession>();  
  
            return callHistoryStatistics[userId];  
        }  
  
        static void callManager_SessionClosed(object sender, Ozeki.VoIP.VoIPEventArgs<Ozeki.VoIP.PBX.PhoneCalls.Session.ISession> e)  
        {  
            var callee = e.Item.CalleeInfo.Owner.Account.UserName;  
  
            UpdateCallStatistics(callee, e.Item);  
  
        }  
  
        private static void UpdateCallStatistics(string party, ISession session)  
        {  
            if (!callHistoryStatistics.ContainsKey(party))  
                callHistoryStatistics[party] = new List<ISession>();  
  
            callHistoryStatistics[party].Add(session);  
        }  
    }  
}

Code example 8: The CallHistory class

To implement the automated informative message playing, you need to create a textToSpeech object within the CallQueueCallHandler class. As a result, your caller will be informed about his/her position in the queue and the estimated waiting time. Concerning to the fact that before and after an informative message an mp3 music can be heard, you need to link these with the calls by using the AudioMixesMediaHandler.

Code example 9 shows the timer_Elapsed method that can be used to calculate the expected waiting time. 

void timer_Elapsed(object sender, ElapsedEventArgs e)  
        {  
            if (!call.CallState.IsInCall())  
                return;  
  
            var memb = new List<string>(members);  
              
            foreach (var activeSession in callManager.ActiveSessions)  
            {  
                var callee = activeSession.CalleeInfo.Owner.Account.UserName;  
                var caller = activeSession.CallerInfo.Owner.Account.UserName;  
  
                memb.Remove(callee);  
                memb.Remove(caller);  
  
                double sum = 0;  
                var callCounter = 0;  
                  
                foreach (var m in memb)  
                {  
                    List<ISession> historyResult = CallHistory.GetCallHistory(m);  
  
                    foreach (var session in historyResult)  
                    {  
                        ++callCounter;  
                        sum += session.TalkDuration.TotalSeconds;  
                    }  
                }  
  
                if (sum != 0)  
                {  
                    waitingTime = sum / callCounter;  
                }  
            }  
  
            if(!blindTransferEnabled)  
                return;  
  
            foreach (var member in memb)  
            {  
                if (extensionContainer.GetExtension(member) != null)  
                {  
                    call.BlindTransfer(member);  
                }  
            }  
        }

Code example 9: The timer_Elapsed method

To determine the estimated waiting time, the program will go through the agent's call historyin order to calculate the average duration of a call. (Please note that in this project one agent has been used.)

To be able to provide information for the callers continuously, you need to create a new timer. It will run out in every 15 seconds then call the timerAgent_Elapsed method (Code example 10). This method will pause the mp3 streaming until the textToSpeech method reads the position of the client and the estimated waiting time. (If the are not any active call, the program will not calculate the waiting time and the informative message will not be played, since the caller will be directed to the agent.) The textToSpeech needs to be cleared by using the Clear method.

void timerAgent_Elapsed(object sender, ElapsedEventArgs e)  
        {  
            mp3Player.PauseStreaming();  
            textToSpeech.Clear();  
  
            if (waitingTime == 0)  
            {  
                textToSpeech.AddAndStartText(string.Format("{0} more customers are waiting in line before you. Please be patient.",  
                                                           callQueueHandlerContainer.GetIndex(this)));  
            }  
            else  
            {  
                textToSpeech.AddAndStartText(string.Format("{0} more customers are waiting in line before you. " +  
                                                           "Your estimated wait time is approximately {1} seconds. Please be patient.",  
                                                           callQueueHandlerContainer.GetIndex(this),  
                                                           (int)waitingTime));  
            }  
        }

Code example 10: The timerAgent_Elapsed method

As you can see below, the textToSpeech method will be carried out when it ends (that is, it is signed up to the TextToSpeechCompleted method). Here you can reset the streaming of the mp3 music.

void TextToSpeechCompleted(object sender, EventArgs e)  
        {  
            mp3Player.StartStreaming();  
            textToSpeech.Clear();  
        }

Code example 11: The TextToSpeechCompleted method

Summary

To sum it up, due to the VoIP technology, your existing VoIP PBX can be easily improved by implementing some useful additional functionalities. An important one of the possible features is call queuing. In this article I described how to create a virtual call queue extension to be able to accept the simultaneous incoming calls. I have tried to make my call queue more user-friendly by implementing mp3 music streaming and prediodical informational message playing. My solution has been written in C#, and I worked with Microsoft Visual Studio and Ozeki VoIP SIP SDK.

References

First steps in my previous Codeproject guide:

Theoretical background:

Download the necessary software:

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here