如何开发一个带有定期播放信息消息的呼叫队列,使用 C# 改进您的 VoIP PBX
本文介绍了如何在 C# IP PBX 中创建虚拟呼叫队列分机,以提高自动呼叫分配 (ACD) 系统的效率。
引言
在呼叫中心和企业通信系统中,来电数量可能会超过可用座席/接线员的数量。人力资源不足常常导致漏接电话,从而引起客户不满和收入下降。作为解决此问题的实际方案,自动呼叫分配 (ACD) 系统通常用于将呼入电话分配给特定资源(座席/接线员)。通过呼叫排队,即使是同时呼入的电话,您的系统也能接收到每一个。
背景
今年我启动了一个很棒的 PBX 项目,并在过去几个月里分享了我的经验。现在,我将继续改进我的 VoIP PBX,介绍如何开发一个虚拟呼叫队列分机来管理同时呼入的电话。通过使用呼叫排队,您的电话系统可以在排队系统中处理来电。**下图**说明了其含义。如果没有可用的呼叫中心座席(因为他们都在通话中),来电将被排入呼叫队列的末尾。当有座席空闲时,队列中的第一个电话将被转接给该座席。(在等待期间,您可以告知呼叫者预计的等待时间,以及/或者播放一些音乐。)
必备组件
这个呼叫队列项目的实现基于我的第一个 PBX 项目。(考虑到呼叫队列功能是一项附加功能,我的原始 PBX 在没有此改进的情况下也是可行的,但呼叫排队可以使整个 VoIP 通信系统更有效。)因此,初始步骤并未在本篇文章中进行介绍。如果您还没有 VoIP SIP PBX,请先学习如何构建一个。
如何用 C# 构建一个简单的 SIP PBX,
https://codeproject.org.cn/Articles/739075/How-to-build-a-simple-SIP-PBX-in-Csharp-extended-w
- 首先,您需要一个支持 C# 的 IDE,因为我的 VoIP PBX 是用这种编程语言编写的。我个人偏好 **Microsoft Visual Studio**。
- 您还需要安装 **.NET Framework**(3.5 或任何更新版本)。
- 为了能够构建任何 VoIP 应用程序,至关重要的是在 IDE 的引用中添加 VoIP 组件。考虑到我一直使用 **Ozeki VoIP SIP SDK** 来完成此目的,在此项目中我也使用了这个 SDK。因此,我的呼叫队列解决方案也要求您的 PC 上安装 Ozeki SDK。
完成初始步骤后,让我们看看如何实现呼叫排队以改进您现有的 VoIP 电话系统!
编写代码
在此项目中,使用了以下 2 个类来构建虚拟呼叫队列分机:CallQueueExtension
和 CallQueueCallHandler
。
实现 CallQueueExtension 类
**代码示例 1** 显示了 CallQueueExtension
类中需要的对象。
IExtensionContainer extensionContainer;
IPBXCallFactory callFactory;
List<CallQueueCallHandler> callHandlers;
ICallManager callManager;
List<string> members;
object sync;
代码示例 1: CallQueueExtension
中必需的对象
如上所示,构造函数有一个列表参数。此列表包含将接收来电的员工的电话号码(**代码示例 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;
}
代码示例 2: CallQueueExtension
的构造函数
实现 CallQueueCallHandler 类
此类用于管理呼叫。此类与呼叫队列的交互方式与处理并行呼叫类似,因为这些呼叫是相互独立的。这样,每个呼叫都将成为一个单独的 CallQueueCallHandler
对象。
现在来看 **代码示例 3**。OnCalled
方法处理来电。callfactory
的 createIncomingPBXCall
方法可用于创建来电的呼叫对象。callQueueHandler
变量包含 CallQueueCallHandler
对象。这些对象将被列在 callHandlers
列表中。如果列表中包含一个呼叫(即呼叫队列中只有一个来电),您可以调用 Start()
方法来启动一个计时器。
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;
}
}
代码示例 3: CallQueueCallHandler
对象
现在我们看看呼叫完成时会发生什么。如果呼叫状态为 Completed,则会调用 CallHandlerCompleted
方法。如果该呼叫是 callHandlers
列表中的第一个呼叫,它将被移除。如果列表中还有其他呼叫,将为下一个呼叫调用 Start()
方法(**代码示例 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);
}
}
}
代码示例 4: CallHandlerCompleted
方法
Start()
方法可以在 CallQueueCallHandler
类中找到。如 **代码示例 5** 所示,这会在呼叫下方启动计时器。
public void Start()
{
if (call.CallState.IsCallEnded())
{
OnCompleted();
return;
}
timer.Start();
}
代码示例 5: Start()
方法
**代码示例 6** 演示了之后发生的事情。如果计时器到期,将调用 timer_Elapsed
方法。经过检查,如果 timer_Elapsed
方法确定呼叫状态不是 InCall,则该方法将使用 callManager
的 ActiveSession
方法检查哪些座席正在通话中。可用座席的电话号码将被存储在一个列表中。分机容器(extensionContainer.GetExtension()
)可以帮助您选择列表中哪个座席已登录到系统。如果至少有一位当前未通话的座席可用,则来电将通过 BlindTransfer
方法转接给该座席。
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);
}
}
}
代码示例 6: timer_Elapsed
方法
实现周期性播放信息消息
为了使您的呼叫队列更用户友好,您还可以通过实现一些高级功能以及周期性播放信息消息流来改进它。因此,您的呼叫者将获得有关当前预计等待时间的信息。这样,(形象地说)您可以缩短等待时间,保持呼叫者的冷静。这也可以鼓励呼叫者在等待呼叫队列时不要挂断电话。在本节中,我将向您展示如何创建此功能。
我使用了 CallQueueHandlerContainer
(**代码示例 7**)和 CallHistory
(**代码示例 8**)类,让文本转语音引擎为排队的呼叫者朗读预先写好的消息。这条消息将每 15 秒告知他们预期的等待时间。
**如下所示**,CallQueueHandlerContainer
类将 callQueueCallHandler
对象存储在一个列表中(换句话说:呼叫)并隐藏所有可以执行的操作(例如 Add
、Count
、GetIndex
、Remove
和 Next
)。因此,可以轻松识别呼叫在呼叫队列中的排名。您只需要调用 GetIndex
方法。
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];
}
}
}
代码示例 7: CallQueueHandlerContainer
类
**代码示例 8** 介绍了 CallHistory
类,该类处理内置系统呼叫,并使这些数据可访问。您只需要调用 GetCallHistrory
方法。
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);
}
}
}
代码示例 8: CallHistory
类
要实现自动信息播放,您需要在 CallQueueCallHandler
类中创建一个 textToSpeech
对象。因此,您的呼叫者将获得有关他们在队列中的位置和预计等待时间的信息。考虑到在信息消息之前和之后可以听到 mp3 音乐,您需要使用 AudioMixesMediaHandler
将它们与呼叫链接起来。
**代码示例 9** 显示了 timer_Elapsed
方法,可用于计算预计等待时间。
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);
}
}
}
代码示例 9: timer_Elapsed
方法
为了确定预计的等待时间,程序将遍历座席的通话记录以计算平均通话时长。(请注意,在此项目中仅使用了一个座席。)
为了能够持续为呼叫者提供信息,您需要创建一个新的计时器。它将在每 15 秒后触发,然后调用 timerAgent_Elapsed
方法(**代码示例 10**)。此方法将暂停 mp3 流,直到 textToSpeech
方法朗读完客户的位置和预计等待时间。(如果没有活动的呼叫,程序将不会计算等待时间,也不会播放信息消息,因为呼叫者将被直接转接给座席。)需要使用 Clear
方法清除 textToSpeech
。
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));
}
}
代码示例 10: timerAgent_Elapsed
方法
**如下所示**,当 textToSpeech
方法完成时(即,它已注册到 TextToSpeechCompleted
方法),它将被执行。在这里,您可以重置 mp3 音乐的流。
void TextToSpeechCompleted(object sender, EventArgs e)
{
mp3Player.StartStreaming();
textToSpeech.Clear();
}
代码示例 11: TextToSpeechCompleted
方法
摘要
总而言之,由于 VoIP 技术,您现有的 VoIP PBX 可以通过实现一些有用的附加功能轻松得到改进。可能的特性中一个重要的就是呼叫排队。在本文中,我描述了如何创建一个虚拟呼叫队列分机来接受同时呼入的电话。我试图通过实现 mp3 音乐流和周期性信息消息播放来使我的呼叫队列更用户友好。我的解决方案是用 C# 编写的,我使用了 Microsoft Visual Studio 和 Ozeki VoIP SIP SDK。
参考文献
我在之前的 Codeproject 指南中的第一步
- 如何构建一个带有拨号计划功能的简单 C# SIP PBX
https://codeproject.org.cn/Articles/739075/How-to-build-a-simple-SIP-PBX-in-Csharp-extended-w
理论背景
- 关于自动呼叫分配 (ACD) 系统:http://en.wikipedia.org/wiki/Automatic_call_distributor
- 关于呼叫排队:http://www.voip-sip-sdk.com/p_424-voip-call-queuing-voip.html
下载必要的软件
- 下载 Microsoft Visual Studio:http://www.microsoft.com/en-us/download/details.aspx?id=40787
- 下载 .NET Framework:http://www.microsoft.com/hu-hu/download/details.aspx?id=30653
- 下载 Ozeki VoIP SIP SDK:http://voip-sip-sdk.com/p_21-download-ozeki-voip-sip-sdk-voip.html
- 免费下载 X-Lite 软电话进行测试:http://www.counterpath.com/x-lite-download