如何在 C# 中通过语音信箱功能改进您的 VoIP PBX
本指南演示如何使用 C# 将语音邮件服务集成到您的 VoIP PBX 中,以更有效地管理您的呼叫。
引言
在商业世界中,接听合作伙伴的来电具有重大意义。然而,在很多情况下您无法接听客户的电话:例如,如果您外出办公、休假或是在周末。语音邮件系统允许您的客户通过录制语音来给您的公司留言,然后稍后由一名员工收听这些录音。
背景
两个月前,我创建了一个简单的 SIP PBX,可用作公司电话系统。文章结尾处我提到我正在研究一个语音邮件解决方案,以改进我的 PBX。在这篇简短的文章中,我想介绍这个语音邮件项目,同时解释如何将语音邮件功能集成到您现有的 PBX 中。
必备组件
这个语音邮件项目基于我之前实现的 PBX 开发。(这是一个附加改进,因此我的原始 PBX 在没有此功能的情况下也能正常工作,但同时,拥有语音邮件服务可以使其更专业。)因此,作为第一步,请学习我之前的 PBX 教程。它包含了您开始此项目所需的所有初始步骤。本文可在以下位置找到:
如何使用 C# 构建一个简单的 SIP PBX,并扩展拨号计划功能:https://codeproject.org.cn/Articles/739075/How-to-build-a-simple-SIP-PBX-in-Csharp-extended-w
- 我的 PBX 是用 C# 编写的,因此您需要一个支持该语言的开发环境,例如 Microsoft Visual Studio。
- 您的 PC 上还需要安装 .NET Framework。
- 要定义默认的 PBX 行为,您需要将一些 VoIP 组件 添加到 Visual Studio 的引用中。由于我已经在进行更多 VoIP 开发时使用了 Ozeki VoIP SIP SDK,因此我使用了该 SDK 的后台支持。所以也需要在您的 PC 上安装它。
编写代码
在实现 PBX 后,您将拥有一个带有拨号计划功能的电话系统。要创建语音邮件服务,首先需要一个虚拟分机,这需要在原始 PBX 代码中进行一些修改。
PBX 类中的修改
如您在**代码示例 1** 中所见,PBX 类的新构造函数包含一个扩展容器。此工具负责管理虚拟分机。由于有扩展处理程序,您可以使用特定的 SIP 帐户创建任何虚拟分机。
protected override void OnStart()
{
Console.WriteLine("PBX started.");
var callManager = GetService<ICallManager>();
callManager.SetDialplanProvider(new MyDialplanProvider(userInfoContainer));
pbxCallFactory = GetService<IPBXCallFactory>();
var extensionContainer = GetService<IExtensionContainer>();
var rootPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
var voiceMailRootPath = Path.Combine(rootPath, "Records");
extensionContainer.TryAddExtension(new VoiceMailExtension(new Account("800", "800", localAddress), pbxCallFactory, voiceMailRootPath));
SetListenPort(TransportType.Udp, 5060);
Console.WriteLine("Listened port: 5060(UDP)");
base.OnStart();
}
代码示例 1:PBX 类的新构造函数
拨号计划提供程序类中的修改
需要此步骤是因为拨号计划规则应包含调用语音邮件分机的情况(**代码示例 2**)。语音邮件将在两种情况下应答呼叫:
- 第一种情况是当分机未应答呼叫时。在这种情况下,PBX 将呼叫重定向到语音邮件分机。此分机将应答呼叫并自动录制来电者的语音留言。
- 另一种情况是当一个终端直接呼叫语音邮件分机以收听已录制的语音邮件时。
public RoutingRule GetRoutingRule(ISIPCall caller, DialInfo callee, SingleRoutingRule.CalleeState calleeState)
{
if (calleeState != SingleRoutingRule.CalleeState.Calling) //busy or notfound
{
if (callee.UserName == "800" && calleeState != SingleRoutingRule.CalleeState.Calling) // VoiceMail is not available
return null;
UserInfo userInfo;
if (userInfoContainer.TryGetUserInfo(callee.UserName, out userInfo))
return new SingleRoutingRule("800", caller.Owner.Account.UserName, callee.UserName);
return null;
}
代码示例 2:拨号计划提供程序类中的修改
语音邮件分机的实现鉴于语音邮件是一个虚拟分机,它需要在实现 IExtension
接口的类中进行定义。语音邮件分机应能够处理联系信息,同时系统需要知道哪个分机想要收听消息(**代码示例 3**)。
class VoiceMailExtension : IExtension
{
List<VoiceMailCallHandler> voiceMailCallHandlers;
IPBXCallFactory pbxCallFactory;
string voiceMailRootPath;
public VoiceMailExtension(Account account, IPBXCallFactory pbxCallFactory, string voiceMailRootPath)
{
ContactInfo = new ContactInfo();
Account = account;
this.pbxCallFactory = pbxCallFactory;
this.voiceMailRootPath = voiceMailRootPath;
voiceMailCallHandlers = new List<VoiceMailCallHandler>();
}
代码示例 3:虚拟分机的定义
现在需要 pbxCall
对象来处理特定分机的呼叫。**代码示例 4** 显示了 pbxCall
对象的实现。
public ISIPCall CreateCall()
{
var pbxCall = pbxCallFactory.CreateIncomingPBXCall(this, SRTPMode.None);
var voiceMailCallHandler = new VoiceMailCallHandler(pbxCall, Account.UserName, voiceMailRootPath);
voiceMailCallHandlers.Add(voiceMailCallHandler);
voiceMailCallHandler.Completed += HandlerCompleted;
return pbxCall;
}
代码示例 4:创建 pbxCall
对象
如您在**代码示例 5** 中所见,当前的语音邮件呼叫是在单独的类中处理的。因此,应订阅呼叫状态已更改事件(在类的构造函数中实现)。
class VoiceMailCallHandler
{
IPBXCall pbxCall;
string extensionUserName;
IVoiceMailCommand voiceMailCommand;
string voiceMailRootPath;
public VoiceMailCallHandler(IPBXCall pbxCall, string extensionUserName, string voiceMailRootPath)
{
this.extensionUserName = extensionUserName;
this.voiceMailRootPath = voiceMailRootPath;
this.pbxCall = pbxCall;
((ICall)this.pbxCall).CallStateChanged += call_CallStateChanged;
}
代码示例 5:语音邮件呼叫的处理
在**代码示例 6** 中,您可以看到如何实现呼叫状态更改的事件处理程序方法。这在有来电的两种不同情况下是必要的:
- 第一种是当呼叫者直接呼叫语音邮件分机的电话号码时。在这种情况下,将播放语音邮件以供收听。这在
VoiceMailPlayCommand
类中实现。 - 当呼叫者呼叫了一个未应答的扩展并且呼叫被重定向到语音邮件分机时,呼叫者可以使用
VoiceMailRecordCommand
类的功能来留下消息。
如果出现任何问题(例如缺少呼叫者或被呼叫者数据),则呼叫将被拒绝。
void call_CallStateChanged(object sender, VoIPEventArgs<CallState> e)
{
if (e.Item == CallState.Ringing)
{
if(pbxCall.CustomTo == null || pbxCall.CustomFrom == null)
{
pbxCall.Reject();
return;
}
if (pbxCall.CustomTo == extensionUserName)
{
voiceMailCommand = new VoiceMailPlayCommand(pbxCall, voiceMailRootPath);
voiceMailCommand.Completed += ActionCompleted;
}
else
{
voiceMailCommand = new VoiceMailRecordCommand(pbxCall, voiceMailRootPath);
voiceMailCommand.Completed += ActionCompleted;
}
voiceMailCommand.Execute();
}
}
代码示例 6:如何实现呼叫状态更改的事件处理程序方法
正如 **代码示例 7** 所示,语音邮件的行为实现在两个单独的类中,这两个类都继承自 IVoiceMailCommand
。
class VoiceMailPlayCommand : IVoiceMailCommand
代码示例 7:语音邮件行为
当您收听您的语音邮件时,播放将以文本转语音消息开始,通知您有多少条消息等待收听。此文本转语音过程由该类的构造函数指定。任何其他功能都在其他方法中定义(**代码示例 8**)。
public VoiceMailPlayCommand(IPBXCall call, string voiceMailRootPath)
{
extensionVoiceMailPath = Path.Combine(voiceMailRootPath, call.CustomFrom);
messageFormat = "You have {0} new message";
this.call = call;
mediaConnector = new MediaConnector();
textToSpeech = new TextToSpeech();
phoneCallAudioSender = new PhoneCallAudioSender();
phoneCallAudioSender.AttachToCall(call);
mediaConnector.Connect(textToSpeech, phoneCallAudioSender);
this.call.CallStateChanged += CallStateChanged;
textToSpeech.Stopped += TextToSpeechCompleted;
}
代码示例 8:播放开始时的文本转语音过程如果在收听过程中有来电,语音邮件分机将自动接听(**代码示例 9**)。
public void Execute()
{
call.Accept();
}
代码示例 9:消息收听期间的自动呼叫接听
呼叫接听后,呼叫状态将变为 InCall
,文本转语音引擎将自动朗读介绍消息(**代码示例 10**)。
void CallStateChanged(object sender, VoIPEventArgs<CallState> e)
{
if (e.Item.IsInCall())
textToSpeech.AddAndStartText(string.Format(messageFormat, GetVoiceMails().Count));
else if (e.Item.IsCallEnded())
CleanUp();
}
代码示例 10:接听呼叫后,介绍消息将自动朗读
文本转语音对象应订阅 Completed 事件。构造函数会完成此操作。当介绍消息朗读完毕后,将发生 Completed 事件并调用事件处理程序。在朗读完待听消息的数量后,语音邮件分机开始播放第一条消息。如果没有更多待听消息,则呼叫结束(**代码示例 11**)。
void TextToSpeechCompleted(object sender, EventArgs e)
{
var voiceMails = GetVoiceMails();
if (voiceMails.Count == 0)
call.HangUp();
else
PlayVoiceMail();
}
代码示例 11:语音邮件分机开始播放待听的第一条消息
重放语音邮件是一个递归过程。您可以在**代码示例 12** 中看到,如果至少还有一条消息,它将被播放,然后已播放的消息将被删除。
void TextToSpeechCompleted(object sender, EventArgs e)
{
var voiceMails = GetVoiceMails();
if (voiceMails.Count == 0)
call.HangUp();
else
PlayVoiceMail();
}
代码示例 12:重放语音邮件是一个递归过程
**代码示例 13** 包含语音邮件分机删除已播放消息时调用的代码。此方法选择被设置为先前当前消息的文件并将其删除。
void DeletePlayedVoiceMail()
{
try
{
if (currentVoiceMailPath == null)
return;
if(waveStreamPlayback == null)
return;
mediaConnector.Disconnect(waveStreamPlayback, phoneCallAudioSender);
waveStreamPlayback.Dispose();
File.Delete(currentVoiceMailPath);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
代码示例 13:重放语音邮件是一个递归过程
现在我们来看 VoiceMailRecordCommand
类(**代码示例 14**),当来电被重定向到语音邮件分机(因为被叫方未应答)时使用该类。VoiceMailRecordCommand
类也实现 IVoiceMailCommand
接口并自动接听呼叫。如下所示,构造函数初始化了一个文本转语音读取器,该读取器向呼叫者朗读一条简单的信息性消息。
public VoiceMailRecordCommand(IPBXCall call, string voiceMailRootPath)
{
extensionVoiceMailPath = Path.Combine(voiceMailRootPath, call.CustomTo);
mediaConnector = new MediaConnector();
phoneCallAudioReceiver = new PhoneCallAudioReceiver();
phoneCallAudioReceiver.AttachToCall(call);
phoneCallAudioSender = new PhoneCallAudioSender();
phoneCallAudioSender.AttachToCall(call);
textToSpeech = new TextToSpeech();
textToSpeech.AddText(string.Format("{0} is not available. Please leave a message after the beep.", call.CustomTo));
mediaConnector.Connect(textToSpeech, phoneCallAudioSender);
this.call = call;
this.call.CallStateChanged += CallStateChanged;
textToSpeech.Stopped += TextToSpeechCompleted;
}
代码示例 14:VoiceMailRecordCommand
类的构造函数
“哔”声会在文本转语音引擎朗读完信息性消息后播放。此声音可以通过使用 DTMF 信令来实现(**代码示例 15**)。
private void SendBeep()
{
call.StartDTMFSignal(VoIPMediaType.Audio, DtmfNamedEvents.Dtmf0);
Thread.Sleep(500);
call.StopDTMFSignal(VoIPMediaType.Audio, DtmfNamedEvents.Dtmf0);
}
代码示例 15:“哔”声的实现
**代码示例 16** 显示语音邮件录音机自动接听呼叫并播放信息性消息和“哔”声。此功能在以下事件处理程序方法中实现:CallStateChange
和 TextToSpeechCompleted
。
void CallStateChanged(object sender, VoIPEventArgs<CallState> e)
{
if (e.Item.IsInCall())
textToSpeech.StartStreaming();
if(e.Item.IsCallEnded())
CleanUp();
}
void TextToSpeechCompleted(object sender, EventArgs e)
{
SendBeep();
RecordVoiceMail();
}
代码示例 16:CallStateChange
和 TextToSpeechCompleted
事件处理程序方法
最后,让我们看一下录制的文件的处理。当前的语音邮件录制在 RecordVoiceMail
方法中实现,该方法使用 WaveStreamRecorder
工具以 .wav 格式录制消息。所有录制的音频文件将存储在该文件夹中,文件名以被叫分机命名。文件名包含呼叫者的姓名(**代码示例 17**)。
private void RecordVoiceMail()
{
if(!Directory.Exists(extensionVoiceMailPath))
Directory.CreateDirectory(extensionVoiceMailPath);
var currentDate = DateTime.Now.ToString("yyyy_dd_mm_hh_ss");
var fileName = string.Format("{0}_{1}.wav", call.DialInfo.UserName, currentDate);
var filePath = Path.Combine(extensionVoiceMailPath, fileName);
waveStreamRecorder = new WaveStreamRecorder(filePath);
mediaConnector.Connect(phoneCallAudioReceiver, waveStreamRecorder);
waveStreamRecorder.StartStreaming();
}
代码示例 17:录制文件的处理
摘要
总而言之,使用 VoIP SDK(及其 VoIP 组件)可以非常简单地为您现有的 VoIP PBX 扩展特定功能。语音邮件——尽管这是一项出色的呼叫管理选项——并不是唯一可以集成到您的 PBX 中的附加功能。如果您学习了我上面的提示以及引用的附加文章和教程,您可以根据您的具体需求轻松开发更高级的功能(例如呼叫队列或会议室等)。
参考文献
有用资源(初始开发任务)
- 此语音邮件解决方案基于我之前创建的 PBX 项目。要完成与 PBX 开发相关的初始任务,请学习以下文章:
如何使用 C# 构建一个简单的 SIP PBX,并扩展拨号计划功能:https://codeproject.org.cn/Articles/739075/How-to-build-a-simple-SIP-PBX-in-Csharp-extended-w
理论背景
- 关于语音邮件服务:http://en.wikipedia.org/wiki/Voicemail
- 关于 IP PBX:http://en.wikipedia.org/wiki/IP_PBX
- 关于附加 PBX 改进:http://voip-sip-sdk.com/p_320-voip-pbx-systems-voip.html
下载所需软件
- 下载 Microsoft Visual Studio:http://www.microsoft.com/hu-hu/download/visualstudio.aspx?q=visual+studio
- 下载 .NET Framework:http://www.microsoft.com/hu-hu/download/details.aspx?id=30653