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.
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" : "<Reason header text; optional>", "reason_header_protocol" : "<Reason header protocol; optional>", "reason_header_cause" : "<Reason header cause code; 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" : "<object with key/value mappings (header name/value), 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>", "register_ttl" : "<integer; number of seconds after which the registration should expire; 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" : "<object with key/value mappings (header name/value), 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" : "<object with key/value mappings (header name/value), 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" : "<object with key/value mappings (header name/value), to specify custom headers to add to the SIP request; optional>" }
{ "request" : "hangup", "headers" : "<object with key/value mappings (header name/value), 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" : "unhold" }
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", "call_id" : "<user-defined value of Call-ID SIP header used to send the message; optional>", "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" : "<object with key/value mappings (header name/value), 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>" } }
After delivery a messagedelivery
event will be sent back with the SIP server response. Used to track the delivery status of the message.
{ "sip" : "event", "call_id" : "<value of SIP Call-ID header for related message>", "result" : { "event" : "messagedelivery", "code" : "<SIP error code>", "reason" : "<SIP error reason>", } }
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" : "<object with key/value mappings (header name/value), 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", "call_id" : "<user-defined value of Call-ID SIP header used in all SIP requests throughout the subscription; optional>", "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>", "subscribe_ttl" : "<integer; number of seconds after which the subscription should expire; optional>", "headers" : "<array of key/value objects, to specify custom headers to add to the SIP SUBSCRIBE; optional>" }
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", "call_id" : "<value of SIP Call-ID header for related subscription>", "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>, "send_peer_pli" : <true|false; whether or not send PLI to request keyframe from peer>, "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.
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:
register
there; this will be the master
session, and will return a master_id
when successfully registered;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;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.
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 Simultaneous SIP calls using the same account 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.