OK, so now we have a client and a server that can both sign in, create a dialog, and recognize when the other has dropped the connection.  I’m sure what you’d really like to do is get them to message each other!  I’m sure some of you have gone ahead and figured it out already but for the rest of us there is one more very important step we need to accomplish before we can send and receive messages.

 

As most of you are aware, SIP is a signaling protocol.  It does not specify how the endpoints are to communicate.  When one endpoint sends and INVITE to another endpoint, part of the INVITE negotiation process is negotiating how they will communicate with each other.  For example, in order to send a message, the client must indicate in its INVITE that it would like to communicate with plain text and the server must indicate that is OK.  The server and client should also contain the ability to reject media offers that they do not support.

 

The protocol used to negotiate media, as many of you already know, is SDP – which stands for Session Description Protocol.  Thankfully, UCMA contains several helper classes to create the SDP necessary for communications.

 

In order for our client or server to partake in media negotiation, each of them must implement the IOfferAnswer interface.  This interface contains five methods.  Two of them, HandleReInviteOfferReceived and HandleThirdPartyControlOfferReceived apply only to reinvites and we will not discuss them today.  A ReINVITE occurs when one of the parties wishes to change media.  An example of this is two parties switching from an IM session to a video session.  I will discuss ReINVITES in a later post.

 

The other three methods are of interest to us today.

 

·         GetOffer – This method is called when you are initiating the INVITE and allows you to specify what type of media to use for the dialog.

 

·         GetAnswer – This method is called when you are receiving the INVITE and the other party did not specify an offer.

 

·         SetAnswer – This method is called when you are receiving an INVITE and the other party did specify an offer.  In this case your job is to accept the offer or specify that certain media types are not acceptable.

 

In order to get UCMA to call your IOfferAnswer methods, you must set the OfferAnswerNegotiation property of the SignalingSession before you call BeginParticipate.  Let’s start implementing this for our server.  First, add the IOfferAnswer interface to the definition of ISDKStudioServerManager then right click on IOfferAnswer and select ‘implement interface’.

 

public class IDKStudioServerManager : IOfferAnswer

 

As mentioned before, you do not need to implement HandleReInviteOfferReceived or HandleThirdPartyControlOfferReceived because they only apply to reinvites.  We will have the same response to both GetAnswer and GetOffer so let’s create a method that returns a ContentDescription we can use for either one.  Below is the code for it, which I will proceed to explain.  You will need to add a using statement for System.Net.Mime to get this to compile.

 

/// <summary>

/// Retrieves the content description for offers and answers

/// </summary>

/// <param name="session">The session</param>

/// <returns>The content description</returns>

private ContentDescription GetContentDescription(SignalingSession session)

{

    IPAddress ipAddress;

 

    // This method is called back every time an outbound INVITE is sent.

    RecordProgress("Getting the content description.");

    if (session.Connection != null)

    {

        ipAddress = session.Connection.LocalEndpoint.Address;

    }

 

    else

    {

        ipAddress = IPAddress.Any;

    }

 

    Sdp<SdpGlobalDescription, SdpMediaDescription> sessionDescription = new Sdp<SdpGlobalDescription, SdpMediaDescription>();

 

    //Set the origin line of the SDP

    //s, t, and v lines are automatically constructed

    sessionDescription.GlobalDescription.Origin.Version = 0;

    sessionDescription.GlobalDescription.Origin.SessionId = "0";

    sessionDescription.GlobalDescription.Origin.UserName = "-";

    sessionDescription.GlobalDescription.Origin.Connection.Set(ipAddress.ToString());

 

    //Set the connection line

    sessionDescription.GlobalDescription.Connection.TrySet(ipAddress.ToString());

 

    SdpMediaDescription mditem = new SdpMediaDescription("message");

 

    if (Settings.TransportType == SipTransportType.Tls)

    {

        mditem.Port = 5061;

    }

    else

    {

        mditem.Port = 5060;

    }

    mditem.TransportProtocol = "sip";

    mditem.Formats = "null";

 

    SdpAttribute aitem = new SdpAttribute("accept-types", "text/plain");

 

    mditem.Attributes.Add(aitem);

 

    //Append the Media description to the Global description

    sessionDescription.MediaDescriptions.Add(mditem);

 

    ContentType ct = new ContentType("application/sdp");

 

    return new ContentDescription(ct, sessionDescription.GetBytes());

}

 

Understanding the ContentDescription Code

In the following sections, let’s break this down into individual pieces that we can understand.

 

Connection Property

We need to specify the IP address for the connection piece in SDP.  If we have a connection associated with our session, we use its IP address.  If we do not have one, we play it safe and use IPAddress.Any.

 

if (session.Connection != null)

{

    ipAddress = session.Connection.LocalEndpoint.Address;

}

 

else

{

    ipAddress = IPAddress.Any;

}

 

Session Description Helper

This creates the session description helper that we will use to create our SDP response.

 

Sdp<SdpGlobalDescription, SdpMediaDescription> sessionDescription = new Sdp<SdpGlobalDescription, SdpMediaDescription>();

 

GlobalDescription Property

The following four statements construct the o= section of the SDP offer, or in other words the origin.  The version is the version of SDP and should always be 0.  We do not care about the session Id so we set it to 0.  The – for the user name indicates we do not care about it.  Finally, we set the connection address to the IP address we previously obtained.

sessionDescription.GlobalDescription.Origin.Version = 0;

sessionDescription.GlobalDescription.Origin.SessionId = "0";

sessionDescription.GlobalDescription.Origin.UserName = "-";

sessionDescription.GlobalDescription.Origin.Connection.Set(ipAddress.ToString());

 

Set the IP Address

This sets the c= line of the SDP offer.  The TrySet method makes sure the IP address is recognized but does not throw an exception if it is not.

 

sessionDescription.GlobalDescription.Connection.TrySet(ipAddress.ToString());

 

Define Acceptable Media

The SDPMediaDescription class creates perhaps the most important piece of the SDP – stating what types of media we accept.  There are a number of valid values for it and what is most important is the two peers agree on the names.  “Audio” and “video” are common values but here we will use “message” because I definitely do not want to get into the details of supporting RTP audio! J

 

SdpMediaDescription mditem = new SdpMediaDescription("message");

 

Configure the Port

Here we set the port depending on whether the user selected Tls or Tcp in the options (remember that they use different ports) and the transport protocol should almost always be “sip” in the case of messages, though it could be some other protocol.  For video and audio, it will most certainly not be sip.  The message media type does not have any format information so we can leave that null – though note that we cannot just simply set it to null.

 

if (Settings.TransportType == SipTransportType.Tls)

{

    mditem.Port = 5061;

}

else

{

    mditem.Port = 5060;

}

mditem.TransportProtocol = "sip";

mditem.Formats = "null";

 

Set the MIME Type

This is a very important line that states that the MIME type we will use is text/plain.

SdpAttribute aitem = new SdpAttribute("accept-types", "text/plain");

 

Add Final Session Attributes

These last lines basically add the MIME type description to our media description, then add the media description to our SDP (it is possible to have multiple media descriptions) and then return a new ContentDescription instance that accepts the MIME type of the content description (which should always be “application/sdp”) and the bytes of the session description we just created.

 

mditem.Attributes.Add(aitem);

 

//Append the Media description to the Global description

sessionDescription.MediaDescriptions.Add(mditem);

 

ContentType ct = new ContentType("application/sdp");

 

return new ContentDescription(ct, sessionDescription.GetBytes());

 

GetOffer and GetAnswer Methods

You can now set the method bodies of GetOffer and GetAnswer to the following.

 

return GetContentDescription((SignalingSession)sender);

 

SetAnswerMethod

We are now left only with SetAnswer.   In this case the client has sent us the offer and we need to respond to it.  This is actually what we will use today because currently only our client can initiate an INVITE with the server – the server will not initiate and INVITE to the client.  Fear not though – we did not write the code above in vain because eventually this code will be useful.

 

For SetAnswer we need to parse the response and see if it supports our “message” content type with the SIP protocol.  If for some reason we are unable to parse, we must terminate the session.  The code for SetAnswer is the following.

 

public void SetAnswer(object sender, ContentDescription answer)

{

    SignalingSession session = sender as SignalingSession;

 

    // Verify that the answer received is consistent

    RecordProgress("Setting the media answer.");

    byte[] Answer = answer.GetBody();

 

    if (Answer != null)

    {

        Sdp<SdpGlobalDescription, SdpMediaDescription> sessionDescription = new Sdp<SdpGlobalDescription, SdpMediaDescription>();

 

        if (!sessionDescription.TryParse(Answer))

        {

            session.BeginTerminate(null, session);

            Error("Unable to parse the answer.");

            return;

        }

        else

        {

            IList<SdpMediaDescription> ActiveMediaTypes = sessionDescription.MediaDescriptions;

 

            if ((ActiveMediaTypes.Count == 1) &&

                (ActiveMediaTypes[0].MediaName.Equals("message", StringComparison.Ordinal)) &&

                (ActiveMediaTypes[0].Port > 0) &&

                (ActiveMediaTypes[0].TransportProtocol.Equals("sip", StringComparison.OrdinalIgnoreCase)))

            {

            }

            else

            {

                session.BeginTerminate(null, session);

                Error("Unsupported media type");

            }

        }

    }

}

 

Now that I have explained it, this code probably does not look very scary.  We first try to parse the SDP and if that fails we terminate the session.  We don’t need to worry about the callback here because we don’t care about this session any more.  We also do not need to worry about removing it from our dictionary because EndParticipate will fail.

 

After we have successfully parsed the SDP, we make sure that we define a port, use the “message” media type, and use “sip” as the transport protocol.  By using SIP as the transport protocol, we are saying that we will use the SIP MESSAGE message to communicate.

 

Set the OfferAnswerNegotiation Property

The final step on our server is to set the OfferAnswerNegotiation property when we receive a session.

 

void SessionReceived(object sender, SessionReceivedEventArgs e)

{

    RecordProgress("An invite was received.");

 

    // Accept the invite

    lock (_syncObject)

    {

        if (false == _acceptingSessions)

        {

            RecordProgress("Terminating session because we are not accepting any more");

            e.Session.BeginTerminate(new AsyncCallback(NonAcceptedSessionTerminateCallback), e.Session);

        }

        else

        {

            e.Session.OfferAnswerNegotiation = this;

            e.Session.BeginParticipate(new AsyncCallback(ParticipateCallback), e.Session);

        }

    }

}

 

The implementation for the client is very similar to above, with the exception that you do not have to implement GetAnswer because the client will always initiate the INVITE.  I will leave this up to you for homework for tomorrow.

 

We now have a client and a server that can create a dialog and agree on a communications protocol.  The next step tomorrow will be actually sending and receiving a message.