src/conferences/Conference.js
import {Map, Set} from 'immutable';
import {BehaviorSubject} from 'rxjs';
import {EventEmitter} from '../eventemitter';
import {bindMethods} from '../utils';
import {ManagedStream} from '../media/ManagedStream';
import {LocalMediaHandler} from '../media/LocalMediaHandler';
import {ConferenceEndReason as EndReason} from './ConferenceEndReason';
import {ConferenceStatus as Status} from './ConferenceStatus';
import {Invitation} from './Invitation';
import {
ParticipantEvents,
StreamChangeEvents,
} from '../wac-proxy/stacks/ConferenceEvents';
import Logs from '../Logs';
import {ConferenceInvite} from './ConferenceInvite';
const log = Logs.instance.getLogger('SippoJS/calls/Conference');
const ALREADY_JOINED_ERROR = 'ALREADY_JOINED';
const CALL_TRANSFER_START_EVENT = 'callTransferStart';
const TEMPORARILY_UNAVAILABLE_RESPONSE = 480;
const OK_RESPONSE = 200;
const existUserButNotConnected = (code, invitation) =>
code === 404 && typeof invitation.getToUser() === 'object';
const getGatewayUri = (session, gtUsername) => {
return session ? gtUsername + `/${session}` : gtUsername;
};
const userStatusAsKeyValue = ({user, status}) => ({[user]: status});
const makePlainUserStatus = (users) => {
const reducer = (acc, user) =>
Object.assign(acc, userStatusAsKeyValue(user));
return users.reduce(reducer, {});
};
/**
* @typedef {string} gatewayUsername
*/
/**
* @typedef {Object} EventInfo
* @property {string} participant gatewayUsername of a participant in the conference
* @property {string} [reason='IN_CALL'|'HOLD'] reason to know if the participant is on hold or not
*/
/**
* @typedef {Object} LocalStreamEvent
* @property {string} participant gatewayUsername of the local stream
* @property {@link ManagedStream} stream
*/
/**
* @typedef {Object} RemoteStreamAddedEvent
* @property {string} participant gatewayUsername of the local stream
* @property {string} [reason='NEW'|'UNHOLD'] reason of stream addition
* @property {@link ManagedStream} stream
*/
/**
* @typedef {Object} RemoteStreamRemovedEvent
* @property {string} participant gatewayUsername of the local stream
* @property {string} [reason='GONE'|'HOLD'] reason of stream addition
* @property {@link ManagedStream} stream
*/
/**
* Provides access to methods for managing outgoing or incoming conferences.
* {@link Conference} objects are obtained calling {@link ConferenceManager#createConference} method or handling
* the `onIncoming` event of a connected {@link ConferenceManager} instance.
*
* ## Events
*
* - `remoteStreams` ({@link ImmutableSet}<{@link ManagedStream}> currentValue, {@link ImmutableSet}<{@link ManagedStream}> oldValue, {@link EventInfo})
* - Emitted every time remoteStreams changes.
* - `outgoingInvitations` ({@link ImmutableSet}<{@link Invitation}> currentValue, {@link Invitation}<{@link ManagedStream}> oldValue)
* - Emitted every a outgoingInvitation changes.
* - `expelled`
* - Emitted when we are expelled from the conference.
* deprecated
* - `destroyed`
* - Emitted when the conference is destroyed.
* deprecated
* - `status` ({@link ConferenceStatus} currentValue, {@link ConferenceStatus}<{@link ManagedStream}> oldValue)
* - Emitted every time the conference status changes.
* deprecated
* - `remoteParticipants` ({@link ImmutableMap}<{@link gatewayUsername}, 'IN_CALL'|'HOLD'> currentValue, {@link ImmutableMap}<{@link gatewayUsername}, 'IN_CALL'|'HOLD'> oldValue)
* - Emitted every time the participants list changes.
* - `transferError`
* - Emitted when an error occurs in a transfer.
* - `participantAdded` ({@link gatewayUsername} gatewayUsername)
* - Emitted when a new participant joins the conference
* - `participantRemoved` ({@link gatewayUsername} gatewayUsername)
* - Emitted when a participant leaves the conference
* - `participantHold` ({@link gatewayUsername} gatewayUsername)
* - Emitted when a participant starts holding the call
* - `participantUnhold` ({@link gatewayUsername} gatewayUsername)
* - Emitted when a participant stops holding the call
* - `localStreamAdded` ({@link LocalStreamEvent})
* - Emitted on a new local stream
* - `localStreamRemoved` ({@link LocalStreamEvent})
* - Emitted on local stream removed
* - `remoteStreamAdded` ({@link RemoteStreamAddedEvent})
* - Emitted on remote stream added
* - `remoteStreamRemove` ({@link RemoteStreamRemovedEvent})
* - Emitted on remote stream removed
*/
export class Conference {
/** @private */
constructor(userManager, room, mediaConstraints) {
this._userManager = userManager;
this._room = room;
this._status = new BehaviorSubject(Status.DISCONNECTED);
this._endReason = EndReason.UNSPECIFIED;
this._localMediaHandler = new LocalMediaHandler(mediaConstraints, this.updateMedia.bind(this));
this._transferingCall = false;
this._outgoingInvitations = new Set();
this._remoteParticipants = new Map();
this._remoteStreams = new Map();
this._remoteWacAddresses = new Set();
/**
* Use to keep the number of invitations sent in this conference
* @type {boolean}
*/
this.invitationCount = 0;
/** @type {EventEmitter} */
this.emitter = new EventEmitter({newListener: true});
this.emitter.on('newListener', (eventName) => {
if (['status', 'destroyed', 'expelled'].includes(eventName)) {
log.warn(`${eventName} is deprecated. Please use getStatus() and/or getStatus$() instead`);
}
});
bindMethods(this, [
'_onLocalStreamChanged',
'_onLocalStreamAdded',
'_onLocalStreamRemoved',
'_onRemoteStreamChanged',
'_onRemoteStreamAdded',
'_onRemoteStreamRemoved',
'_onExpelled',
'_onTransferingCall',
'_onDestroyed',
'_onParticipantAdded',
'_onParticipantRemoved',
'_onParticipantHold',
'_onParticipantUnhold',
]);
// Asume at least someone was invited for recovered conferences
if (room.recovered) {
this.invitationCount++;
}
}
/**
* @protected
* @param {UserManager} userManager
* @param {Room} room
* @param {MediaStreamConstraints} mediaConstraints
*/
static async create(userManager, room, mediaConstraints) {
const conference = new Conference(userManager, room, mediaConstraints);
conference._bindEventHandlers();
conference._owner = await userManager.resolveUser(room.owner);
await conference._localMediaHandler.init();
return conference;
}
/**
* @private
*/
_bindEventHandlers() {
this._room.on(StreamChangeEvents.LOCAL_STREAM_CHANGED, this._onLocalStreamChanged);
this._room.on(StreamChangeEvents.LOCAL_STREAM_ADDED, this._onLocalStreamAdded);
this._room.on(StreamChangeEvents.LOCAL_STREAM_REMOVED, this._onLocalStreamRemoved);
this._room.on(StreamChangeEvents.REMOTE_STREAM_CHANGED, this._onRemoteStreamChanged);
this._room.on(StreamChangeEvents.REMOTE_STREAM_ADDED, this._onRemoteStreamAdded);
this._room.on(StreamChangeEvents.REMOTE_STREAM_REMOVED, this._onRemoteStreamRemoved);
this._room.on('expelled', this._onExpelled);
this._room.on('transferingCall', this._onTransferingCall);
this._room.on('conference-destroyed', this._onDestroyed);
this._room.on(ParticipantEvents.PARTICIPANT_ADDED, this._onParticipantAdded);
this._room.on(ParticipantEvents.PARTICIPANT_REMOVED, this._onParticipantRemoved);
this._room.on(ParticipantEvents.PARTICIPANT_HOLD, this._onParticipantHold);
this._room.on(ParticipantEvents.PARTICIPANT_UNHOLD, this._onParticipantUnhold);
}
/**
* @private
*/
_unbindEventHandlers() {
this._room.off(StreamChangeEvents.LOCAL_STREAM_CHANGED, this._onLocalStreamChanged);
this._room.off(StreamChangeEvents.LOCAL_STREAM_ADDED, this._onLocalStreamAdded);
this._room.off(StreamChangeEvents.LOCAL_STREAM_REMOVED, this._onLocalStreamRemoved);
this._room.off(StreamChangeEvents.REMOTE_STREAM_CHANGED, this._onRemoteStreamChanged);
this._room.off(StreamChangeEvents.REMOTE_STREAM_ADDED, this._onRemoteStreamAdded);
this._room.off(StreamChangeEvents.REMOTE_STREAM_REMOVED, this._onRemoteStreamRemoved);
this._room.off('expelled', this._onExpelled);
this._room.off('transferingCall', this._onTransferingCall);
this._room.off('conference-destroyed', this._onDestroyed);
this._room.off(ParticipantEvents.PARTICIPANT_ADDED, this._onParticipantAdded);
this._room.off(ParticipantEvents.PARTICIPANT_REMOVED, this._onParticipantRemoved);
this._room.off(ParticipantEvents.PARTICIPANT_HOLD, this._onParticipantHold);
this._room.off(ParticipantEvents.PARTICIPANT_UNHOLD, this._onParticipantUnhold);
}
/**
* @private
*/
_onLocalStreamChanged(event) {
if (event.streams && event.streams.length > 0) {
this._localMediaHandler.setAudioOnlyStream(event.streams.find(s => s.getAudioTracks().length > 0));
this._localMediaHandler.setVideoStream(event.streams.find(s => s.getVideoTracks().length > 0));
// Detect stopped screen track
this._localMediaHandler.getVideoStream().getScreenTracks().forEach((screenTrack) => {
screenTrack.onended = () => this._localMediaHandler.toggleScreen(false);
});
} else {
this._localMediaHandler.setVideoStream();
this._localMediaHandler.setAudioOnlyStream();
}
}
/**
* @private
*/
_onLocalStreamAdded({participant, stream}) {
this.emitter.emit(StreamChangeEvents.LOCAL_STREAM_ADDED, {
participant,
stream: new ManagedStream(stream, true),
});
}
/**
* @private
*/
_onLocalStreamRemoved({participant, stream}) {
this.emitter.emit(StreamChangeEvents.LOCAL_STREAM_REMOVED, {
participant,
stream: new ManagedStream(stream, true),
});
}
/**
* @private
*/
_onRemoteStreamAdded({participant, stream, reason}) {
this.emitter.emit(StreamChangeEvents.REMOTE_STREAM_ADDED, {
participant,
reason,
stream: this._getRemoteStream(stream) || new ManagedStream(stream),
});
}
/**
* @private
*/
_getRemoteStream(stream) {
for (const remoteStreams of this._remoteStreams.values()) {
for (const remoteStream of remoteStreams) {
if (remoteStream && remoteStream.mediaStream === stream) {
return remoteStream;
}
}
}
}
/**
* @private
*/
_onRemoteStreamRemoved({participant, stream, reason}) {
this.emitter.emit(StreamChangeEvents.REMOTE_STREAM_REMOVED, {
participant,
reason,
stream: this._getRemoteStream(stream) || new ManagedStream(stream),
});
}
/**
* @private
*/
_onRemoteStreamChanged(event) {
return Promise.resolve(event.participant ? this._userManager.resolveUser(event.participant) : null).then((user) => {
const oldRemoteStreams = this._remoteStreams;
let invitationToSetStreamsTrue;
if (!event.streams || event.streams.length <= 0) {
this._remoteStreams = this._remoteStreams.delete(event.participant);
if (user) {
this._remoteWacAddresses = this._remoteWacAddresses.delete(user.getAddress());
}
} else {
let managedStreams = event.streams.map(stream => this._getRemoteStream(stream) || new ManagedStream(stream));
this._remoteStreams = this._remoteStreams.set(event.participant, managedStreams);
if (user) {
this._remoteWacAddresses = this._remoteWacAddresses.add(user.getAddress());
}
invitationToSetStreamsTrue = this._getOutgoingInvitation(event.participant);
}
this.emitter.emit('remoteStreams', this._remoteStreams, oldRemoteStreams, {
participant: event.participant,
reason: event.reason,
});
if (invitationToSetStreamsTrue) {
this._updateOutgoingInvitations(invitationToSetStreamsTrue.setStreams(true), invitationToSetStreamsTrue);
}
});
}
/**
* @private
*/
_onExpelled() {
this._localMediaHandler.setVideoStream();
this._localMediaHandler.setAudioOnlyStream();
this._remoteStreams = new Map();
this.emitter.emit('expelled');
this._setStatus(Status.DESTROYED);
}
/**
* @private
*/
_onDestroyed() {
this._localMediaHandler.setVideoStream();
this._localMediaHandler.setAudioOnlyStream();
this._remoteStreams = new Map();
this.emitter.emit('destroyed');
this._setStatus(Status.DESTROYED);
}
/**
* @private
*/
_onTransferingCall(event) {
this._setTransferingCall(true);
return this._room.acceptCallTransfer(event.payload.callTransferID).then(() => {
this.emitter.emit(CALL_TRANSFER_START_EVENT);
return this.inviteParticipant(event.payload.to, undefined, event.payload.mediatypes).then((response) => {
if (response != 200) {
this.emitter.emit('transferError', response);
}
}).finally(() => {
this._setTransferingCall(false);
});
});
}
/**
* @private
*/
async _onParticipantAdded(participantObj) {
log.debug(`onParticipantAdded(participant) : ${participantObj}`);
this._updateParticipants(participants =>
participants.set(participantObj.user, participantObj.status));
this.emitter.emit(ParticipantEvents.PARTICIPANT_ADDED, participantObj.user);
}
_updateParticipants(updater) {
const oldValue = this.getRemoteParticipants();
const currentValue = updater(oldValue);
this._remoteParticipants = currentValue;
this.emitter.emit('remoteParticipants', currentValue, oldValue);
return currentValue;
}
/**
* @private
*/
async _onParticipantRemoved(participant) {
log.debug(`onParticipantRemoved(participant) : ${participant}`);
this._updateParticipants(participants =>
participants.delete(participant));
this.emitter.emit(ParticipantEvents.PARTICIPANT_REMOVED, participant);
}
/**
* @private
* @param {string} participant - Participant user name
*/
_onParticipantHold(participant) {
log.debug(`onParticipantHold(participant) : ${participant}`);
this._updateParticipants(participants =>
participants.set(participant, 'HOLD'));
this.emitter.emit(ParticipantEvents.PARTICIPANT_HOLD, participant);
}
/**
* @private
* @param {string} participant - Participant user name
*/
_onParticipantUnhold(participant) {
log.debug(`onParticipantUnhold(participant) : ${participant}`);
this._updateParticipants(participants =>
participants.set(participant, 'IN_CALL'));
this.emitter.emit(ParticipantEvents.PARTICIPANT_UNHOLD, participant);
}
/**
* Manages the Invitations Set and events.
* @private
*/
_updateOutgoingInvitations(invitationToAdd, invitationToRemove) {
const oldOutgoingInvitations = this._outgoingInvitations;
if (invitationToRemove) {
this._outgoingInvitations = this._outgoingInvitations.delete(invitationToRemove);
}
if (invitationToAdd) {
this._outgoingInvitations = this._outgoingInvitations.add(invitationToAdd);
}
this.emitter.emit('outgoingInvitations', this._outgoingInvitations, oldOutgoingInvitations);
}
/**
* @private
*/
_getOutgoingInvitation(gatewayUsername) {
return this._outgoingInvitations.find((invitation) => {
return invitation.getTo() === gatewayUsername;
});
}
/**
* @private
*/
_hasActiveInvitation(gatewayUsername) {
return this._outgoingInvitations.some((i) => {
return i.getTo() === gatewayUsername && i.isActive();
});
}
/**
* This is assuming that if it does not has an invitation answered 200, it could never been in the room
* Made to solve PSTN problems (users with early media cannot be re-invited)
* TODO We need a proper way to check if the user is already joined
* @private
*/
_alreadyJoined(gatewayUsername) {
const invitation = this._getOutgoingInvitation(gatewayUsername);
if (invitation) {
return invitation.getResponseCode() === OK_RESPONSE && this._remoteStreams.has(gatewayUsername);
} else {
return this._remoteStreams.has(gatewayUsername);
}
}
/**
* @private
*/
_canSendInvitation(gatewayUsername) {
return !this._hasActiveInvitation(gatewayUsername) && !this._alreadyJoined(gatewayUsername);
}
/**
* Returns the ID of the conference
* @return {string}
*/
getId() {
return this._room.id;
}
/**
* Returns the remote participants
* @return {ImmutableMap<string, 'IN_CALL'|'HOLD'>}
*/
getRemoteParticipants() {
return this._remoteParticipants;
}
/**
* @private
*/
async fetchRemoteParticipants() {
const myAddress = this._userManager.getOwnAddress();
const myUser = await this._userManager.resolveUser(myAddress);
const notMyUser = ({user}) => user !== myUser.gatewayUsername;
const participantRetrieved = await this._room.getParticipants();
const remoteParticipants = participantRetrieved.filter(notMyUser);
const plainRemoteParticipants = makePlainUserStatus(remoteParticipants);
this._remoteParticipants = Map(plainRemoteParticipants);
return this._remoteParticipants;
}
/**
* Returns the current status of the conference
* @return {ConferenceStatus}
*/
getStatus() {
return this._status.value;
}
/**
* Returns an observable for the status of the conference
* @return {Observable<ConferenceStatus>}
*/
getStatus$() {
return this._status.asObservable();
}
/**
* Returns the username of the conference's owner
* @return {string}
*/
getOwner() {
return this._owner;
}
/**
* Allows access to local streams and actions related to changing which local media is shared
* @return {LocalMediaHandler}
*/
getLocalMediaHandler() {
return this._localMediaHandler;
}
/**
* Returns a Map containing a list of ManagedStreams per participant (gateway user name)
* @return {ImmutableMap<string, ManagedStream[]>}
*/
getRemoteStreams() {
return this._remoteStreams;
}
/**
* Returns a Set of strings containing the gateway username of every
* remote participant
* @return {ImmutableSet<string>}
*/
getRemoteGatewayUsernames() {
return new Set(this._remoteStreams.keySeq().filter(x => !!x));
}
/**
* Returns a Set of strings containing the address of remote participants (gateway user names)
* that are also wac users
* @return {ImmutableSet<string>}
*/
getRemoteWacAddresses() {
return this._remoteWacAddresses;
}
/**
* Returns a Set of outgoing invitations
* @return {ImmutableSet<Invitation>}
*/
getOutgoingInvitations() {
return this._outgoingInvitations;
}
/**
* Check if a conference is being transfering
* @return {boolean}
*/
isBeingTransfered() {
return this._transferingCall;
}
updateMedia(audio, video, screen) {
if (this.getStatus() === Status.CONNECTED) {
return this._room.update({audio, video, screen});
}
return Promise.resolve();
}
/**
* Returns a boolean value indicating if at least one of the remote conference participants
* has a video track
* @return {boolean}
*/
hasRemoteVideo() {
for (let [, [stream]] of this._remoteStreams) {
if (stream.hasVideoTracks()) {
return true;
}
}
return false;
}
/**
* @private
* @param {ConferenceStatus} status
*/
_setStatus(status) {
const oldStatus = this.getStatus();
this._status.next(status);
this.emitter.emit('status', status, oldStatus);
if (status === Status.DISCONNECTED) {
this._unbindEventHandlers();
}
}
/**
* @private
* @param {boolean} value
*/
_setTransferingCall(value) {
this._transferingCall = value;
}
/**
* Indicates if participants can be invited to this conference or not
* @return {boolean}
*/
canManage() {
return this._room.isOwner() || !this._room.hasOwner();
}
/**
* @return {boolean}
*/
isRecovered() {
return this._room.recovered;
}
/**
* Returns the current participant count.
* Includes in the count the users with active invitations.
* @return {number}
*/
getCurrentParticipants() {
var participantCount = this._remoteStreams.size;
this._outgoingInvitations.forEach((invitation) => {
if (invitation.isActive()) {
participantCount++;
}
});
return participantCount;
}
/**
* Returns the invitation to the conference if the participant was invited.
* @return {ConferenceInvite}
*/
getInvite() {
if (this._room.invite) {
const {id, from, mediaTypes} = this._room.invite;
return new ConferenceInvite(id, from, mediaTypes);
}
return undefined;
}
/**
* Invites a new participant to this conference
* @throws {Error} throw error when a participant has already invited or is joined to the conference.
* @param {String} participant
* @param {String} [session]
* @param {MediaTypes} [mediaTypes] the MediaTypes of this invitation
* @param {boolean} [autoAccepted=false]
* @param {string} [requestId] the ID of the request that generated this invitation.
* @param {boolean} [resolve=true] indicates if participant address should be resolved to an user
* gatewayUsername.
* @param {object} [context] Arbitrary context that can be sent when making an invite
* @returns {Promise<Object>} A promise with an object with two properties: SIP code and whoAccepted the invite
* @property {number} code SIP code of the response
* @property {string} whoAccepted User id of the participant that accepted the invite
*/
async inviteParticipant(participant, session, mediaTypes, autoAccepted = false, requestId, resolve = true, context = {}) {
if (!mediaTypes) {
mediaTypes = this._localMediaHandler.getMediaTypes();
mediaTypes.screen = false;
}
let gatewayUsername;
return this._userManager.resolveUser(participant).then((user) => {
log.debug('calls/Conference.js/inviteParticipant/resolveUser', participant, user);
gatewayUsername = resolve && user && user.gatewayUsername ? user.gatewayUsername : participant;
if (!this._canSendInvitation(gatewayUsername)) {
log.debug('calls/Conference.js/inviteParticipant/cannotSendInvite', gatewayUsername);
throw new Error(ALREADY_JOINED_ERROR);
}
this._updateOutgoingInvitations(
new Invitation(user, gatewayUsername, mediaTypes, autoAccepted, undefined, false, false),
this._getOutgoingInvitation(gatewayUsername));
this.invitationCount++;
return this._room.inviteParticipant(getGatewayUri(session, gatewayUsername), mediaTypes, requestId, context);
}).then((response) => {
log.debug('calls/Conference.js/inviteParticipant/room.inviteParticipant/response', response);
const oldInvitation = this._getOutgoingInvitation(gatewayUsername);
if (existUserButNotConnected(response.code, oldInvitation)) {
response.code = TEMPORARILY_UNAVAILABLE_RESPONSE;
}
let newInvitation = oldInvitation.setResponseCode(response.code);
if (response.whoAccepted) {
gatewayUsername = response.whoAccepted;
newInvitation = newInvitation.setTo(response.whoAccepted);
}
if (this._remoteStreams.get(gatewayUsername)) {
newInvitation = newInvitation.setStreams(true);
}
this._updateOutgoingInvitations(newInvitation, oldInvitation);
return response;
}).catch((error) => {
log.error('calls/Conference.js/inviteParticipant', error);
if (error.message !== ALREADY_JOINED_ERROR) {
const oldInvitation = this._getOutgoingInvitation(gatewayUsername);
if (oldInvitation) {
this._updateOutgoingInvitations(oldInvitation.setCanceled(true), oldInvitation);
}
}
throw error;
});
}
/**
* Expels a participant from this conference
* @param {string} participant
* @param {string} [session]
* @returns {Promise}
*/
expelParticipant(participant, session) {
if (session) {
participant += `/${session}`;
}
return this._room.expelParticipant(participant);
}
/**
* Cancels a invitation of this conference
*
* @param {Invitation} invitation
* @param {string} [session]
*/
cancelInvitation(invitation, session) {
if (invitation.isCanceled()) {
return;
}
if (!invitation.getResponseCode()) {
this._room.cancelInvitation(getGatewayUri(session, invitation.getTo()));
}
this._updateOutgoingInvitations(invitation.setCanceled(true), invitation);
}
/**
* Invites a list of new participant to this conference
* @param {string[]} participants
* @returns {Promise}
*/
inviteParticipants(participants) {
return Promise.all(participants.map(p => this.inviteParticipant(p)));
}
/**
* Expels a list of participant from this conference
* @param {string[]} participants
* @returns {Promise}
*/
expelParticipants(participants) {
return Promise.all(participants.map(p => this.expelParticipant(p)));
}
/**
* Connects to the conference
* @returns {Promise}
*/
async join() {
if (this.getStatus() !== Status.DISCONNECTED) {
return Promise.reject(Error(`You can't connect to a conference in ${Status[this.getStatus()]} status`));
}
this._setStatus(Status.CONNECTING);
try {
await this._room.join(this._localMediaHandler.getCurrentMediaConstraints());
await this.fetchRemoteParticipants();
this._setStatus(Status.CONNECTED);
} catch (error) {
this._setStatus(Status.DISCONNECTED);
this._room.leave();
throw error;
}
}
/**
* Leaves the conference
* @param {number} errorCode the rejection cause when an invitation is received
* and the conference has not been joined.
* @returns {Promise}
*/
async leave(errorCode) {
switch (this.getStatus()) {
case Status.DESTROYED:
return;
case Status.DISCONNECTED:
return this._room.leave(errorCode);
case Status.DISCONNECTING:
throw Error(`You can't disconnect to a conference in ${Status[this.getStatus()]} status`);
}
this._setStatus(Status.DISCONNECTING);
if (this.canManage()) {
try {
this._outgoingInvitations.forEach((invitation) => {
this.cancelInvitation(invitation);
});
} catch (error) {
log.error(error);
}
}
try {
return this._room.leave();
} catch (error) {
if (!error.message.match(/already disconnected/)) {
throw error;
}
} finally {
this._setStatus(Status.DISCONNECTED);
}
}
/**
* Local hold
* Stop publishing and subscribing to the media in the conference
* @return {Promise} resolved on success
*/
hold() {
return this._room.hold();
}
/**
* Stop local hold
* Republishes all previously published media, subscribes
* to previously subscribed media and notifies new status to
* other participants.
* @return {Promise} resolved on success
*/
unhold() {
return this._room.unhold();
}
/**
* Check if conference is on hold state.
* @type {boolean}
*/
get isOnHold() {
return this._room.isOnHold;
}
/**
* Create an unattendedCallTransfer towards a user in the system
* @param {string} participant
* @param {MediaTypes} [mediaTypes] the current media types will be used if nothing is specified
* @returns {Promise}
*/
unattendedCallTransfer(participant, mediaTypes = this._localMediaHandler.getMediaTypes()) {
return this._userManager.resolveUser(participant).then((user) => {
let gatewayUsername = user && user.gatewayUsername ? user.gatewayUsername : participant;
return this._room.unattendedCallTransfer(gatewayUsername, mediaTypes);
});
}
/**
* Send data to the audio's room
* @param {string} value
* @returns {Promise<undefined, Error}
*/
sendDtmf(value) {
if (this.getStatus() !== Status.CONNECTED) {
return Promise.reject(new Error('You aren\'t joined to the conference'));
}
return this._room.sendDtmf(value);
}
}