Course 1 / Lecture 3

How to make and accept calls using SIP/VoIP

This tutorial demonstrates how to develop a softphone in C# using the VoIP-SIP-SDK to make and accept SIP/VoIP calls. It covers the complete SIP call flow including Invite, Trying, Ringing, OK, Ack, and Bye PDUs, along with detailed source code analysis for call management, device handling, and media connections. The guide also includes Wireshark capture examples to visualize SIP traffic and provides practical solutions for thread blocking issues in softphone applications.

Poster how to make and accept calls

Why do we need to make and accept calls in a VoIP Softphone?

Registering a softphone to a PBX only makes it reachable on the network — it doesn't yet let the softphone actually talk to anyone. Without dedicated logic for making and accepting calls, a registered phone would simply sit idle, unable to originate a call to another extension or respond when someone dials in. Handling calls means reacting to a sequence of states, an incoming call arriving, the call being answered, audio actively flowing, and eventually the call ending, and wiring the right MediaHandlers to the call object at each stage.

Why do we need call handling
Why do we need call handling

What is a SIP call?

A SIP call is a voice or video session established between two SIP endpoints through a series of signaling messages, INVITE, provisional responses like TRYING and RINGING, a final 200 OK, and an ACK, that negotiate who is calling whom and which audio codec and network port to use. Once this signaling exchange completes, the two endpoints exchange RTP media packets directly, carrying the actual voice data for the conversation. The call ends when either side sends a BYE, which the other side acknowledges with a 200 OK, tearing down the session and releasing the resources tied to it.

What is a SIP call
What is a SIP call

How does it work?

The diagram below illustrates a complete SIP call from start to finish between a caller softphone, a PBX acting as the SIP server, and a callee softphone. The caller sends an INVITE, which the PBX forwards to the callee while replying with a provisional 100 Trying; once the callee's phone starts ringing, a 180 Ringing response travels back to the caller so it can play a ringback tone. When the callee answers, a 200 OK carrying the agreed codec and RTP port travels back through the PBX, the caller confirms with an ACK, and audio then flows directly between the two softphones over RTP. Either side can end the call with a BYE, which the other side acknowledges with a final 200 OK, tearing the dialog down and releasing the call's resources.

How does it work
How does it work

Why do we need SIP Registration in a VoIP Softphone?

A PBX needs to know exactly where to send a call before it can deliver one. Phones don't have a fixed, permanent location on the network: they can be assigned a different IP address each time they connect, move between networks, or simply be switched off. SIP registration solves this by having each phone periodically tell the PBX "this is who I am, and this is where you can currently reach me." Without this step, the PBX would have no reliable way of finding a phone, and incoming calls to that extension would have nowhere to go.

SIP Invite PDU

A SIP INVITE PDU is the message a SIP user agent sends to initiate a VoIP call, specifying the caller, callee and the proposed media session in its headers and SDP body. Key headers include From and To, which identify the endpoints, Call-ID and CSeq for correlating this INVITE with its provisional and final responses, and Via to describe the transport path through the SIP network. The INVITE also typically carries a Contact header and codec/port information in SDP so the remote side can establish media streams once the call is answered and moves into the InCall state.

SIP Invite PDU
SIP Invite PDU

SIP Trying PDU

A SIP TRYING PDU is a provisional response the PBX or SIP server sends to indicate that it has received the INVITE and is currently attempting to locate and alert the called party. It uses the same Call-ID, CSeq and Via header values as the original INVITE so the caller can match this TRYING to the correct transaction. This response does not carry media information and serves purely as a signaling acknowledgment that call setup is in progress before any ringing or answer occurs.

SIP Trying PDU
SIP Trying PDU

SIP Ringing PDU

A SIP RINGING PDU is a provisional response sent when the destination user agent is being alerted, informing the caller that the remote phone is currently ringing. It maintains the dialog context using the same Call-ID and incremented CSeq, and includes To and From headers that identify the established call leg. Like TRYING, RINGING contains no SDP body by default, but it provides user feedback and can be used by the softphone to update its UI to a Ringing state.

SIP Ringing PDU
SIP Ringing PDU

SIP OK(Invite) PDU

A SIP OK (INVITE) PDU is the final 200-class response indicating that the called party has accepted the call, completing the INVITE transaction and establishing the SIP dialog. In addition to mirrored Call-ID, To, From, CSeq and Via headers, it typically includes a Contact header and an SDP body that confirms the negotiated codecs and media endpoints. Once this OK is received, the caller sends an ACK and both sides start media transmission, transitioning the softphone into the InCall state with active audio handlers attached to the call object.

SIP OK(Invite) PDU
SIP OK(Invite) PDU

SIP Ack PDU

A SIP ACK PDU is the confirmation message a SIP user agent sends after receiving a final 200 OK to an INVITE, signaling that the call setup has completed successfully. Key fields include the Call-ID and matching CSeq, which bind the ACK to the original INVITE transaction, along with the From and To headers that identify the established dialog endpoints. Although the ACK itself usually carries no SDP body, its successful delivery allows both sides to rely on the previously negotiated media parameters and transition fully into the InCall state with active audio handlers.

SIP Ack PDU
SIP Ack PDU

SIP Bye PDU

A SIP BYE PDU is the termination request sent by either party to end an established SIP call, signaling that the current dialog should be torn down. It reuses the existing Call-ID, To and From headers of the dialog and carries an appropriate CSeq value to ensure the BYE is processed in order within that session. When a BYE is sent or received, the softphone stops devices, detaches media handlers, unsubscribes from call events, and releases the call object to return to an idle state.

SIP Bye PDU
SIP Bye PDU

SIP OK(Bye) PDU

A SIP OK (BYE) PDU is the 200-class response confirming that the BYE request was successfully processed and that the call has now ended. It matches the BYE using the same Call-ID and CSeq, and includes standard To, From and Via headers to complete the transaction cleanly. After receiving this OK, both sides can safely release resources associated with the dialog, ensuring that media streams are stopped and no further signaling occurs for that call.

SIP OK(Bye) PDU
SIP OK(Bye) PDU

From this tutorial example you can learn the followings:

  • How to reach media handlers, handle devices and attach them to the call object
  • How to make and accept calls
  • How to handle the call's states and events

What knowledge would you need?

To fully understand this guide, you might need to study the following chapters first:

  • SIP registration: you can learn how to begin softphone developing and how to be able to register to a pbx with a sip account.
    Learn more...
  • Managing media handlers: you can find here examples and a simple guide about how to be able to use, connect and manage different media handlers, and how can you attach them to calls.
    Learn more...

Please note that, only the softphone's new elements will be introduced here. You can find the registration's process and the needed elements for that in the first example, called "SIP Registration".

Key source code

This snippet shows the Main() method in Program.cs, which is the entry point of the application. It initializes the softphone, subscribes to the registration state, call state, and incoming call events, then calls the registration method to connect to the PBX. Once registered, the program enters a loop asking the user to dial a number or accept an incoming call, keeping the console open until the user chooses to exit.

Main function source code
Main function source code

Source code analysis

To understand the source code, the softphone's functions and their usage, we need to discuss the details. In the "using section" we need to add some extra lines, like:

using Ozeki.Media.MediaHandlers;
using Ozeki.VoIP;
using Ozeki.VoIP.SDK;

Without these lines we would have to use the namespace information as a label for all tools of the SDK.

What is Softphone.cs used for?

This class is used to introduce how to declare, define and initialize a softphone, how to handle some of the Ozeki VoIP SIP SDK's events and how to use some of that's functions. In other words, we would like to create a "telephone software", which has the same functions (or much more), as an ordinary mobile (or any other) phone. In the Program.cs class we will use this class to create a new softphone, so we can use the functions, we can listen to the events placed here.

What objects does the Softphone class uses?

In the last example we've created a softphone and a phone line object from the ISoftPhone and the IPhoneLine interfaces. Now, we need to create some new objects:

IPhoneCall call;
Microphone microphone;
Speaker speaker;
MediaConnector connector;
PhoneCallAudioSender mediaSender;
PhoneCallAudioReceiver mediaReceiver;

The microphone will be connected to the mediaSender, and the speaker will be connected to the mediaReceiver, by the help of the connector object. The mediaSender and the mediaReceiver media handlers will be attached to the call object, this step is necessary if we would like to send and receive media data (for example: voice) during the communication. This sender and receiver object can be connected to different devices, media handlers as well (see below, at the connection's chapter). We also need a variable to indicate if we have an incoming call:

bool incomingCall;

In a constructor, we need to initialize these new objects, variables:

microphone = Microphone.GetDefaultDevice();
speaker = Speaker.GetDefaultDevice();
connector = new MediaConnector();
mediaSender = new PhoneCallAudioSender();
mediaReceiver = new PhoneCallAudioReceiver();

incomingCall = false;

Since our softphone has no incoming call yet, we need to set the variable to "false".

How to start and stop the devices?

There are some methods to help our later work, for example the methods to start and to stop the devices, to connect them with the other media handlers etc. Let's start with the device starter/stopper ones. In this example we are using a microphone and a speaker, but we could use several other devices as objects as well, for example: a webCamera.

if (microphone != null)
{
	microphone.Start();
}

As You can see, this method checks first if there is a device to be started or not. After that, it starts the existing device with a single command. The method which stops them works on a similar way, with the device's object's Stop() command.

How to connect the media handlers?

To send our voice through the microphone to the other client's speaker, we need to connect the correct devices. Please note that, it is possible to use several different media handlers with the Ozeki VoIP SIP SDK, for example there is a similar way to connect a WaveStreamPlayback or an MP3Playback object to the mediaSender object, to play a .wav or an .mp3 file into the call, as voice (and we can receive and record voices, videos as well, and there is much more). To connect the microphone object to the mediaSender object with the help of the connector object, we use the following lines:

if (microphone != null)
{
	connector.Connect(microphone, mediaSender);
}

The connector's first parameter is the source, and the second is the destination of the data stream. So, in the case of the speaker it looks like:

if (speaker != null)
{
	connector.Connect(mediaReceiver, speaker);
}

As it shows, the mediaReceiver sends the voice to the speaker. In this example we don't use, but You can find in the source code the "disconnector" method, which closes the connection between the two media handlers with the Disconnect() method. There is a way to close all of the connections with only one command:

connector.Dispose();

This line closes every connection. In this example we are connecting the media handlers only once, at the initialization of the softphone, because we don't need to disconnect them later.

How to subscribe to the call's events?

In the source code, You can find a method to help to subscribe to, and to unsubscribe the call from the call events. In this example we are subscribing to the event like this:

call.CallStateChanged += (call_CallStateChanged);

This event handle, if the call's state changes, or if an error occurs during the call. The details of this call state will be introduced below in other methods, this one only helps to subscribe to them. The method to unsubscribe from the event works similar to the subscriber (with the "-=" operator).

How to listen to incoming calls?

If we would like to be notified when there is an incoming call, we need to set an event at the softphone's initialization for this purpose:

softphone.IncomingCall += softphone_IncomingCall;

When the event notifies that there is a call waiting to be accepted, it sets the call object, and subscribes to the call's events with the previously written method's help. It also sets the incomingCall variable's value to "false", to indicate: there is no other call waiting.

How to handle errors, occurred during the call?

This example won't do anything to handle the error itself, only notifies the user if it happens. With further developments (for example to practice), You can tell the softphone how to react, when an error occurs during a call.

How to handle the call's states?

Maybe we can say, this is the main part of our softphone within the Softphone class. If the call's state changes, an event occurs. We are handling these events within the Softphone.cs and the Program.cs file as well. In the Softphone class we are telling the softphone what to do with the media handlers, with the events, the objects, so, we are setting the call's states to be similar to an ordinary phone's.

In this example we are using three states by their name:

CallState.Answered
CallState.InCall
CallState.Error

The "Answered" state occurs, when the call is being answered, and the "InCall" state occurs, when we are during a call, so if there is an active communication. To understand the difference between the two cases: the "Answered" state can occur only once per call, but we can enter into the same call again and again (for example if we put the call on hold, and then take the call off hold. You can find more information about this in the next example, called "Controlling the call").

In the "Answered" state, we have to:

  • Start the devices, with the help of the previously written method.
  • Attach the media handlers to the call.
  • Please note that, we need the devices to be connected to the media sender and receiver objects, but it's already done at the softphone's initialization.

In this example we need these lines to attach the media handlers to the call:

mediaReceiver.AttachToCall(call);
mediaSender.AttachToCall(call);

In the "InCall" state, we have to:

  • Start the devices, with the help of the previously written method.
  • Please note that, the media handlers are already set to the call.

In both cases, we were subscribing to the call's events when the call has been made (see below; at the call making chapter) or has been received (see above; when we noticed the incoming call).

We are using more states when the call has been ended: If the call ends, we can be notified about it as the IsCallEnded() method returns with a true value. For example it can occur, when an error occurs or the call enters into "Completed" state, but we can handle them all in once. When the call ends, we have to:

  • Stop the devices, with the help of the previously written method.
  • Detach the media handlers from the call
  • Unsubscribe from the call's events
  • Finally, set the call objects to "null"

In the Program class, we will tell the softphone what to do, when the call's state changes.

How to make a call?

Making a call means creating a call object, subscribing to the call's events, and then call the call object's Start() method. In the example the softphone checks first, if there is an already active call or not.

if (call == null)
{
call = softphone.CreateCallObject(phoneLine, numberToDial);
WireUpCallEvents();
call.Start();
}

If there is no active call, the we have to call the correct methods, illustrated above. To hang up the call, You can use the call object's HangUp() method.

How to accept a call?

As we could see, when the variable which indicates the incoming call has been set to "true", than there is a call to accept (so, our phone is "ringing "). In this example there is only text to notify if the phone is ringing. To make it to ring with voice, You have a lot of options provided by the SDK or just by most of the programming languages.

When we would like to accept a call, we need to set the variable's value to "false", to indicate that there are no incoming calls anymore, than we need to accept the call with the call object's Accept() method:

if (incomingCall == true)
{
	incomingCall = false;
	call.Answer();
}

To hang up the call, You can use the call object's HangUp() method.

Solving the Thread blockings

You can find a method, called "DispatchAsync(Action action)" in the source file. This method is used to solve the thread blockings, because the ReadLine() methods would block the thread.

What is Program.cs used for?

This class will introduce the usage of a softphone object, handles the console events, interacts with the user, and uses the opportunities provided by the Softphone class. In this example, the softphone can accept calls automatically, and can call the number, given by the user. We are handling everything with separated methods. These methods are communicating with each other, and making the source code more understandable and reusable.

How to initialize the softphone?

The first step is to initialize the softphone, we have created. To do this, we need to call an initializer method. We also need to subscribe to the call's events, to do this, we need the following lines:

mySoftphone = new Softphone();
mySoftphone.RegistrationStateChanged += mySoftphone_PhoneLineStateChanged;
mySoftphone.CallStateChanged += mySoftphone_CallStateChanged;
mySoftphone.IncomingCall += mySoftphone_IncomingCall;

We are listening to the phone line's and the call's states or errors and to the incoming calls. This method is being called in the Main() method.

How to handle errors, occurred during the call?

With subscribing to listen to the call state changes, we can notify the user about an occurred error with a simple message in this example.

How to handle incoming calls?

If there is an incoming call, our softphone notifies the user about that, and automatically accepts the call with the help of the softphone's previously written method.

How to handle the call's states?

We can reach the call's states, since we are listening to them. When the call ends, we are calling the method which is asking the user about a number to be dialled. There are more call states, but in this tutorial we will need only this one. You can learn more about call states in the third example, called "Controlling the call".

How to handle if the registration succeeded?

In the previous example (which introduced how to register to a PBX with a SIP account), we learnt how to handle the states of the phone line. We've notified the user about the success, and now we are telling the user to dial a number by calling a method to ask about it.

Asking the user about a number to be dialled

In this example, the phone asks the user about a number to be dialled and then stores that number as a string. We need to store the number as a string value, since it can contain special characters as well.

Capture SIP call make and accept traffic with Wireshark

The following video shows how to use Wireshark to capture and inspect SIP call make and accept traffic.

To begin capturing SIP traffic, open Wireshark and select the active network interface — typically the Ethernet adapter — from the capture interface list on the welcome screen. Once the correct interface is highlighted, click the blue shark-fin start button in the toolbar to begin recording all packets passing through that adapter in real time. At this stage Wireshark is capturing all protocol traffic indiscriminately, so the packet list will fill rapidly until a display filter is applied in the next step to isolate only SIP messages.

Start capturing the interface with SIP traffic in Wireshark
Figure 1 - Start capturing the interface with SIP traffic in Wireshark

With the live capture running, type sip into the display filter bar at the top of the Wireshark window and press Enter to restrict the packet list to Session Initiation Protocol messages only. This filter does not stop the capture of other protocols; it simply hides non-SIP frames from the view so that the INVITE, TRYING, RINGING, OK, ACK and BYE messages are easy to identify and inspect in sequence. Applying this filter before initiating or accepting a test call ensures that every SIP PDU exchanged during the call setup and teardown is immediately visible and color-coded in the packet list pane.

Filter for SIP packets in Wireshark
Figure 2 - Filter for SIP packets in Wireshark

The SIP INVITE packet appears as the first entry in the filtered list when a call is initiated, and clicking on it reveals the full PDU detail in the middle pane of Wireshark. Key header fields visible here include From and To identifying the calling and called parties, Call-ID uniquely labeling this dialog, CSeq sequencing the transaction, Via describing the transport hop, and an SDP body carrying the proposed codec list and RTP media port. Inspecting the INVITE in Wireshark confirms that the softphone has correctly formatted and transmitted the call setup request to the PBX before any provisional responses arrive.

View the SIP Invite request in Wireshark
Figure 3 - View the SIP Invite request in Wireshark

The 100 Trying response appears immediately after the INVITE in the filtered packet list, sent by the PBX to acknowledge that it has received the request and is processing it. Expanding the SIP layer in Wireshark shows that the Trying message echoes the same Call-ID, CSeq, Via and branch parameters as the original INVITE, allowing the user agent to match this provisional response to the correct transaction. No SDP body is present in the Trying, confirming it is a pure signaling acknowledgment with no media negotiation content, simply preventing the client from retransmitting the INVITE while the server works.

View the SIP Trying response in Wireshark
Figure 4 - View the SIP Trying response in Wireshark

The 180 Ringing provisional response follows the Trying in the capture and indicates that the destination endpoint has been successfully located and its device is now alerting the called user. In Wireshark the Ringing PDU shows the same dialog identifiers — Call-ID, From, To and CSeq — binding it to the original INVITE transaction, while the Via header confirms the return path back to the calling softphone. This packet causes the calling side to play a ringback tone to the user, and its presence in the capture confirms that the PBX has successfully routed the call to the correct extension.

View the SIP Ringing response in Wireshark
Figure 5 - View the SIP Ringing response in Wireshark

The 200 OK response to the INVITE is the most significant packet in the capture, confirming that the called party has answered and the SIP dialog is now fully established. Expanding the packet in Wireshark reveals a Contact header pointing to the answering endpoint, a To tag finalizing the dialog, and an SDP body specifying the agreed codec and the RTP port and IP address where the callee expects to receive media. Once this 200 OK is observed in the capture, the next expected packet is an ACK from the caller, after which both sides begin transmitting RTP audio streams on the negotiated ports.

View the SIP OK(Invite) response in Wireshark
Figure 6 - View the SIP OK(Invite) response in Wireshark

The ACK request appears immediately after the 200 OK in the filtered packet list and is sent by the calling user agent to confirm that it has received and processed the final response to the INVITE. In Wireshark the ACK carries the same Call-ID, From, To tags and CSeq method field as the established dialog, but contains no SDP body since media parameters were already negotiated in the INVITE and 200 OK exchange. The presence of this ACK in the capture marks the end of the three-way INVITE handshake and signals that active media transmission via RTP has commenced between the two endpoints.

View the SIP ACK response in Wireshark
Figure 7 - View the SIP ACK response in Wireshark

The BYE request appears in the capture when either the caller or callee terminates the call, and clicking it in Wireshark reveals the dialog headers — Call-ID, From, To and CSeq — that tie this termination message to the established session. Unlike the INVITE, the BYE carries no SDP body; its sole purpose is to signal the remote side that the sending party has stopped media transmission and wishes to release the dialog. Observing the BYE in the capture confirms that the softphone has correctly triggered the call teardown sequence, stopped the audio devices and detached the media handlers from the active call object.

View the SIP Bye request in Wireshark
Figure 8 - View the SIP Bye request in Wireshark

The 200 OK response to the BYE is the final packet in the SIP call flow and confirms that the remote side has acknowledged the termination request and released its side of the dialog. In Wireshark this packet mirrors the BYE's Call-ID, CSeq, From and To headers to close the transaction cleanly, and no SDP body is included since media has already stopped. Once this final OK is visible in the capture, the complete call lifecycle — from INVITE through ACK to BYE and its acknowledgment — is fully documented and both endpoints can safely free all resources associated with the session.

View the SIP OK(Bye) response in Wireshark
Figure 9 - View the SIP OK(Bye) response in Wireshark

Debug SIP call events in Asterisk

The following video shows how to enable SIP debugging in the Asterisk console and observe the SIP messages exchanged during a call in real time. It covers connecting to the Asterisk CLI, turning on SIP debug output, reading the INVITE and provisional responses as they appear after a call is made from the softphone.

Connect to the Asterisk console with verbose logging enabled, then run the following command to enable SIP debugging. Once enabled, Asterisk will print the full contents of every SIP message it sends or receives directly to the console, including the INVITE, 100 Trying, 180 Ringing, 200 OK, ACK, and BYE messages that make up a complete call.

sip set debug on

Turn on SIP debugging in Asterisk
Figure 10 - Turn on SIP debugging in Asterisk

With SIP debugging enabled, place a call from the softphone and watch the Asterisk console output. The highlighted block shows an incoming INVITE event, including the From and To SIP URIs identifying the caller and callee, the Call-ID uniquely labeling the dialog, the CSeq sequencing the transaction, and the SDP body beginning below the headers, which carries the media parameters for the call.

View SIP debugging information in Asterisk
Figure 11 - View SIP debugging information in Asterisk

Conclusion

After reading this documentation and studying the source code, we must be familiar about how to make the audio handlers to work, how to connect them to the actual call object, how to handle the necessary events. We also learnt how to make and accept calls.

The next example will introduce how to:

  • Put the call on hold
  • Take the call off hold
  • Transfer a call
  • Hang up the call
  • Redial

If you have any questions or need assistance, please contact us at info@voip-sip-sdk.com

Related Pages


More information