High performance VoIP SDK for .Net developers

VoIP SIP SDK

Source code for building C# autodialer using Ozeki VoIP SIP softphone source code

Download: Autodialer.zip

This page is entitled to be a source code explanation for help you build an autodialer in C# using the Ozeki VoIP SIP softphone source code.

The sample code

The sample application can be divided into four main parts:

MainForm.cs
It displays the user interface of the application.

Autodialer.cs
It is responsible for call setup and after setup it plays the audio files.

CallItem
It represents call information (CallItem). CallItem is displayed on the user interface by data binding. The main advantage of this is the fact that even complex data structures can be displayed with minimal coding. .Net provides a built-in support for data binding.

Datagrid
The data binding for Datagrid is run in the MainForm_Load method in the following way:

void MainForm_Load(object sender, EventArgs e)
{
    #region DataBinding

    dataGridView1.AutoGenerateColumns = false;
    // Create the list of CallItem objects which will supply data to the DataGridView.
    source = new SyncBindingList<CallItem>(SynchronizationContext.Current);
    source.AddingNew += (sender1, e1) => e1.NewObject = new CallItem();
    dataGridView1.DataSource = source;

    #endregion

    #region Configuration
    // ...
    #endregion

    #region Copyright
        // ...
    #endregion
}

You can provide any collections as the source of Datagrid but it is recommended to use a BindingList. With using BindingList it is possible to notify the user interface if, for example, the number of items has been modified. SyncBinding<T> calls deriving from BindingList<T> will be explained in details later.

In Datagrid it can be set which property of data source should be bound to the given columns. To see this, switch to MainForm.cs designer view by right clicking on the Datagrid and selecting the Edit Columns menu item (Figure 1).


Figure 1 - Edit columns

Here you can specify (beside the various properties of columns) which of the properties of data source items the columns should be bound to. This can be controlled by the DataPropertyName property (Figure 2).


Figure 2 - Edit columns

For making a two-way data binding (in other words, for update the user interface when the value of data type is changed), the INotifyPropertyChanged interface needs to be implemented. In this case, this will be done in the CallItem class.

class CallItem : INotifyPropertyChanged, IDataErrorInfo
{
        #region Properties
        // Implementation…
        #endregion

        #region INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;

        void OnPropertyChanged(string propertyName)
        {
                if(PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

        #region IDataErrorInfo implementation
        // See later
        #endregion
}

If the value of a property changes in the CallItem, users can be notified via the PropertyChanged event.

private string phoneNumber;
public string PhoneNumber
{
        get
        {
                return phoneNumber;
        }
        set
        {
                phoneNumber = value;
                OnPropertyChanged("PhoneNumber");
        }
}

IDataErrorInfo is responsible for validate the GUI of CallItem items. If one of the values is changed in the user interface and the given type (CallItem) implements the IDataErrorInfo interface, the gride validates the given value by providing the property name in the indexer.

If the phone number is not entered correctly, an error message is returned ("Phone number cannot be empty). If the phone number is correct, then an empty string is returned.

#region IDataErrorInfo implementation
public string this[string columnName]
{
    get
    {
        if (columnName == "PhoneNumber")
        {
            if (string.IsNullOrWhiteSpace(PhoneNumber))
            {
                Error =  "Phone number cannot be empty";
                return Error;
            }

        }

        Error = string.Empty;
        return Error;
    }
}

public string Error { get; set; }

#endregion

The following code section will be executed in the MainForm.cs class after the sample program is started, the phone numbers are entered and you clicked on Start button:

void btnStart_Click(object sender, EventArgs e)
{
    if (!(phoneLineState == PhoneLineState.NoRegNeeded
           || phoneLineState == PhoneLineState.RegistrationSucceeded))
    {

        MessageBox.Show(string.Format("Phone line state is not valid: {0}", phoneLineState));
        return;
    }

    dataGridView1.Columns["phoneNumberColumn"].ReadOnly = true;

    btnStart.Enabled = false;
    btnStop.Enabled = true;

    autodialer.StartProcessing(source);
}

When the user clicks on the Start button but the registration to the PBX is still not successful, the user will be notified. If the registration has been successful, CallItems are started to be processed in the Autodialer class.

 public void StartProcessing(IEnumerable<CallItem> callItems)
        {
            #region Validation
            // Implementation…
            #endregion

            #region Creates a new task to process call items
            task =  Task.Factory.StartNew(()=>
                    {
                        using(var timer = new Timer())
                        {
                            #region Progress timer
                            // See later
                            #endregion

                            foreach (var cItem in callItems)
                            {
                               callItem = cItem;
                               if(string.IsNullOrEmpty(callItem.PhoneNumber))
                                   continue;

        //If the user clicking on the stop button, then the field set to be false.
                               if (!started)
                                   break;

                               call = softPhone.CreateCallObject(phoneLine, callItem.PhoneNumber);

                              #region Call events
                              // See later
                              #endregion

                              #region Waiting for the next phone call
                              // See later
                              #endregion
                            }

                            started = false;
                            OnProcessingCompleted();
                      }
                    });
            #endregion
        }

In the Autodialer class, start a new task that will handle calls. In this way, processing is done in a separated thread. It is important because if a process takes too long time in the GUI thread, it can cause the stoppage of GUI.

Since the update of call items are not realized in the GUI thread, we need the SyncBindingList. The task of SyncBindingList is to notify the GUI thread about changes.

After starting processing, the audio file to be played during the call will be read into the file stream.

The timer is responsible for storing the call process in the call related CallItem. This can be calculated by the ratio of the percentage of the already played audio data during the call and the size of the audio file.

#region Progress timer

timer.Interval = 1000;
timer.Elapsed += (sender, e) =>
                 {
                     if (call != null && call.CallState == CallState.InCall)
                     {
                                var percent = (int)(((double) waveFile.Stream.Position/(double) waveFile.Stream.Length)*100);
                                                callItem.Progress = percent;
                        if(percent >= 100) call.HangUp();
                     }
                 };

timer.Start();
#endregion

foreach (var cItem in callItems)
{
    callItem = cItem;

    if(string.IsNullOrEmpty(callItem.PhoneNumber))
        continue;

   //If the user clicking on the stop button, then the field set to be false.
    if (!started)
        break;

    call = softPhone.Call(phoneLine, callItem.PhoneNumber);

    #region Call events
    // See later
    #endregion

    #region Waiting for the next phone call
    // See later
    #endregion

}

After the initialization of timers each CallItem will be iterated after the initialization of timers. The next CallItem will be processed when the actual call is finished. Now I create a new instance of the class for the CallItem.

Subscribe to call events, e.g.: for receiving DTMF signals, the status of the call has been changed. Since the CallItem class realizes INotifyPropertyChanged interface, if a property of the CallItem is changed, the user will be instantly notified on the interface.

The waitingForNextCall and waitingToPickUp properties are responsible for the synchronization between calls. These two fields are the instances of the AutodResetEvent class. After starting the call, it waits for acceptance for at most 1 minute. If the call is not accepted within 1 minute or the called party ignore the connection, the actual call is hanged and the next CallItem starts to be processed.

If the call is accepted (the call state is changed to InCall), it is positioned back to the beginning of the file. The given thread waits at waitingfornextCall until the callstate is completed. Callstate is completed if the fileStream is played to the end in mediaTimer or the called party hand the call.

#region Call events
//Occurs when the current call has received dtmf event
call.DtmfReceived += (sender, e) => callItem.Answer = e.Item.Item2.Signal.ToString();

//Occurs when the current call has received dtmf event
call.CallStateChanged += (sender, e) =>
{
    callItem.CallState =
        e.Item.ToString();
    switch (e.Item)
    {
        case CallState.InCall:
            waitingToPickUp.Set();

            waveFile = new WaveStreamPlayback(wavPath);
            connector.Connect(waveFile, mediaSender);

            mediaSender.AttachToCall(call);

            waveFile.Start();

            break;
        case CallState.Completed:
            waitingForNextCall.Set();

            waveFile.Stop();
            connector.Disconnect(waveFile, mediaSender);
            mediaSender.Detach();

            break;
        case CallState.Rejected:
        case CallState.Error:
        case CallState.Cancelled:
            waitingToPickUp.Set();
            waitingForNextCall.Set();
            break;
    }
};

call.CallErrorOccured += (sender, e) =>
                         {
                             callItem.CallState = e.Item.ToString();
                             waitingToPickUp.Set();
                             waitingForNextCall.Set();
                         };
call.Start();
#endregion

#region Waiting for the next phone call
if (waitingToPickUp.WaitOne(TimeSpan.FromMinutes(1)))
    waitingForNextCall.WaitOne();
else
    call.HangUp();
#endregion

Possibilities for further development of the autodialer

The following list contains the functions that are not included in the sample program but there is a possibility to implement them with Ozeki VoIP SIP SDK. For further information please contact us at info@voip-sip-sdk.com.

These further functions can also be realized with Ozeki VoIP SIP SDK effectively:

  1. Handling more calls simultaneously.
  2. Playing back various audio messages.
  3. Support for Voice Activity Detection.
  4. Specify the exact call time.
  5. Run from Autodialer service (therefore calls will not be interrupted by e.g. the screensaver).

For checking a complete autodialer product with further functions please visit http://www.ozekidialer.com/!

Name:Softphone Example Program
Download:Autodialer.zip (14.9 MB)


For ordering Ozeki VoIP SIP SDK and getting licensing information please go to How to buy page! For more information please contact us at info@voip-sip-sdk.com!