How to improve your VoIP PBX with voicemail functionality in C#
This guide demonstrates how to build voicemail service into your VoIP PBX in C# in order to manage your calls more effectively.
Introduction
Accepting the the partners’ incoming calls has a major significance is business world. However, there are many cases when you are unable to accept your customers’ calls: for instance if you are out of the office, you are on holiday or it is weekend. Voicemail systems allow your customers to leave a message for your company by recording their voice, then later one of the employees can listen the recordings back.
Background
Two months ago I have created a simple SIP PBX that can be used as a corporate phone system. At the end of the article I have mentioned that I was working on a voicemail solution in order to improve my PBX. In this brief article I would like to describe this voicemail project while explaining how to build a voicemail feature into your existing PBX.
Prerequisites
This voicemail project is based on my previously implemented PBX development. (This is an additional improvement, so my original PBX is viable without this feature, but at the same time it can be more professional completed with voicemail service.) For this reason, as a first step, please study my previous PBX tutorial. It contains all the initial steps you need to do for starting this project. This article can be found here:
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
- My PBX has been written in C#, so you need a development environment supporting this language, e.g. Microsoft Visual Studio.
- .NET Framework installed on your PC is also needed.
- For defining the default PBX behaviour, you need to add some VoIP components to the references in Visual Studio. As I have already used Ozeki VoIP SIP SDK for more VoIP developments, I used the background support of this SDK. So it needs to be installed on your PC, as well.
Writing the code
After implementing the PBX, you have a phone system with dial plan functionality. To create a voicemail service, first you need a virtual extension that requires some modifications in the original PBX code.
Modifications in the
PBX class
As you can see in Code example 1, the new constructor of the PBX class contains an extension container. This tool is resposible for the management of the virtual extension. Due to the extension handler you can create any virtual extensions using a specific SIP account.
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();
}
Code example 1: The new constructor of the PBX class
Modifications in the dial
plan provider class
This step is needed because the dial plan rules should contain the cases when the voicemail extension is called (Code example 2). The voicemail will answer a call in two cases:
- The first one is when an extension does not answer a call. In this case the PBX redirects the call to the voicemail extension. This extension will answer the call and record the caller’s voice message automatically.
- The other case occurs when an endpoint calls the voicemail extension directly in order to listen to the recorded voicemails back.
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;
}
Code example 2: Modifications in the dial plan provider class
The implementation of the voicemail extensionConcerning to the fact that the voicemail is a virtual
extension, it needs to be defined in a class that implements the IExtension
interface.
The voicemail extension should be able to handle the contact information while
the system needs to know which extension wants to listen the messages back (Code
example 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>();
}
Code example 3: The definition of the virtual extension
Now there is a need for pbxCall
objects that handle the calls of the particular
extensions. Code example 4 shows the implementation of the pbxCall
object.
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;
}
Code example 4: Creating the pbxCall
object
As you can see on Code example 5, the current voicemail calls are handled in a separate classes. Therefore, the calls should be subscribed for the call state changed event (it is implemented in the constructor of the class).
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;
}
Code example 5: Handling of the voicemail calls
In Code example 6 you can see how to implement the event handler method of the call state change. This is necessary in two different cases when there is an incoming call:
- The first one is when the caller calls the phone
number of the voicemail extension directly. In this case the voicemail will be
played for listening. This is implemented in the
VoiceMailPlayCommand
class. - When the caller called an extension that did not
answer and the call was redirected to the voicemail extension, the caller can
leave a message by using the
VoiceMailRecordCommand
class functionalities.
In case of any problems (such as missing caller or callee data) the call is rejected.
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();
}
}
Code example 6: How to implement the event handler method of the call state change
As Code example 7 shows, the behaviour of the voicemail
is implemented in two separate classes, both of them derived from IVoiceMailCommand
.
class VoiceMailPlayCommand : IVoiceMailCommand
Code example 7: The voicemail behaviour
When you listen to your voicemails back, the playback starts with a text-to-speech message that informs you about the number of messages to be listened. This text-to-speech process is specified by the constructor of this class. Any other features are defined in other methods (Code example 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;
}
Code example 8: Text-to-speech process at the beginning of the playback If there is an incoming call during the listening, it will be accepted automatically by the voicemail extension (Code example 9).
public void Execute()
{
call.Accept();
}
Code example 9: Automated call accepting during the message listening
When the call has accepted, the state of the call changes to InCall
and
the introduction message will be read automatically by the text-to-speech
engine (Code example 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();
}
Code example 10: After accepting the call, the introduction message will be read automatically
The text-to-speech object should be subscribed for the Completed event. The constructor completes this. When the introduction messgae reading has done, the Completed event occurs and the event handler will be invoked. After reading the number of messages to be heard, the voicemail extension starts to play the first message. If there are no more messages waiting, the call is finished (Code example 11).
void TextToSpeechCompleted(object sender, EventArgs e)
{
var voiceMails = GetVoiceMails();
if (voiceMails.Count == 0)
call.HangUp();
else
PlayVoiceMail();
}
Code example 11: The voicemail extension starts to play the first message waiting
Replaying the voicemails is a recursive process. You can see on Code example 12 that if there is at least one message left that will be played and then the played message will be deleted.
void TextToSpeechCompleted(object sender, EventArgs e)
{
var voiceMails = GetVoiceMails();
if (voiceMails.Count == 0)
call.HangUp();
else
PlayVoiceMail();
}
Code example 12: Replaying the voicemails is a recursive process
Code example 13 contains the code that is called when the voicemail extension deletes a played message. This method chooses the file that was set as the previous current message and deletes it.
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);
}
}
Code example 13: Replaying the voicemails is a recursive process
Now let’s see the VoiceMailRecordCommand
class (Code
example 14) that is used when the incoming call has been redirected to the
voicemail extension because the called party didn’t accept that. The VoiceMailRecordCommand
class also implements the IVoiceMailCommand
interface and automatically accepts
the call. As it can be seen below, the constructor initializes a text-to-speech
reader that reads out a simple informative message to the caller.
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;
}
Code example 14: The constructor of the VoiceMailRecordCommand
class
The ’beep’ is played after the informative message read out by the text-to-speech engine. This sound can be implemented by using DTMF signalling (Code example 15).
private void SendBeep()
{
call.StartDTMFSignal(VoIPMediaType.Audio, DtmfNamedEvents.Dtmf0);
Thread.Sleep(500);
call.StopDTMFSignal(VoIPMediaType.Audio, DtmfNamedEvents.Dtmf0);
}
Code example 15: The implementation of ’beep’
Code example 16 shows that the voicemail recorder accepts the call and
plays the informative message and the ’beep’ automatically. This feature is
implemented in the following event handler methods: CallStateChange
and 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();
}
Code example 16: The CallStateChange
and TextToSpeechCompleted
event handler methods
And finally, take a look at the recorded files. The
current voicemail recording is implemented in the RecordVoiceMail
method that records
the messages in .wav format by using the WaveStreamRecorder
tool. All the
recorded audio files will be stored in that folder by the name of the called
extension. The file name contains the name of the caller (Code example 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();
}
Code example 17: Handling the recorded files
Summary
To sum it up, extending your existing VoIP PBX with specific features can be really simple by using a VoIP SDK (and its VoIP components of course). Voicemail – however this is an great call management option – is not the only one extra feature that can be built into your PBX. If you study my tip above and the referenced additional articles and tutorials, you can easily develop more advanced functionalities (such as call queue or conference room, etc.) based on your specific needs.
References
Useful source (initial development
tasks):
- This voicemail solution is based on my previously created PBX project. To achieve the initial tasks related to the PBX development, please study the following article:
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
Theoretical background:
- About voicemail services: http://en.wikipedia.org/wiki/Voicemail
- About
IP PBXs: http://en.wikipedia.org/wiki/IP_PBX
- About
additional PBX improvements: http://voip-sip-sdk.com/p_320-voip-pbx-systems-voip.html
Download the required software:
- Download
Microsoft Visual Studio:
http://www.microsoft.com/hu-hu/download/visualstudio.aspx?q=visual+studio
- Download
.NET Framework: http://www.microsoft.com/hu-hu/download/details.aspx?id=30653