janode
Version:
Meetecho adapter for the Janus WebRTC Server
612 lines (549 loc) • 20.6 kB
JavaScript
'use strict';
/**
* This module contains the implementation of the SIP plugin (ref. {@link https://janus.conf.meetecho.com/docs/sip.html}).
* @module sip-plugin
*/
import Handle from '../handle.js';
import { JANODE } from '../protocol.js';
/* The plugin ID exported in the plugin descriptor */
const PLUGIN_ID = 'janus.plugin.sip';
/* These are the requests defined for the Janus SIP plugin API */
const REQUEST_REGISTER = 'register';
const REQUEST_CALL = 'call';
const REQUEST_ACCEPT = 'accept';
const REQUEST_HANGUP = 'hangup';
const REQUEST_DECLINE = 'decline';
/* These are the events/responses that the Janode plugin will manage */
/* Some of them will be exported in the plugin descriptor */
const PLUGIN_EVENT = {
REGISTERED: 'sip_registered',
REGISTERING: 'sip_registering',
CALLING: 'sip_calling',
RINGING: 'sip_ringing',
PROCEEDING: 'sip_proceeding',
INCOMING: 'sip_incoming',
HANGUP: 'sip_hangup',
HANGINGUP: 'sip_hangingup',
DECLINING: 'declining',
ACCEPTED: 'sip_accepted',
MISSED: 'sip_missed',
INFO: 'sip_info',
DTMF: 'sip_dtmf',
ERROR: 'sip_error',
ERROR_EVENT: 'sip_error_event',
};
/**
* The class implementing the SIP plugin (ref. {@link https://janus.conf.meetecho.com/docs/sip.html}).<br>
*
* It extends the base Janode Handle class and overrides the base "handleMessage" method.<br>
*
* Moreover it defines some methods to support SIP operations.<br>
*
* @hideconstructor
* @extends module:handle~Handle
*/
class SipHandle extends Handle {
/**
* Create a Janode SIP handle.
*
* @param {module:session~Session} session - A reference to the parent session
* @param {number} id - The handle identifier
*/
constructor(session, id) {
super(session, id);
this._pendingRegister = null;
this._pendingCalls = {};
this.on(JANODE.EVENT.HANDLE_HANGUP, _ => {
this._pendingRegister = null;
this._pendingCalls = {};
});
this.on(JANODE.EVENT.HANDLE_DETACHED, _ => {
this._pendingRegister = null;
this._pendingCalls = {};
});
}
/**
* The custom "handleMessage" needed for handling SIP plugin messages.
*
* @private
* @param {Object} janus_message
* @returns {Object} A falsy value for unhandled events, a truthy value for handled events
*/
handleMessage(janus_message) {
const { plugindata, transaction } = janus_message;
if (plugindata && plugindata.data && plugindata.data.sip) {
/**
* @type {SipData}
*/
const message_data = plugindata.data;
const { sip, result, call_id, error, error_code } = message_data;
/* The event can not be recognized, return a falsy value */
if (!error && sip !== 'event' && !result.event)
return null;
/* Prepare an object for the output Janode event */
const janode_event = this._newPluginEvent(janus_message);
/* Add call id information if available */
if (call_id) {
janode_event.data.call_id = call_id;
this._pendingCalls[call_id] = this._pendingCalls[call_id] || {};
}
/* Plugin messaging error (not related to SIP requests) */
if (error) {
janode_event.event = PLUGIN_EVENT.ERROR;
janode_event.data = new Error(`${error_code} ${error}`);
/* In case of error, close a transaction */
this.closeTransactionWithError(transaction, janode_event.data);
return janode_event;
}
/* Emit the event to the application */
let emit = false;
/* Close the related janus transaction */
const CLOSE_TX_NO = 0;
const CLOSE_TX_SUCCESS = 1;
const CLOSE_TX_ERROR = -1;
let closeTx = CLOSE_TX_NO;
let txId = transaction;
switch (result.event) {
/* Registering event */
case 'registering':
janode_event.event = PLUGIN_EVENT.REGISTERING;
closeTx = CLOSE_TX_NO;
emit = true;
break;
case 'registration_failed':
janode_event.event = PLUGIN_EVENT.ERROR_EVENT;
janode_event.data = new Error(`${result.code} ${result.reason}`);
closeTx = CLOSE_TX_ERROR;
txId = transaction || this._pendingRegister;
emit = false;
break;
case 'registered':
janode_event.event = PLUGIN_EVENT.REGISTERED;
janode_event.data.username = result.username;
janode_event.data.register_sent = result.register_sent;
closeTx = CLOSE_TX_SUCCESS;
txId = transaction || this._pendingRegister;
emit = false;
break;
case 'calling':
janode_event.event = PLUGIN_EVENT.CALLING;
closeTx = CLOSE_TX_NO;
emit = true;
break;
case 'ringing':
janode_event.event = PLUGIN_EVENT.RINGING;
closeTx = CLOSE_TX_NO;
emit = true;
break;
case 'proceeding':
janode_event.event = PLUGIN_EVENT.PROCEEDING;
closeTx = CLOSE_TX_NO;
emit = true;
break;
/* Inbound call */
case 'incomingcall': {
janode_event.event = PLUGIN_EVENT.INCOMING;
/* Store the incoming call URI */
const call = this._pendingCalls[call_id];
if (call) {
call.incoming = result.username;
}
janode_event.data.username = result.username;
janode_event.data.callee = result.callee;
janode_event.data.display_name = result.displayname || undefined;
closeTx = CLOSE_TX_NO;
emit = true;
break;
}
case 'hangup': {
/* Is there is a pending call without a reply? */
const call = this._pendingCalls[call_id];
if (call && !call.accepted && !call.declined && !call.incoming) {
janode_event.event = PLUGIN_EVENT.ERROR_EVENT;
janode_event.data = new Error(`${result.code} ${result.reason}`);
closeTx = CLOSE_TX_ERROR;
}
/* Async hangup */
else {
janode_event.event = PLUGIN_EVENT.HANGUP;
closeTx = CLOSE_TX_NO;
emit = true;
}
delete this._pendingCalls[call_id];
break;
}
case 'hangingup':
janode_event.event = PLUGIN_EVENT.HANGINGUP;
closeTx = CLOSE_TX_SUCCESS;
emit = false;
break;
case 'declining': {
janode_event.event = PLUGIN_EVENT.DECLINING;
const call = this._pendingCalls[call_id];
if (call) {
call.declined = true;
}
closeTx = CLOSE_TX_SUCCESS;
emit = false;
break;
}
/* A call has been accepted */
case 'accepted': {
janode_event.event = PLUGIN_EVENT.ACCEPTED;
janode_event.data.username = result.username || this._pendingCalls[call_id].incoming;
const call = this._pendingCalls[call_id];
if (call) {
call.accepted = true;
}
closeTx = CLOSE_TX_SUCCESS;
emit = false;
break;
}
/* Call has been missed */
case 'missed_call': {
delete this._pendingCalls[call_id];
janode_event.event = PLUGIN_EVENT.MISSED;
janode_event.data.callee = result.callee;
janode_event.data.caller = result.caller;
closeTx = CLOSE_TX_NO;
emit = true;
break;
}
/* SIP INFO */
case 'info': {
janode_event.event = PLUGIN_EVENT.INFO;
janode_event.data.sender = result.sender;
if (result.displayname) {
janode_event.data.displayname = result.displayname;
}
janode_event.data.type = result.type;
janode_event.data.content = result.content;
if (result.headers) {
janode_event.data.headers = result.headers;
}
emit = true;
break;
}
/* RFC2833 DTMF */
case 'dtmf': {
janode_event.event = PLUGIN_EVENT.DTMF;
janode_event.data.sender = result.sender;
janode_event.data.signal = result.signal;
janode_event.data.duration = result.duration;
emit = true;
break;
}
}
/* The event has been handled */
if (janode_event.event) {
if (closeTx === CLOSE_TX_SUCCESS)
this.closeTransactionWithSuccess(txId, janus_message);
if (closeTx === CLOSE_TX_ERROR)
this.closeTransactionWithError(txId, janode_event.data);
if (emit)
this.emit(janode_event.event, janode_event.data);
return janode_event;
}
}
/* The event has not been handled, return a falsy value */
return null;
}
/**
* Register to the SIP plugin (sending of a SIP REGISTER is optional).
*
* @param {Object} params
* @param {string} [params.type] - optional SIP session type, either "guest" or "helper"
* @param {boolean} [params.send_register] - True to send a SIP register
* @param {boolean} [params.force_udp] - True to force UDP for the SIP messaging
* @param {boolean} [params.force_tcp] - True to force TCP for the SIP messaging
* @param {boolean} [params.sips] - True to configure a SIPS URI too when registering
* @param {boolean} [params.rfc2543_cancel] - True to configure sip client to CANCEL pending INVITEs without having received a provisional response
* @param {string} params.username - The SIP URI to register
* @param {string} [params.secret] - The password to use, if any
* @param {string} [params.ha1_secret] - The prehashed password to use, if any
* @param {string} [params.display_name] - The display name to use when sending SIP REGISTER
* @param {string} [params.proxy] - The server to register at (not needed for guests)
* @param {string} [params.outbound_proxy] - The server to register at (not needed for guests)
* @param {number} [params.register_ttl] - The number of seconds after which the registration should expire
*
* @returns {Promise<module:sip-plugin~SIP_EVENT_REGISTERED>}
*/
async register({ type, send_register, force_udp, force_tcp, sips, rfc2543_cancel, username, secret, ha1_secret, display_name, proxy, outbound_proxy, register_ttl }) {
const body = {
request: REQUEST_REGISTER,
username,
};
if (typeof type === 'string') body.type = type;
if (typeof send_register === 'boolean') body.send_register = send_register;
if (typeof force_udp === 'boolean') body.force_udp = force_udp;
if (typeof force_tcp === 'boolean') body.force_tcp = force_tcp;
if (typeof sips === 'boolean') body.sips = sips;
if (typeof rfc2543_cancel === 'boolean') body.rfc2543_cancel = rfc2543_cancel;
if (typeof secret === 'string') body.secret = secret;
if (typeof ha1_secret === 'string') body.ha1_secret = ha1_secret;
if (typeof display_name === 'string') body.display_name = display_name;
if (typeof proxy === 'string') body.proxy = proxy;
if (typeof outbound_proxy === 'string') body.outbound_proxy = outbound_proxy;
if (typeof register_ttl === 'number') body.register_ttl = register_ttl;
const request = {
janus: 'message',
body,
};
this.decorateRequest(request);
this._pendingRegister = request.transaction;
const response = await this.sendRequest(request, 10000);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.REGISTERED) {
evtdata.username = username;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Start a SIP call.
*
* @param {Object} params
* @param {string} params.uri - The SIP URI to call
* @param {string} [params.call_id] - The user-defined value of Call-ID SIP header used in all SIP requests throughout the call
* @param {string} [params.authuser] - The username to use to authenticate as to call, only needed in case authentication is needed and no REGISTER was sent
* @param {string} [params.secret] - The password to use for authentication, if any
* @param {string} [params.ha1_secret] - The prehashed password to use for authentication, if any
* @param {RTCSessionDescription} params.jsep - JSEP offer
* @returns {Promise<module:sip-plugin~SIP_EVENT_ACCEPTED>}
*/
async call({ uri, call_id, authuser, secret, ha1_secret, jsep }) {
if (typeof jsep === 'object' && jsep && jsep.type !== 'offer') {
const error = new Error('jsep must be an offer');
return Promise.reject(error);
}
const body = {
request: REQUEST_CALL,
uri,
};
if (typeof call_id === 'string') body.call_id = call_id;
if (typeof authuser === 'string') body.authuser = authuser;
if (typeof secret === 'string') body.secret = secret;
if (typeof ha1_secret === 'string') body.ha1_secret = ha1_secret;
const request = {
janus: 'message',
body,
jsep,
};
this.decorateRequest(request);
const response = await this.sendRequest(request, 120000);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.ACCEPTED)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Accept an incoming SIP call.
*
* @param {Object} params
* @param {RTCSessionDescription} params.jsep - JSEP answer
* @returns {Promise<module:sip-plugin~SIP_EVENT_ACCEPTED>}
*/
async accept({ jsep }) {
if (typeof jsep === 'object' && jsep && jsep.type !== 'answer') {
const error = new Error('jsep must be an answer');
return Promise.reject(error);
}
const body = {
request: REQUEST_ACCEPT,
};
const request = {
janus: 'message',
body,
jsep,
};
this.decorateRequest(request);
const response = await this.sendRequest(request, 10000);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.ACCEPTED)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Hangup a SIP call.
*
* @returns {Promise<module:sip-plugin~SIP_EVENT_HANGINGUP>}
*/
async sip_hangup() {
const body = {
request: REQUEST_HANGUP,
};
const request = {
janus: 'message',
body,
};
this.decorateRequest(request);
const response = await this.sendRequest(request, 10000);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.HANGINGUP)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Decline an incoming SIP call.
*
* @returns {Promise<module:sip-plugin~SIP_EVENT_DECLINING>}
*/
async decline() {
const body = {
request: REQUEST_DECLINE,
};
const request = {
janus: 'message',
body,
};
this.decorateRequest(request);
const response = await this.sendRequest(request, 10000);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.DECLINING)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
}
/**
* The payload of the plugin message (cfr. Janus docs).
* {@link https://janus.conf.meetecho.com/docs/sip.html}
*
* @private
* @typedef {Object} SipData
*/
/**
* The success event for a register request
*
* @typedef {Object} SIP_EVENT_REGISTERED
* @property {string} username - The URI that has been registered
* @property {boolean} register_sent - True is a REGISTER has been sent
*/
/**
* The success event for an accept request
*
* @typedef {Object} SIP_EVENT_ACCEPTED
* @property {string} call_id
* @property {string} username
* @property {RTCSessionDescription} [jsep]
*/
/**
* The success event for an hangup request
*
* @typedef {Object} SIP_EVENT_HANGINGUP
* @property {string} call_id
*/
/**
* The success event for a decline request
*
* @typedef {Object} SIP_EVENT_DECLINING
* @property {string} call_id
*/
/**
* The exported plugin descriptor.
*
* @type {Object}
* @property {string} id - The plugin identifier used when attaching to Janus
* @property {module:sip-plugin~SipHandle} Handle - The custom class implementing the plugin
* @property {Object} EVENT - The events emitted by the plugin
* @property {string} EVENT.SIP_REGISTERING {@link module:sip-plugin~SipHandle#event:SIP_REGISTERING SIP_REGISTERING}
* @property {string} EVENT.SIP_CALLING {@link module:sip-plugin~SipHandle#event:SIP_CALLING SIP_CALLING}
* @property {string} EVENT.SIP_RINGING {@link module:sip-plugin~SipHandle#event:SIP_RINGING SIP_RINGING}
* @property {string} EVENT.SIP_PROCEEDING {@link module:sip-plugin~SipHandle#event:SIP_PROCEEDING SIP_PROCEEDING}
* @property {string} EVENT.SIP_INCOMING {@link module:sip-plugin~SipHandle#event:SIP_INCOMING SIP_INCOMING}
* @property {string} EVENT.SIP_HANGUP {@link module:sip-plugin~SipHandle#event:SIP_HANGUP SIP_HANGUP}
* @property {string} EVENT.SIP_MISSED {@link module:sip-plugin~SipHandle#event:SIP_MISSED SIP_MISSED}
* @property {string} EVENT.SIP_INFO {@link module:sip-plugin~SipHandle#event:SIP_INFO SIP_INFO}
* @property {string} EVENT.SIP_DTMF {@link module:sip-plugin~SipHandle#event:SIP_DTMF SIP_DTMF}
*/
export default {
id: PLUGIN_ID,
Handle: SipHandle,
EVENT: {
/**
* The event notifying a register is in progress
*
* @event module:sip-plugin~SipHandle#event:SIP_REGISTERING
* @type {Object}
*/
SIP_REGISTERING: PLUGIN_EVENT.REGISTERING,
/**
* Event for a SIP call in progress
*
* @event module:sip-plugin~SipHandle#event:SIP_CALLING
* @type {Object}
* @property {string} call_id - SIP Call-ID header for related call
*/
SIP_CALLING: PLUGIN_EVENT.CALLING,
/**
* Event for a SIP call ringing
*
* @event module:sip-plugin~SipHandle#event:SIP_RINGING
* @type {Object}
* @property {string} call_id - SIP Call-ID header for related call
*/
SIP_RINGING: PLUGIN_EVENT.RINGING,
/**
* Event for a SIP call proceeding
*
* @event module:sip-plugin~SipHandle#event:SIP_PROCEEDING
* @type {Object}
* @property {string} call_id - SIP Call-ID header for related call
*/
SIP_PROCEEDING: PLUGIN_EVENT.PROCEEDING,
/**
* Event for an incoming SIP call
*
* @event module:sip-plugin~SipHandle#event:SIP_INCOMING
* @type {Object}
* @property {string} call_id - SIP Call-ID header for related call
* @property {string} callee - SIP URI that was called
* @property {string} display_name - Display name of the caller
* @property {string} username - SIP URI of the caller
* @property {RTCSessionDescription} [jsep] - The JSEP offer
*/
SIP_INCOMING: PLUGIN_EVENT.INCOMING,
/**
* Event for a SIP call hangup
*
* @event module:sip-plugin~SipHandle#event:SIP_HANGUP
* @type {Object}
* @property {string} call_id - SIP Call-ID header for related call
*/
SIP_HANGUP: PLUGIN_EVENT.HANGUP,
/**
* Event for a missed SIP call
*
* @event module:sip-plugin~SipHandle#event:SIP_MISSED
* @type {Object}
* @property {string} call_id - SIP Call-ID header for related call
* @property {string} callee - SIP URI that was called
* @property {string} caller - SIP URI of the caller
*/
SIP_MISSED: PLUGIN_EVENT.MISSED,
/**
* @event module:sip-plugin~SipHandle#event:SIP_INFO
* @type {Object}
* @property {string} sender - SIP URI of the message sender
* @property {string} [call_id] - SIP Call-ID header for related call
* @property {string} [displayname] - Display name of the sender
* @property {string} type - Content type of the message
* @property {string} content - Content of the message
* @property {Object} [headers] - Custom headers extracted from SIP event
*/
SIP_INFO: PLUGIN_EVENT.INFO,
/**
* @event module:sip-plugin~SipHandle#event:SIP_DTMF
* @type {Object}
* @property {string} sender - SIP URI of the DTMF sender
* @property {string} [call_id] - SIP Call-ID header for related call
* @property {string} signal - The DTMF tone signal
* @property {number} duration - The DTMF tone duration
*/
SIP_DTMF: PLUGIN_EVENT.DTMF,
},
};