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
, 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>", "reason_header" : "<SIP reason header; optional>" } }
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>", "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> }
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>" "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>" } }
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>" }
{ "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.