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, decline , info , message , dtmf_info , subscribe , unsubscribe , transfer , 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 and decline are used to accept or reject the call in case one is invited instead of inviting; transfer takes care of attended and blind transfers (see Attended and blind transfers for more details); 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; subscribe and unsubscribe are used to deal with SIP events, i.e., to send SUBSCRIBE requests that will result in NOTIFY asynchronous events; 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>",
                "reason_header" : "<SIP reason header; optional>"
        }
}

Coming to the available requests, you send a SIP REGISTER using the register request. To be more precise, a register request MAY result in a SIP REGISTER, as this method actually provides ways to start using a SIP account with no need for a registration. It is the case, for instance, of the so-called guest registrations: if you register as a guest , it means you'll use the provided SIP URI in your From headers for calls, but you will actually not send a SIP REGISTER; this is especially useful for outgoing calls to services that don't require registration (e.g., IVR systems, or conference bridges), but also means you won't be able to receive calls unless peers know what your private SIP address is. A SIP REGISTER isn't sent also when registering as a helper : as we'll explain later, helper sessions are sessions only meant to facilitate the setup of Simultaneous SIP calls using the same account.

That said, a register request has to be formatted as follows:

{
        "request" : "register",
        "type" : "<if guest or helper, 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>,
        "rfc2543_cancel" : <true|false; if true, configures sip client to CANCEL pending INVITEs without having received a provisional response first; 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>",
        "contact_params" : "<array of key/value objects, to specify custom Contact URI params to add to the SIP REGISTER; optional>",
        "incoming_header_prefixes" : "<array of strings, to specify custom (non-standard) headers to read on incoming SIP events; optional>",
        "refresh" : <true|false; if true, only uses the SIP REGISTER as an update and not a new registration; optional>",
        "master_id" : <ID of an already registered account, if this is an helper for multiple calls (more on that later); 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>,
                "master_id" : <unique ID of this registered session in the plugin, if a potential master>
        }
}

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>",
        "refer_id" : <in case this is the result of a REFER, the unique identifier that addresses it; optional>,
        "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>",
        "autoaccept_reinvites" : <true|false, whether we should blindly accept re-INVITEs with a 200 OK instead of relaying the SDP to the application; optional, TRUE by default>
}

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",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

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>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

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>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

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>",
                "callee" : "<SIP URI that was called (in case the user is associated with multiple public URIs)>",
                "referred_by" : "<SIP URI header conveying the identity of the transferor, if this is a transfer; optional>",
                "replaces" : "<call-ID of the call that this is supposed to replace, if this is an attended transfer; optional>",
                "srtp" : "<whether the caller mandates (sdes_mandatory) or offers (sdes_optional) SRTP support; optional>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; 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>"
        "autoaccept_reinvites" : <true|false, whether we should blindly accept re-INVITEs with a 200 OK instead of relaying the SDP to the browser; optional, TRUE by default>
}

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>",
                "callee" : "<SIP URI that was called (in case the user is associated with multiple public URIs)>"
        }
}

Besides, you only use accept to answer the first INVITE. To accept a re-INVITE instead, which would be notified via an updatingcall event, you do NOT use accept, but the previously introduced 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 answer to provide, though, as part of the renegotiation. As before, an updated event will be sent back, as this is an asynchronous request.

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>",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP request; optional>"
}
{
        "request" : "hangup",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP BYE; optional>"
}

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.

First of all, you can put a call on-hold with the hold request. By default, this request will send a new INVITE to the peer with a sendonly direction for media, but in case you want to set a different direction (recvonly or inactive ) you can do that by passing a direction attribute as well:

{
        "request" : "hold",
        "direction" : "<sendonly, recvonly or inactive>"
}

No WebRTC renegotiation will be involved here on the holder side, as this will only trigger a re-INVITE on the SIP side. To remove the call from on-hold, just send a unhold request to the plugin, which requires no additional attributes:

{
        "request" : "hold",
        "direction" : "<sendonly, recvonly or inactive>"
}

and will restore the media direction that was set in the SDP before putting the call on-hold.

The message request allows you to send a SIP MESSAGE to the peer. By default, it is sent in dialog, during active call. But, if the user is registered, it might be sent out of dialog also. In that case the uri parameter is required.

{
        "request" : "message",
        "content_type" : "<content type; optional>"
        "content" : "<text to send>",
        "uri" : "<SIP URI of the peer; optional; if set, the message will be sent out of dialog>",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP MESSAGE; optional>"
}

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_type" : "<content type of the message>",
                "content" : "<content of the message>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

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>",
        "headers" : "<array of key/value objects, to specify custom headers to add to the SIP INFO; optional>"
}

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>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

As anticipated, SIP events are supported as well, using the SUBSCRIBE and NOTIFY mechanism. To do that, you need to use the subscribe request, which has to be formatted like this:

{
        "request" : "subscribe",
        "event" : "<the event to subscribe to, e.g., 'message-summary'; mandatory>",
        "accept" : "<what should be put in the Accept header; optional>",
        "to" : "<who should be the SUBSCRIBE addressed to; optional, will use the user's identity if missing>"
}

A subscribing event will be sent back, followed by a subscribe_succeeded if the SUBSCRIBE request was accepted, and a subscribe_failed if the transaction failed instead. Incoming SIP NOTIFY events, instead, are notified in notify events:

{
        "sip" : "event",
        "result" : {
                "event" : "notify",
                "notify" : "<name of the event that the user is subscribed to, e.g., 'message-summary'>",
                "substate" : "<substate of the subscription, e.g., 'active'>",
                "content-type" : "<content-type of the message>"
                "content" : "<content of the message>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

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.

Simultaneous SIP calls using the same account

As anticipated in the previous sections, attaching to the SIP plugin with a Janus handle means creating a SIP stack on behalf of a user or application: this typically means registering an account, and being able to start or receive calls, handle subscriptions, and so on. This also means that, since in Janus each core handle can only be associated with a single PeerConnection, each SIP account is limited to a single call per time: if a user is in a SIP session already, and another call comes in, it's automatically rejected with a 486 Busy .

While usually not a big deal, there are use cases where it might make sense to be able to support multiple concurrent calls, and maybe switch from one to the other seamlessly. This is possible in the SIP plugin using the so-called helper sessions. Specifically, helper sessions work under the assumption that there's a master session that is registered normally (the "regular" SIP plugin handle, that is), and that these helper sessions can simply be associated to that: any time another concurrent call is needed, if the master session is busy one of the helpers can be used; the more helper sessions are available, the more simultaneous calls can be established.

The way this works is simple:

  1. you create a SIP session the usual way, and send a regular register there; this will be the master session, and will return a master_id when successfully registered;
  2. for each helper you want to add, you attach a new Janus handle to the SIP plugin, and send a register with type: "helper" and providing the same username as the master, plus a master_id attribute referencing the main session;
  3. at this point, the new helper is associated to the master , meaning it can be used to start new calls or receive calls exactly as the main session, and using the same account information, credentials, etc.

Notice that, as soon as the master unregisters, or the Janus handle it's on is detached, all the helper sessions associated to it are automatically torn down as well. Specifically, the plugin will forcibly detach the related handles. Should you need to register again, and want some helpers there too, you'll have to add them again.

If you want to see this in practice, the SIP plugin demo has a "hidden" function you can invoke from the JavaScript console to play with helpers: calling the addHelper() function will add a new helper, and show additional controls. You can add as many helpers as you want.

Attended and blind transfers

The Janus SIP plugin supports both attended and blind transfers, and to do so mostly relies on the multiple calls functionality: as such, make sure you've read and are familiar with the section on Simultaneous SIP calls using the same account .

Most of the transfer-related functionality are based on existing messages and events already documented in the previous section, but there are a few aspects you need to be aware of. First of all, if you're the transferor, you need to use a new request called transfer , that allows you to send a SIP REFER to the transferee so to reach a different target. The transfer request must be formatted like this:

{
        "request" : "transfer",
        "uri" : "<SIP URI to send the transferee too>",
        "replace" : "<call-ID of the call this attended transfer is supposed to replace; default is none, which means blind/unattended transfer>"
}

Whether this is a blind (no call to replace) or attended transfer, a transferring event will be sent back, as this is an asynchronous request. Further updates will come in the form of NOTIFY-related events, as a REFER implicitly creates a subscription.

The recipient of a REFER, instead, will receive an asynchronous event called transfer as well, with info it needs to be aware of. In fact, the SIP plugin doesn't do anything automatically: an incoming REFER is notified to the application, so that it can decide whether to follow up on the transfer or not. The syntax of the event is the following:

{
        "sip" : "event",
        "result" : {
                "event" : "transfer",
                "refer_id" : <unique ID, internal to Janus, of this referral>,
                "refer_to" : "<SIP URI to call>",
                "referred_by" : "<SIP URI SIP URI header conveying the identity of the transferor; optional>",
                "replaces" : "<call-ID of the call this transfer is supposed to replace; optional, and only present for attended transfers>",
                "headers" : "<object with key/value strings; custom headers extracted from SIP event based on incoming_header_prefix defined in register request; optional>"
        }
}

The most important property in that list is refer_id as that value must be included in the call request to call the target, if the transfer is accepted: in fact, that's the only way the SIP plugin has to correlate the new outgoing call to the previous transfer request, and thus be able to notify the transferor about how the call is proceeding by means of NOTIFY events. Notice that, if the transferee decides to follow up on the transfer request, and they're already in a call (e.g., with the transferor), then they must use a different handle for the purpose, e.g., via a helper as described in the sipmc section.

The transfer target will receive the call exactly as previously discussed, with the difference that it may or may not include a referred_by property for information purposes. Just as the transferee, if they're already in a call, it's up to the application to create a helper to setup a new Janus handle to accept the transfer.

Notice that the plugin will NOT put the involved calls on-hold, or automatically close calls that are meant to be replaced by a transfer. All this is the application responsibility, and as such it's up to the developer to react to events accordingly.