WebRTC using OpenSIPS and RTPEngine

In this article you will find tips, pointers and code snippets to help you get started with WebRTC using OpenSIPS and RTPEngine. At the end I have provided some notes and URL links that may be useful to anyone wishing to learn more about the media handling.

In the initialisation section of opensips.cfg

A listen statement is required to make opensips accept websocket connections. The usual port is 443, but you can use a different port if you want. Generally, you will also want to add an alias matched to the published hostname. I also recommend that you consider setting a value for maximum file handles as this directly impacts the number of simultaneous connections possible on one server. The default value may not be enough.

open_files_limit=4096
alias=wss:myhostname.com:443
listen=wss:123.4.5.67:443

In the module loading section

As well as the usual modules you would use, it is necessary to ensure the following modules are also loaded:

loadmodule "proto_udp.so"
loadmodule "proto_tls.so
loadmodule "proto_wss.so"
loadmodule "tls_mgm.so
loadmodule "rtpengine.so"
loadmodule "nathelper.so"

Here are a few suggestions for module parameter settings that you might want to use. This is by no means a comprehensive list – more of a hint to make sure you remember to consider including them. The actual values assigned in the code snippet shown below were taken from my test server so remember that they may not be the right values for your situation. Make your own decision on what values to assign.

modparam("proto_wss", "wss_max_msg_chunks", 16)

modparam("tls_mgm", "tls_method", "SSLv23")
modparam("tls_mgm", "ciphers_list", "AES256-GCM-SHA384,AES256-SHA256,AES256-SHA,CAMELLIA256-SHA,AES128-SHA,CAMELLIA128-SHA,RC4-SHA")
modparam("tls_mgm", "verify_cert", "0")
modparam("tls_mgm", "require_cert", "0")

modparam("rtpengine", "rtpengine_sock", "udp:123.4.5.68:2123")

In the main route section

In this section, I will provide some guidelines and hints along with a few snippets of code to help. I may add to this section over the course of time – please leave feedback in the comments at the end of this article to make suggestions if there are particular issues where you like to see more information and guidance.

Source type identification

For all requests, detect the transport protocol of the source and set a flag to retain and recall this information throughout the route:

if ($pr == "ws" || $pr == "wss")
    setflag(SRC_WS);
else
    setflag(SRC_SIP);

NAT handling

Your script should assume that all websocket connections are from devices behind NAT. Generally, you will want to use functions like fix_nated_register() and fix_nated_contact() to automatically fix SIP requests as they arrive. When dealing with a registration from a device behind NAT you should use a branch flag to identify it as such. This is because branch flags are stored in the location table. If your server is handling a mix of ordinary SIP as well as WebRTC devices, then you should use branch flags to identify if the registration is WebRTC or NAT’d SIP or no-NAT SIP – this might require two branch flags.

Other suggestions for the main route

When handling an initial INVITE request:

  • Create a new dialog (requires the DIALOG module)
  • You will very likely need to add a Record-Route header (requires the RR module)
  • Make sure a branch route will be used (t_on_branch) and a reply handler has been defined (t_on_reply)

It is quite useful to use the topology hiding module because it not only provides better security but also helps to keep the size of your SIP packets down. There is a greater than usual risk of exceeding the MTU when using WebRTC.

If you don’t use topology hiding, you will need to include a section to handle loose routed requests.

Remember that sequential requests, especially re-INVITEs, may need to interact with RTPEngine. I was able to use the same sub-route to relay initial requests and also to relay loose-routed requests; so the logic used to control RTPEngine was all in one place and was common to both situations.

Media Proxying with RTPEngine

Most VoIP engineers who try to set up WebRTC using OpenSIPS or Kamailio find it difficult to know how to correctly activate RTPEngine. The documentation is okay, but there are too few working examples published on the Internet. So let’s see if we can fix that by giving you some example code to try.

I found there were several places in my code where I simply wanted to check if it was necessary to use RTPEngine, so I put the code into a sub-route that could be called from anywhere. The sub-route simply sets a flag called “USE_RTPE” which is quick and easy to reference later in the script. Here is the sub-route – it should be self-explanatory:

route[IS_RTPE_REQUIRED] {
    # Sets the flag to show if RTPEngine is required
    if (isbflagset(WS_DEVICE)) {
        # WebRTC destination
        setflag(USE_RTPE);
    } else {
        # SIP destination
        if (isflagset(SRC_SIP)) {
            # Check SDP for rfc1918 addresses - some natted SIP user devices need RTPEngine
            if (nat_uac_test("8") || isbflagset(NATTED_CLIENT))
                setflag(USE_RTPE);
        } else
            setflag(USE_RTPE);
    }
}

The scope of a transaction flag in OpenSIPS is limited to the handling of the current request so I found it useful to duplicate the function of this flag in a dialog flag that could be interrogated in sequential requests or responses. It may also be useful to add a distinctive parameter to the Record-Route headers because this allows you to use the function check_route_param() in subsequent sequential loose-routed requests to see if a call is using RTPEngine. For example, by calling add_rr_param(“;RTPE=Y”) in the initial INVITE request, would make it possible to check the BYE request with check_route_param(“RTPE=Y”). [Useful because dialog flags don’t always behave as you want at the end of the dialog when BYE is received].

It’s a good idea to activate RTPEngine in a branch route and not in the main route of your code. This is what my branch route looks like:

branch_route[1] {
    if (isflagset(USE_RTPE) && is_method("INVITE|UPDATE") && has_body("application/sdp"))
        route(rtpengine_offer);
}

You will need to call RTPEngine_answer from your onreply handler. Here is a snippet from my handler:

if (isflagset(USE_RTPE)) {
    if (t_check_status("(183)|(200)") && has_body("application/sdp"))
        route(rtpengine_answer);
}

Now you just need to know what the two sub-routes look like that deal with activation and control of RTPEngine. The parameters shown below may include options that are not strictly necessary. For example, you may not actually need to include “replace-session-connection”. So why did I include them? Because I tried so many combinations and variations, with mixed results, that it became necessary to work more methodically and not to take anything for granted. So I made some options explicit rather than implicit just for clarity (and for the sake of my own sanity). You may also not want to use the same transcode options as are shown in my examples for the SIP-to-web connection.

route[rtpengine_offer] {
    if (isflagset(SRC_WS) && isbflagset(WS_DEVICE))
        # - Web to web
        $var(reflags) = "trust-address replace-origin replace-session-connection SDES-off ICE=force";
    else if (isflagset(SRC_WS))
        # - Web to SIP
        $var(reflags) = "trust-address replace-origin replace-session-connection rtcp-mux-demux ICE=remove RTP/AVP";
    else if (isbflagset(WS_DEVICE))
        # - SIP to web
        $var(reflags) = "trust-address replace-origin replace-session-connection rtcp-mux-offer ICE=force transcode-PCMU transcode-G722 SDES-off UDP/TLS/RTP/SAVP";
    else
        # - SIP to SIP
        $var(reflags) = "trust-address replace-origin replace-session-connection rtcp-mux-demux ICE=remove RTP/AVP";

    rtpengine_offer("$var(reflags)");
}

route[rtpengine_answer] {
    if (isflagset(SRC_WS) && isbflagset(WS_DEVICE))
        $var(reflags) = "trust-address replace-origin replace-session-connection SDES-off ICE=force";
    else if (isflagset(SRC_WS))
        $var(reflags) = "trust-address replace-origin replace-session-connection rtcp-mux-require ICE=force RTP/SAVPF";
    else
        $var(reflags) = "trust-address replace-origin replace-session-connection rtcp-mux-demux ICE=remove RTP/AVP";

    rtpengine_answer("$var(reflags)");
}

How do you test it?

Testing your WebRTC solution can be difficult. I found that results would depend not only on the chosen client and browser, but I even got different results on different laptops, PC’s and mobile phones. As you would expect, the network environment makes a difference too. For example, you should test Web-to-Web calls where both devices are on the same LAN and also where one device is on your LAN and the other is on a remote site depending entirely on access via the Internet. Test calls in both directions too.

I found a small number of free-to-try WebRTC clients that were useful for testing, although you need to avoid any that are not being kept updated because this is an evolving technology. Here are two links you may want to check out:

https://www.doubango.org/sipml5/call.htm?svn=15

https://collecttix.github.io/ctxSip/

Things you may see in the SDP and further reading

My research around this topic turned up some useful articles on the web. Here are a few notes I made and links to the articles I used to learn about the details of this technology.

Media Encryption

The use of unencrypted RTP is explicitly forbidden by the WebRTC specification. The specification requires that any compliant WebRTC implementation support RTP/SAVPF. The actual SRTP key exchange is initially performed end-to-end with DTLS-SRTP. Read more here:

https://webrtchacks.com/webrtc-must-implement-dtls-srtp-but-must-not-implement-sdes/

http://webrtc-security.github.io/

Media Stream Tracks

Media Stream Tracks are components in a “grouped” media stream. e.g. video and audio tracks for one end point using both. Here are links to some articles about Synchronisation Source (“ssrc”) and MediaStreamTracks (appear as “msid” and “msid-semantic” in the SDP). Read more here:

https://help.callstats.io/hc/en-us/articles/360004485394

https://help.callstats.io/hc/en-us/articles/360004502113-What-does-MSID-in-SDP-mean

https://help.callstats.io/hc/en-us/articles/360001188494-What-s-in-my-MST-charts

Trickle ICE:

Collecting ICE candidates can introduce very long delays when a call is started. Trickle is an enhancement to allow the SIP requests to be transmitted as candidates are being collected rather than wait for them all then send.

https://webrtchacks.com/trickle-ice/

 

Leave a comment