Programmable Voice

  1. Home
  2. Docs
  3. Programmable Voice
  4. Tutorials
  5. Conferencing

Conferencing

Download

This tutorial is a walkthrough explanation of the Conference sample solution that can be downloaded from your demo dashboard.  If you haven’t already, sign up for a demo account and get 100 minutes of free call time for the next 30 days with the Voice Elements servers.

This tutorial covers how to write simple programmable voice conferencing applications using Voice Elements. It is important to note that this project is set up to run either as a windows service or as a Windows form.  While you are debugging, you will want to simply start the application using the default Windows form.  Once you are ready to deploy for production, we recommend that you install as a Windows Service. See the documentation for .net windows services here. Once you have the project downloaded, unzip it and open GettingStarted.sln with Visual Studio.

This application is already complete and ready to go, so you can test it out to see what it does before you start to read through the code to see how it works. The pass code to get into the conference is set to 123.  When you first compile the solution, it will automatically get the Voice Elements Client from NuGet.

Voice Elements MainCode

The core class of this project is IvrApplication. This class contains a lot of logic that sets up the application as a windows service so you can ignore a lot of the code in it for now. The most important method here is MainCode().

When the application is run, it starts a new thread which runs MainCode(). This connects to the Voice Elements servers in the cloud. Then loops indefinitely checking for new tasks to run, and inbound call events.

Note that Log.Write() is used frequently to log call progress and help with debugging. It is recommended that you continue to do this as you program your own Voice Elements applications.

The first thing MainCode() does is connect to the Voice Elements servers. This is done by constructing a new TelephonyServer object passing in server ip, username, and password as parameters. These values have already been generated for your account but you can change them in your Settings.settings file.

MainCode() also sets the CacheMode on the TelephonyServer object. ClientSession mode means that the server will stream and cache the files to and from your client machine. These files are flushed after you disconnect. Server mode means that the files reside on the server and will use the full path name to find them there. Note that Server mode can only be used on your own dedicated Voice Elements server.

After connecting to the server and setting its cache mode the new call event should be subscribed to. This sets a method to be called when an incoming call is received. In this example TelephonyServer_NewCall() is the method to be called on new incoming call events.

RegisterDNIS() is then called on the TelephonyServer to tell the server which phone numbers the application will handle. This method can be called with no parameters to instruct Voice Elements to handle calls from all phone numbers on your account. Otherwise you can specify numbers to handle as parameters.

MainCode() then sets everything on the TelephonyServer needed for sms. Private and public keypairs have already been generated for your account, located in Settings.settings. The API Keys can be found and changed in the customer portal.  The TelephonyServer is then subscribed to the TelephonyServer_SmsMessage() method for handling inbound texts and the TelephonyServer_SmsDeliveryReport() method for logging message delivery status.

try
{
    Log.Write("Connecting to: {0}", Properties.Settings.Default.PhoneServer);

    s_telephonyServer = new TelephonyServer("gtcp://" + Properties.Settings.Default.PhoneServer, Properties.Settings.Default.UserName, Properties.Settings.Default.Password);

    // CHANGE YOUR CACHE MODE HERE
    s_telephonyServer.CacheMode = VoiceElements.Interface.CacheMode.ClientSession;

    // SUBSCRIBE to the new call event.
    s_telephonyServer.NewCall += new VoiceElements.Client.NewCall(TelephonyServer_NewCall);
    s_telephonyServer.RegisterDNIS();

    s_telephonyServer.SmsMyPrivateKeyXml = Properties.Settings.Default.CustomerKeyPairXml;
    s_telephonyServer.SmsBorderElementsPublicKeyXml = Properties.Settings.Default.BorderKeyPairXml;
    s_telephonyServer.SmsMessage += TelephonyServer_SmsMessage;
    s_telephonyServer.SmsDeliveryReport += TelephonyServer_SmsDeliveryReport;

    // Subscribe to the connection events to allow you to reconnect if something happens to the internet connection.
    // If you are running your own VE server, this is less likely to happen except when you restart your VE server.
    s_telephonyServer.ConnectionLost += new ConnectionLost(TelephonyServer_ConnectionLost);
    s_telephonyServer.ConnectionRestored += new ConnectionRestored(TelephonyServer_ConnectionRestored);
}

Inbound Call

Let’s take a look at the logic for taking care of an inbound call. In the MainCode() of IvrApplication we set TelephonyServer_NewCall() to be called when a new phone call is received. This generates the ChannelResource which you can basically think of as the object that is the phone line. The ChannelResource class contains all of the methods and properties that you would expect to be able to perform with a phone line. The InboundCall class is used for handling all of the logic for inbound phone calls.

TelephonyServer_NewCall() constructs a new InboundConference object for which the TelephonyServer object and the ChannelResource object are provided as parameters. The RunScript() method is then called on the new InboundConference object. This method contains all of the logic for setting up and adding new callers to the conference call.

static void TelephonyServer_NewCall(object sender, VoiceElements.Client.NewCallEventArgs e)
{
    try
    {
        Log.Write("NewCall Arrival! DNIS: {0}  ANI: {1}  Caller ID Name: {2}", e.ChannelResource.Dnis, e.ChannelResource.Ani, e.ChannelResource.CallerIdName);

        // Handle The New Call Here

        InboundConference inboundConference = new InboundConference(s_telephonyServer, e.ChannelResource);
        inboundConference.RunScript();

    }
    catch (Exception ex)
    {
        Log.WriteException(ex, "IvrApplication::NewCall");
        e.ChannelResource.Disconnect();
        e.ChannelResource.Dispose();
    }
}

Outbound Call

Let’s look at how making an outbound call with Voice Elements works. The IvrApplication class contains the method MakeOutboundCall()  which is called when the button is clicked on the GUI. This project uses the OutboundCall class to handle the logic for outbound calls. This method first constructs a new OutboundCall object passing in the TelephonyServer object and the number that is to be called. The constructor creates a new ChannelResource to the TelephonyServer. A new thread is then started to call the RunScript() method on the new OutboundCall object.

public static void MakeOutboundCall(string number)
{
    OutboundCall outbound = new OutboundCall(s_telephonyServer, number);

    // Always spawn calls on new threads
    ThreadStart ts = new ThreadStart(outbound.RunScript);
    Thread t = new Thread(ts);
    t.Name = "Outbound";

    t.Start();
}

The RunScript() method contains all of the logic for making an outbound call. It first sets the OriginatingPhoneNumber property of the ChannelResource, which sets the outbound Caller ID. It then sets the MaximumTime property on the ChannelResource so that the call will fail after 30 seconds if it does not connect. To actually place the call, the Dial() method is called on the ChannelResource passing in the number to be called as a parameter. This method returns a DialResult property which is used to determine if the call connects or not. RunScript() then has logic to determine what to do if the call is answered or not. If the call is answered then the channel resource is connected to the conference. The call is then disconnected and all of the resources are cleaned up.

try
{
    // Use WriteWithId to differentiate between separate instances of the class
    Log.WriteWithId(m_channelResource.DeviceName, "OutboundCall Script Starting");
    Log.WriteWithId(m_channelResource.DeviceName, "Dialing {0}", m_numberToCall);

    // With this, the server will detect if a human or machine answers the phone
    // m_channelResource.CallProgress = CallProgress.AnalyzeCall;

    // You can display any outbound Caller ID phone number if needed (this is disabled for testing)
    m_channelResource.OriginatingPhoneNumber = Properties.Settings.Default.TestPhoneNumber;

    // Instruct the server to wait no more then 30 seconds for a connection
    m_channelResource.MaximumTime = 30;

    // Place the call
    DialResult dr = m_channelResource.Dial(m_numberToCall);

    Log.WriteWithId(m_channelResource.DeviceName, "The dial result for {0} was: {1}", m_numberToCall, dr);

    if (dr == DialResult.Connected)
    {
        InboundConference inboundConference = new InboundConference(m_telephonyServer, m_channelResource);
        inboundConference.RunScript();
    }
    else
    {
        Log.WriteWithId(m_channelResource.DeviceName, "Unexpected dial result, cancelling Call");

        if (dr == DialResult.OperatorIntercept && m_channelResource.GeneralCause == 402)
            Log.WriteWithId(m_channelResource.DeviceName, "You have ran out of minutes. Contact customer support to have more added");
    }
}

Inbound Conference

The InboundConference class is the important class to pay attention to for writing conferencing applications with Voice Elements. The RunScript() method contains most all of the logic for conferencing. This method first asks for a pass code that needs to be entered in order to join the conference. This is set up so that there can be 3 failed inputs but on the fourth fail the incoming call will be disconnected. Otherwise if the pass code is entered correctly then the program first checks to see if the conference as already been created or not. If it has yet to be created then the conference is created. The ChannelResource is then added to the Conference. Certain callers can be muted by calling Monitor() on the ConferenceResource instead of Add().

This demo contains the coach, pupil conference call feature. Where one of the phones in the conference can be a pupil who can hear the one coach, however a normal caller in the conference cannot hear the coach. In this example this is set up so that a specific pass code must be entered to join the conference as either a pupil or a coach. So the pass code 123 will put the caller in as normal, 456 will put the caller in as a pupil, and 789 will put the caller in as a coach. This class also keeps track of weather or not there is already a pupil or coach in the call so that there can’t be more than one of each.

try
{
    Log.Write("Starting Script");

    ChannelResource.Answer();
    VoiceResource.TerminationDigits = "ANY";

    int i = 0;

    string passcode = "";

    for (i = 0; i < 3; i++)
    {
        if (i == 0)
        {
            VoiceResource.PlayTTS("Please enter your passcode followed by the pound sign");
        }
        else
        {
            VoiceResource.PlayTTS("Invalid Conference ID. Please try again. Enter the passcode followed by the pound sign");
        }

        VoiceResource.GetDigits(5, 20, "#", 4, false);

        passcode = VoiceResource.DigitBuffer;
        VoiceResource.WipeDigitBuffer();

        if (passcode == "123" || passcode == "456" || passcode == "789")
        {
            Log.Write("User entered correct passcode");
            // You can add logic to do a database lookup instead!
            break;
        }
    }

    if (i >= 3)
    {
        // Disconnect the user because they exceeded the max attempts
        VoiceResource.PlayTTS("Goodbye!");
        ChannelResource.Disconnect();
        return;
    }

    lock (InboundConference.SyncVar)
    {
        // We'll create the conference if it doesn't exist. Otherwise we will add the user to the conference
        // If you would like to manage multiple conferences .NET Dictionaries come in handy, that way you can do a lookup to see if the conference they are trying to enter already exists.
        if (InboundConference.Conference == null)
        {
            // Get the Conference resource
            InboundConference.Conference = TelephonyServer.GetConference();

            // Subscribe to the conference changed event
            InboundConference.Conference.ConferenceChanged += new ConferenceChanged(Conference_ConferenceChanged);

            // Set the ConferenceNotifyMode to On so that we are notified when users enter or leave the conference
            InboundConference.Conference.ConferenceNotifyMode = ConferenceNotifyMode.On;
        }
    }

    // We will clamp DTMF tones so that way when we add a menu other users won't hear when they press digits
    ChannelResource.ConferenceAttributes.MemberToneClamp = true;

    // We will add echo cancellation to improve voice quality
    ChannelResource.ConferenceAttributes.EchoCancellation = true;

    switch (passcode)
    {
        case "123":
            ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Normal;
            ChannelResource.VoiceResource.PlayTTS("You are connected normaly");
            break;
        case "456":
            //check that there isn't already a pupil
            if (!hasPupil)
            {
                hasPupil = true;
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Pupil;
                ChannelResource.VoiceResource.PlayTTS("You are connected as a pupil");
            }
            else
            {
                ChannelResource.VoiceResource.PlayTTS("There is already a pupil");
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Normal;
                ChannelResource.VoiceResource.PlayTTS("You are connected normaly");
            }
            break;
        case "789":
            //check that there isn't already a coach
            if (!hasCoach)
            {
                hasCoach = true;
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Coach;
                ChannelResource.VoiceResource.PlayTTS("You are connected as a coach");
            }
            else
            {
                ChannelResource.VoiceResource.PlayTTS("There is already a coach");
                ChannelResource.ConferenceAttributes.ConfereeType = ConfereeType.Normal;
                ChannelResource.VoiceResource.PlayTTS("You are connected normaly");
            }
            break;
    }

    // Now we'll add the user to the conference
    InboundConference.Conference.Add(ChannelResource);

    // If you would like to add listen only (muted) participants you can user Monitors
    // Because the audio from monitors is not used for mixing in the conference you can create very large conferences if most of the participants are monitors
    //InboundConference.Conference.Monitor(ChannelResource);

    ConferenceStarted = true;

    // We will wait until the user disconnects before terminating the call
    m_TerminateCall.WaitOne();
    if (hasCoach)
    {
        hasCoach = false;
    }
    if (hasPupil)
    {
        hasPupil = false;
    }
}

It’s also important to take note of the ConferenceChanged event method.When the conference is created above the ConferenceChanged event is subscribed to the Conference_ConferenceChanged() method which logs the status of the conference and disposes the conference when everyone leaves.

void Conference_ConferenceChanged(ConferenceChangedEventArgs ccea)
{
    Log.WriteWithId(InboundConference.Conference.ConferenceName, "The conference changed. Participants: {0} Monitors: {1}", InboundConference.Conference.Participants.Count, InboundConference.Conference.Monitors.Count);

    // We want to check to see if there are participants. If there are not we will dispose of the conference.
    if (!ConferenceStarted)
    {
        return;
    }
    if (InboundConference.Conference.Participants.Count > 0)
    {
        return;
    }

    //Unsubscribe from the conference changed event
    InboundConference.Conference.ConferenceChanged -= new ConferenceChanged(Conference_ConferenceChanged);

    if (recorderResource != null)
    {
        StopRecording();
    }
    // Dispose the conference and set to null
    InboundConference.Conference.Dispose();
    InboundConference.Conference = null;
}

This demo also contains the logic for recording the voice conference. This is done using helper functions in the InboundConference class. RecordConference() is called when the Record button is pressed on the gui. This method gets a new VoiceResource from the TelephonyServer for recording the call. It then sets a dynamic filename, and the termination conditions. The new VoiceResource is then added to the conference as a Monitor and begins to Record.

CheckConference() simply returns whether there is a conference or not so that you can’t click the Record button unless there is a conference to record.

StopRecording() just stops the recording and disposes of the VoiceResource.

public static void RecordConference()
{
    if (InboundConference.Conference != null)
    {
        recorderResource = TelephonyServer.GetVoiceResource();

        string filename = "VM_" + DateTime.Now.ToString("yyMMddhhmmss") + ".wav";

        recorderResource.TerminationDigits = "";
        recorderResource.MaximumTime = 1200;
        recorderResource.MaximumSilence = 1200;

        InboundConference.Conference.Monitor(recorderResource);

        Log.Write("Call is recording to " + filename);

        recorderResource.Record(filename);
    }
}

public static bool CheckConference()
{
    if (InboundConference.Conference == null)
    {
        return false;
    }
    return true;
}

public static void StopRecording()
{
    recorderResource.Stop();
    recorderResource.Dispose();
    Log.Write("Recording has stopped");

    frmInteractive._mainForm.setButtonText("Record");
}
Was this article helpful to you? Yes 13 No

How can we help?