Getting Started with IPPhone

Requirements: IPWorks VoIP

Introduction

The IPPhone component can be used to implement a software-based phone utilizing modern Voice over Internet Protocol (VoIP) technology. This softphone can perform many different functions of a traditional telephone, such as making and receiving calls, performing blind and attended transfers, placing calls on hold, establishing and joining conferences, and more. This guide will cover the basics of performing such functions.

Contents

Registration

To begin, the first step is activating, or registering, the component. The Server, Port, User, and Password properties must be set to the appropriate values to register with your SIP server/provider. After these values are set, call the Activate method. If the component has successfully activated/registered, the Activated event will fire and the Active property will be set to true. The component will now be able to make and receive phone calls. For example:

ipphone1.OnActivated += (o, e) => { Console.WriteLine("Activation Successful"); }; ipphone1.User = "sip_user"; ipphone1.Password = "sip_password"; ipphone1.Server = "sip_server"; ipphone1.Port = 5060; // Default, 5061 is typical for SSL/TLS ipphone1.Activate();

Additionally, it's important to note that the registration of a SIP client will expire if not refreshed. The expiration time is negotiated with the server when registering. By default, the component will attempt to negotiate a value of 60 seconds. This value can be changed via the RegistrationInterval configuration. Note this is merely a suggestion to the server, and the server can change this accordingly. If the server does change this, after registration is complete, RegistrationInterval will be updated.

To prevent the registration from expiring, the component will refresh the registration within the DoEvents method, when needed. To ensure this occurs, we recommend calling DoEvents frequently. In a form-based application, we recommend doing so within a timer. For example, this could look something like:

private void timer1_Tick(object sender, EventArgs e) { ipphone1.DoEvents(); } private System.Windows.Forms.Timer timer1; timer1.Interval = 1000; timer1.Tick += new System.EventHandler(this.timer1_Tick); timer1.Enabled = true;

Note that in console applications, you must call DoEvents in a loop to provide accurate message processing, in addition to this case.

Audio Setup

While not required to function, you may set the microphone and speaker for the component to use during calls. First, you must call the ListMicrophones and ListSpeakers methods. Doing so will populate the appropriate Microphone and Speaker properties/collections. Once this is done, you can set these devices via the SetMicrophone and SetSpeaker methods given their device name. For example:

ipphone1.ListSpeakers(); ipphone1.ListMicrophones(); foreach (Speaker s in ipphone1.Speakers) { Console.WriteLine(s.Name); } foreach (Microphone m in ipphone1.Microphones) { Console.WriteLine(m.Name); } ipphone.SetSpeaker(ipphone1.Speakers[0].Name); ipphone.SetMicrophone(ipphone1.Microphones[0].Name);

Security

By default, the component operates in plaintext for both SIP signaling and RTP (audio) communication. To enable completely secure communication using the component, both SIPS (Secure SIP) and SRTP (Secure RTP) must be enabled.

Enable SIPS

To enable SIPS (Secure SIP, or SIP over SSL/TLS) the SIPTransportProtocol property must be set to 2 (TLS). The Port property will typically need to be set to 5061 (this may vary). Additionally, the SSLServerAuthentication event must be handled, allowing users to check the server identity and other security attributes related to server authentication. Once this is complete, the component can then be activated. All subsequent SIP signaling will now be secured. For example:

ipphone1.OnSSLServerAuthentication += (o, e) => { if (!e.Accept) { if (e.CertSubject == "SIPS_SAMPLE_SUBJECT" && e.CertIssuer == "SIPS_CERT_ISSUER") { e.Accept = true; } } }; // Enable SIPS ipphone1.SIPTransportProtocol = IpphoneSIPTransportProtocols.tpTLS; ipphone1.User = "sip_user"; ipphone1.Password = "sip_password"; ipphone1.Server = "sip_server"; ipphone1.Port = 5061; // 5061 is typical for SSL/TLS ipphone1.Activate();

Information related to the SSL/TLS handshake will be available within the SSLStatus event with the prefix [SIP TLS].

Enable SRTP

While the above process enables secure SIP signaling, this does not secure RTP (audio) communication. The RTPSecurityMode property can be used to specify the security mode that will be used when transmitting RTP packets. By default, this property is 0 (None), and RTP packets will remain unencrypted during communication with the remote party.

To ensure the audio data is encrypted and SRTP is enabled, the RTPSecurityMode property must be set to either of the following modes: 1 (SDES), or 2 (DTLS-SRTP). The selected mode will be used to securely derive a key used to encrypt and decrypt RTP packets, enabling secure audio communication with the remote party. The appropriate mode to use may depend on the service provider and configuration of a particular user. For example:

ipphone1.OnSSLServerAuthentication += (o, e) => { if (!e.Accept) { if (e.CertSubject == "SIPS_SAMPLE_SUBJECT" && e.CertIssuer == "SIPS_CERT_ISSUER") { e.Accept = true; } } }; ipphone1.RTPSecurityMode = IpphoneRTPSecurityModes.etSDES; // Enable SRTP (SDES) ipphone1.RTPSecurityMode = IpphoneRTPSecurityModes.etDTLS; // Enable SRTP (DTLS-SRTP) ipphone1.SIPTransportProtocol = IpphoneSIPTransportProtocols.tpTLS; ipphone1.User = "sip_user"; ipphone1.Password = "sip_password"; ipphone1.Server = "sip_server"; ipphone1.Port = 5061; // 5061 is typical for SSL/TLS ipphone1.Activate(); ipphone1.Dial("123456789", "", true);

Note it is highly recommended that the SIPTransportProtocol is set to 2 (TLS) when enabling SRTP. Additionally, if SRTP is enabled, the remote party must support the selected mode, otherwise no call will be established.

Managing Calls

All incoming and outgoing calls currently recognized by the component will be stored in the Call properties/collection. These connections will be initiated or accepted through the interface identified by the LocalHost and LocalPort properties.

Incoming Calls

After successful activation, incoming calls will be detected, and IncomingCall will fire for each call. Within this event, the Answer, Decline, and Forward methods can be used to handle these calls. For example:

iphone1.OnIncomingCall += (sender, e) => { iphone1.Answer(e.CallId); };

Outgoing Calls

To make an outgoing call, you must use the Dial method. This method takes three parameters: the user you wish to call, your caller ID (optional), and a boolean that determines whether the method will connect synchronously (True) or asynchronously (False). If set, the second parameter will cause P-Asserted-Identity headers (RFC 3325) to be sent in requests to the server. If left as an empty string, this header will not be sent. Dial will return a call identification string (Call-ID) that is unique to the call. After the method returns successfully, the call will be added to the Call properties/collection.

Synchronous Dial

If the third parameter of Dial is specified as true, the method will not return until the call has either been answered, declined, or ignored. Typically, three events will fire in the given order: OutgoingCall, DialCompleted, and CallReady. First, OutgoingCall will fire as soon as the invite process for the call has begun. After this process has been completed and the call has been answered, declined, or ignored, DialCompleted and CallReady will fire. At this point, audio can be transmitted. In the below example, we assume the call has been answered:

string callId = ""; bool connected = false; ipphone1.OnCallReady += (sender, e) => { connected = true; }; try { callId = ipphone1.Dial("123456789", "", true); } catch (IPWorksVoIPException e) { MessageBox.Show(e.Code + ": " + e.Message); } if (connected) { ipphone1.PlayText(callId, "Hello"); }

Asynchronous Dial

If the third parameter of Dial is specified as false, the method will immediately return with the call's CallId. Typically, three events will fire in the given order: OutgoingCall, DialCompleted, and CallReady. First, OutgoingCall will fire as soon as the invite, or dial, process for the call has begun. Before the remaining two events fire, it is possible for the CallId of this call to change. This can occur if call forwarding, or redirection, occurs. In this case, when wait is false, the value returned by Dial (and present in OutgoingCall) will be incorrect.

This above issue is taken care of in the DialCompleted event parameters. The OriginalCallId parameter will contain the original CallId returned by Dial. The CallId parameter will contain the updated and correct CallId. Along with this, other call details are specified within the event and can be used to determine if there were any issues within the dialing process. It is important to note that when wait is true, this method will return the correct CallId.

Lastly, if DialCompleted has fired with no errors (indicated by an ErrorCode of 0), CallReady will fire containing the updated CallId. It is important to note that this event will fire after the call has either been answered, declined, or ignored. If the call has been declined or ignored, this event will still fire, but the component will attempt to leave a voicemail. You can end the voicemail at any time using the Hangup (or HangupAll) method. In the below example, we assume the call has been answered and take care to update the returned value of Dial in case redirection occurs:

bool connected = false; string callId = ""; ipphone.OnDialCompleted += (sender, e) => { if (e.ErrorCode != 0) { MessageBox.Show(e.ErrorCode + ": " + e.Description); // Handle error } if (e.OriginalCallId != e.CallId) { callId = e.CallId; // Update callId if redirect occurred } }; ipphone.OnCallReady += (sender, e) => { connected = true; // If fired, we are ready to talk }; string callId = ipphone.Dial("123456789", "", false); ... ... ... // Somewhere else... if (connected) { ipphone.PlayText(callId, "Hello"); }

Transferring Calls

Ongoing calls can be transferred using the Transfer method. The component supports two types of transfers:

Basic Transfers

Basic transfers are very simple to perform. First, the user establishes a call with the number they will be transferring (transferee). After the call is established, the user can transfer the call to the appropriate number (transfer target). The call will then be removed. For example:

string callId = ipphone1.Dial("123456789", "", true) // Establish call with transferee, hold if needed //ipphone1.Hold(callId); ipphone1.Transfer(callId, "number");

Attended Transfers

Typically, attended transfers are used to manually check if the "number" they are transferring to (transfer target) is available for a call, provide extra information about a call, etc., before transferring. In addition to establishing a call with the transferee, the component must also establish a call with the transfer target. Once both calls are active, you may perform an attended transfer by calling Transfer at any moment. Afterward, a session will be established between both calls, and they will be removed. Note that Transfer must be used with the call ID of the call you wish to transfer (transferee) and the number of the call you wish to transfer (transfer target). For example:

string callId1 = ipphone1.Dial("123456789", "", true); // Establish call with Transferee, hold if needed //ipphone1.Hold(callId1); string callId2 = ipphone1.Dial("123456789", "", true); // Establish call with Transfer Target, hold if needed //ipphone1.Hold(callId2); ipphone1.Transfer(callId1, "number");

Note in these examples, the Hold method can be used to place a call on hold before a transfer. This is optional.

Playing Audio

The component supports three methods of playing audio to a call, using the PlayFile, PlayText, and PlayBytes methods. Note for each of these methods, the audio transmission will only occur when the call has connected and the CallReady event has fired. Additionally, only audio data with a sampling rate of 8 kHz and a bit depth of 16 bits per sample can be played (PCM 8 kHz 16-bit format). Also note that these methods are non-blocking, and will return immediately. The component can also handle playing audio to concurrent calls.

PlayFile can be used to play audio from a WAV file to a specific call. PlayText can also be used to play audio but will do so using Text-to-Speech. Once the audio has finished playing, the Played event will fire.

PlayBytes can be used to play audio but will do so in an event-based manner. The behavior of this method can be very different from the previous two methods. To start, the method takes three parameters, being the callId, bytesToPlay, and lastBlock. The callId parameter simply specifies the call to which the audio bytes will be played. The bytesToPlay parameter will contain the appropriate audio bytes that the component will send to the specified call. Internally, these bytes will be stored within a buffer.

The lastBlock parameter is a boolean indicating whether the component will expect further uses of PlayBytes. When true, this indicates that no additional bytes will be provided for this particular audio stream, and the Played event will fire once after the bytes have been played. Until this parameter is specified as true, the component will be considered to be playing audio. After being specified as true, users can start a new stream to the associated call ID with further uses of PlayBytes.

If the lastblock parameter is specified as true, this indicates that the component should expect more calls to PlayBytes. Once all bytes have played and the buffer is empty, Played will fire as expected and will continue firing until this parameter is set to true. Within Played, the user can provide further bytes to be played. For example:

Example: Playing audio from a stream MemoryStream playBytesStream = new MemoryStream(byteSource); ipphone1.PlayBytes("callId", new byte[0], false); ipphone1.OnPlayed += (o, e) => { if (e.Completed) { Console.WriteLine("Playing Bytes Completed"); } else { byte[] data = new byte[4096]; // Arbitrary length int dataLen = playBytesStream.Read(data, 0, data.Length); if (dataLen > 0) { byte[] newData = new byte[dataLen]; Array.Copy(data, newData, dataLen) // Normalize array ipphone1.PlayBytes(e.CallId, newData, false); } else { ipphone1.PlayBytes(e.CallId, null, true); } } }; Example: Playing a single audio block MemoryStream playBytesStream = new MemoryStream(byteSource); ipphone1.PlayBytes("callId", playBytesStream.ToArray(), true); ipphone1.OnPlayed += (o, e) => { Console.WriteLine("Done!"); // No further calls to PlayBytes are expected in this case }

Recording Audio

Ongoing calls can be recorded using the StartRecording method. The audio can be recorded directly to a WAV file by specifying the filename parameter. Additionally, if this parameter is not specified, the audio will be recorded internally and made available once the recording is finished. The recorded data will be available within the Record event.

Note in both scenarios, the recording will end either when the call is terminated, or the StopRecording method is called. The recorded audio will have a sampling rate of 8 kHz and a bit depth of 16 bits per sample (PCM 8 kHz 16-bit format).

Example: Using the 'Record' event MemoryStream recordStream = new MemoryStream(); ipphone1.StartRecording("callId", ""); ipphone1.OnRecord += (o, e) => { recordStream.Write(e.RecordedDataB, 0, e.RecordedDataB.Length); File.WriteAllBytes(recordFile, recordStream.ToArray()); };

Conferencing

This component also supports conferencing. A call can join a conference using the JoinConference method, passing in the call ID of the call and a custom conference ID. If the conference ID does not exist, then a new conference will be created given this ID. Other calls can then join the existing conference with this same ID.

To monitor existing conferences, the ListConferences method will return a string containing all ongoing conferences and calls within each conference. This value will have the following format:

  • ConferenceId_1: CallId_1, CallId_2
  • ...
  • ConferenceId_n: CallId_3, CallId_4, CallId_5, ...

At any moment, a call can be removed from the conference using the LeaveConference method. If the user is the last call within the conference, then the conference will be removed.

Call Termination

Ongoing calls are terminated by passing the CallId to the Hangup method, or by calling HangupAll, which will terminate all ongoing calls. When a call has ended (by either party), the CallTerminated event will fire. It's important to note that in the case where an outgoing call is never answered, the component will attempt to leave a voicemail. The voicemail will end once Hangup or HangupAll is called, and CallTerminated will fire.

We appreciate your feedback.  If you have any questions, comments, or suggestions about this article please contact our support team at kb@nsoftware.com.