Fork me on GitHub
SIP plugin documentation

This is a simple SIP plugin for Janus, allowing WebRTC peers to register at a SIP server (e.g., Asterisk) and call SIP user agents through a Janus instance. Specifically, when attaching to the plugin peers are requested to provide their SIP server credentials, i.e., the address of the SIP server and their username/secret. This results in the plugin registering at the SIP server and acting as a SIP client on behalf of the web peer. Most of the SIP states and lifetime are masked by the plugin, and only the relevant events (e.g., INVITEs and BYEs) and functionality (call, hangup) are made available to the web peer: peers can call extensions at the SIP server or wait for incoming INVITEs, and during a call they can send DTMF tones. Calls can do plain RTP or SDES-SRTP.

The concept behind this plugin is to allow different web pages associated to the same peer, and hence the same SIP user, to attach to the plugin at the same time and yet just do a SIP REGISTER once. The same should apply for calls: while an incoming call would be notified to all the web UIs associated to the peer, only one would be able to pick up and answer, in pretty much the same way as SIP forking works but without the need to fork in the same place. This specific functionality, though, has not been implemented as of yet.

SIP Plugin API

All requests you can send in the SIP Plugin API are asynchronous, which means all responses (successes and errors) will be delivered as events with the same transaction.

The supported requests are register , unregister , call , accept, info , message , dtmf_info , recording , hold , unhold , update and hangup . register can be used, as the name suggests, to register a username at a SIP registrar to call and be called, while unregister unregisters it; call is used to send an INVITE to a different SIP URI through the plugin, while accept is used to accept the call in case one is invited instead of inviting; hold and unhold can be used respectively to put a call on-hold and to resume it; info allows you to send a generic SIP INFO request, while dtmf_info is focused on using INFO for DTMF instead; message is the method you use to send a SIP message to the other peer; recording is used, instead, to record the conversation to one or more .mjr files (depending on the direction you want to record); update allows you to update an existing session (e.g., to do a renegotiation or force an ICE restart); finally, hangup can be used to terminate the communication at any time, either to hangup (BYE) an ongoing call or to cancel/decline (CANCEL/BYE) a call that hasn't started yet.

No matter the request, an error response or event is always formatted like this:

{
        "sip" : "event",
        "error_code" : <numeric ID, check Macros below>,
        "error" : "<error description as a string>"
}

Notice that the error syntax above refers to the plugin API messaging, and not SIP error codes obtained in response to SIP requests, which are notified using a different syntax:

{
        "sip" : "event",
        "result" : {
                "event" : "<name of the error event>",
                "code" : <SIP error code>,
                "reason" : "<SIP error reason>"
        }
}

Coming to the available requests, you send a SIP REGISTER using the register request, which has to be formatted as follows:

{
        "request" : "register",
        "type" : "<if guest, no SIP REGISTER is actually sent; optional>",
        "send_register" : <true|false; if false, no SIP REGISTER is actually sent; optional>,
        "force_udp" : <true|false; if true, forces UDP for the SIP messaging; optional>,
        "force_tcp" : <true|false; if true, forces TCP for the SIP messaging; optional>,
        "sips" : <true|false; if true, configures a SIPS URI too when registering; optional>,
        "username" : "<SIP URI to register; mandatory>",
        "secret" : "<password to use to register; optional>",
        "ha1_secret" : "<prehashed password to use to register; optional>",
        "authuser" : "<username to use to authenticate (overrides the one in the SIP URI); optional>",
        "display_name" : "<display name to use when sending SIP REGISTER; optional>",
        "user_agent" : "<user agent to use when sending SIP REGISTER; optional>",
        "proxy" : "<server to register at; optional, as won't be needed in case the REGISTER is not goint to be sent (e.g., guests)>",
        "outbound_proxy" : "<outbound proxy to use, if any; optional>",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP REGISTER; optional>",
        "refresh" : <true|false; if true, only uses the SIP REGISTER as an update and not a new registration; optional>"
}

A registering event will be sent back, as this is an asynchronous request.

In case it is required to, this request will originate a SIP REGISTER to the specified server with the right credentials. 401 and 407 responses will be handled automatically, and so errors will not be notified back to the caller unless they're definitive (e.g., wrong credentials). A failure to register will return an error with name registration_failed. A successful registration, instead, is notified in a registered event formatted like this:

{
        "sip" : "event",
        "result" : {
                "event" : "registered",
                "username" : <SIP URI username>,
                "register_sent" : <true|false, depending on whether a REGISTER was sent or not>
        }
}

To unregister, just send an unregister request with no other arguments:

{
        "request" : "unregister"
}

As before, an unregistering event will be sent back. Just as before, this will also send a SIP REGISTER in case it had been sent originally. A successful unregistration is notified in an unregistered event:

{
        "sip" : "event",
        "result" : {
                "event" : "unregistered",
                "username" : <SIP URI username>,
                "register_sent" : <true|false, depending on whether a REGISTER was sent or not>
        }
}

Once registered, you can call or wait to be called: notice that you won't be able to get incoming calls if you chose never to send a REGISTER at all, though.

To send a SIP INVITE, you can use the call request, which has to be formatted like this:

{
        "request" : "call",
        "call_id" : "<user-defined value of Call-ID SIP header used in all SIP requests throughout the call; optional>",
        "uri" : "<SIP URI to call; mandatory>",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP INVITE; optional>",
        "srtp" : "<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>",
        "srtp_profile" : "<SRTP profile to negotiate, in case SRTP is offered; optional>",
        "secret" : "<password to use to call, only needed in case authentication is needed and no REGISTER was sent; optional>",
        "ha1_secret" : "<prehashed password to use to call, only needed in case authentication is needed and no REGISTER was sent; optional>",
        "authuser" : "<username to use to authenticate as to call, only needed in case authentication is needed and no REGISTER was sent; optional>"
}

A calling event will be sent back, as this is an asynchronous request.

Notice that this request MUST be associated to a JSEP offer: there's no way to send an offerless INVITE via the SIP plugin. This will generate a SIP INVITE and send it according to the instructions. While a 100 Trying will not be notified back to the user, a 180 Ringing will, in a ringing event:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "ringing",
        }
}

If the call is declined, or any other error occurs, a hangup error event will be sent back. If the call is accepted, instead, an accepted event will be sent back to the user, along with the JSEP answer originated by the callee:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "accepted",
                "username" : "<SIP URI of the callee>"
        }
}

At this point, PeerConnection-related considerations aside, the call can be considered established. A SIP ACK is sent automatically by the SIP plugin, so there's no action required of the application to do that manually.

Notice that the SIP plugin supports early-media via 183 responses responses. In case a 183 response is received, it's sent back to the user, along with the JSEP answer originated by the callee, in a progress event:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "progress",
                "username" : "<SIP URI of the callee>"
        }
}

In case the caller received a progress event, the following accepted event will NOT contain a JSEP answer, as the one received in the "Session Progress" event will act as the SDP answer for the session.

Notice that you only use call to start a conversation, that is for the first INVITE. To update a session via a re-INVITE, e.g., to renegotiate a session to add/remove streams or force an ICE restart, you do NOT use call, but another request called update instead. This request needs no arguments, as the whole context is derived from the current state of the session. It does need the new JSEP offer to provide, though, as part of the renegotiation.

{
        "request" : "update"
}

An updating event will be sent back, as this is an asynchronous request.

While the call request allows you to send a SIP INVITE (and the update request allows you to update an existing session), there is a way to react to SIP INVITEs as well, that is to handle incoming calls. Incoming calls are notified to the application via incomingcall events:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "incomingcall",
                "username" : "<SIP URI of the caller>",
                "displayname" : "<display name of the caller, if available; optional>",
                "srtp" : "<whether the caller mandates (sdes_mandatory) or offers (sdes_optional) SRTP support; optional>"
        }
}

The incomingcall may or may not be accompanied by a JSEP offer, depending on whether the caller sent an offerless INVITE or a regular one. Either way, you can accept the incoming call with the accept request:

{
        "request" : "accept",
        "srtp" : "<whether to mandate (sdes_mandatory) or offer (sdes_optional) SRTP support; optional>",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP OK; optional>"
}

An accepting event will be sent back, as this is an asynchronous request.

This will result in a 200 OK to be sent back to the caller. An accept request must always be accompanied by a JSEP answer (if the incomingcall event contained an offer) or offer (in case it was an offerless INVITE). In the former case, an accepted event will be sent back just to confirm the call can be considered established; in the latter case, instead, an accepting event will be sent back instead, and an accepted event will only follow later, as soon as a JSEP answer is available in the SIP ACK the caller sent back.

Notice that in case you get an incoming call while you're in another call, you will NOT get an incomingcall event, but a missed_call event instead, and just as a notification as there's no way to have two calls at the same time on the same handle in the SIP plugin:

{
        "sip" : "event",
        "call_id" : "<value of SIP Call-ID header for related call>",
        "result" : {
                "event" : "missed_call",
                "caller" : "<SIP URI of the caller>",
                "displayname" : "<display name of the caller, if available; optional>"
        }
}

Closing a session depends on the call state. If you have an incoming call that you don't want to accept, use the decline request; in all other cases, use the hangup request instead. Both requests need no additional arguments, as the whole context can be extracted from the current state of the session in the plugin:

{
        "request" : "decline",
        "code" : <SIP code to be sent, if not set, 486 is used; optional>"
}
{
        "request" : "hangup"
}

Since these are asynchronous requests, you'll get an event in response: declining if you used decline and hangingup if you used hangup.

As anticipated before, when a call is declined or being hung up, a hangup event is sent instead, which is basically a SIP error event notification as it includes the code and reason . A regular BYE, for instance, would be notified with 200 and SIP BYE, although a more verbose description may be provided as well.

When a session has been established, there are different requests that you can use to interact with the session.

The message request allows you to send a SIP MESSAGE to the peer:

{
        "request" : "message",
        "content" : "<text to send>"
}

A messagesent event will be sent back. Incoming SIP MESSAGEs, instead, are notified in message events:

{
        "sip" : "event",
        "result" : {
                "event" : "message",
                "sender" : "<SIP URI of the message sender>",
                "displayname" : "<display name of the sender, if available; optional>",
                "content" : "<content of the message>"
        }
}

SIP INFO works pretty much the same way, except that you use an info request to one to the peer:

{
        "request" : "info",
        "type" : "<content type>"
        "content" : "<message to send>"
}

A infosent event will be sent back. Incoming SIP INFOs, instead, are notified in info events:

{
        "sip" : "event",
        "result" : {
                "event" : "info",
                "sender" : "<SIP URI of the message sender>",
                "displayname" : "<display name of the sender, if available; optional>",
                "type" : "<content type of the message>",
                "content" : "<content of the message>"
        }
}

You can also record a SIP call, and it works pretty much the same the VideoCall plugin does. Specifically, you make use of the recording request to either start or stop a recording, using the following syntax:

{
        "request" : "recording",
        "action" : "<start|stop, depending on whether you want to start or stop recording something>"
        "audio" : <true|false; whether or not our audio should be recorded>,
        "video" : <true|false; whether or not our video should be recorded>,
        "peer_audio" : <true|false; whether or not our peer's audio should be recorded>,
        "peer_video" : <true|false; whether or not our peer's video should be recorded>,
        "filename" : "<base path/filename to use for all the recordings>"
}

As you can see, this means that the two sides of conversation are recorded separately, and so are the audio and video streams if available. You can choose which ones to record, in case you're interested in just a subset. The filename part is just a prefix, and dictates the actual filenames that will be used for the up-to-four recordings that may need to be enabled.

A recordingupdated event is sent back in case the request is successful.