Home Reference Source

src/conferences/ConferenceManager.js

import {cloneDeep} from 'lodash-es';

import {EventEmitter} from '../eventemitter';
import {bindMethods} from '../utils';

import {Conference} from './Conference';
import {ConferenceLog} from './ConferenceLog';

import Logs from '../Logs';
const log = Logs.instance.getLogger('SippoJS/calls/ConferenceManager');

/**
 * This class provides access to every conference related feature
 * This class must never be directly instantiated, instances of this class
 * can be obtained using Session#getConferenceManager.
 *
 * ## Events
 *
 * - `conferenceInvitation` {@link Conference} - Emitted every time an invitation to
 *    a conference room is received
 * - `conferenceInvitationRequest` {@link any} - Emitted every time a request for
 *    an invitation to a conference room is received
 * - `conferenceInvitationRequestCancelled` {@link any} -
 */
export class ConferenceManager {
	/**
	 * @protected
	 * @param {WacProxy} wacProxy
	 * @param {UserManager} userManager
	 */
	static newInstance(wacProxy, userManager) {
		return new ConferenceManager(wacProxy, userManager);
	}

	/**
	 * @private
	 */
	constructor(wacProxy, userManager) {
		/** @type {WacProxy} */
		this._wacProxy = wacProxy;
		/** @type {UserManager} */
		this._userManager = userManager;
		this._defaultMediaConstraints = {
			audio: true,
			video: true,
		};
		this._conferences = new Map();
		this._conferenceLog = null;
		// [SIPPOJS-493] The cancel can arrive while the username is being resolved
		// This array keeps the invitation requests whose incoming process is in progress
		this._incomingInvitationRequestIds = new Set();
		/** @type {EventEmitter} */
		this.emitter = new EventEmitter();

		bindMethods(this, [
			'_onIncomingConference',
			'_onConferenceInvitationRequest',
			'_onConferenceInvitationRequestCancelled',
		]);
		this._bindEventHandlers();

		/*
		let existingConferences = this._wacProxy.conference.fetch() || [];
		existingConferences.forEach(stackConference => {
			this._onIncomingConference(stackConference, mediaConstraints, true).catch(console.error);
		});
		*/
	}

	/**
	 * @private
	 */
	_bindEventHandlers() {
		this._wacProxy.conference.on('incomingConference', this._onIncomingConference);
		this._wacProxy.conference.on('invitationRequest', this._onConferenceInvitationRequest);
		this._wacProxy.conference.on('cancelInvitationRequest', this._onConferenceInvitationRequestCancelled);
	}

	/**
	 * @private
	 */
	async _onIncomingConference(room, mediaTypes) {
		const conference = await Conference.create(this._userManager, room, this._defaultMediaConstraints);
		await conference.getLocalMediaHandler().setMediaTypes(mediaTypes);
		this._conferences.set(room.id, conference);
		this.emitter.emit('conferenceInvitation', conference);
	}

	/**
	 * @private
	 */
	_onConferenceInvitationRequest(event) {
		this._incomingInvitationRequestIds.add(event.id);
		let [gatewayUsername, session] = event.user.split('/');
		let call = {
			id: event.id,
			from: gatewayUsername,
			hasRemoteVideo: () => event.video,
			hasStatus: () => true,
			getRemoteUser: () => call.remoteUser,
			getRemoteGatewayUser: () => gatewayUsername,
			acceptInvitationRequest: () => event.accept(),
			_connect: (conference) => {
				call.status = 'trying';
				return conference.inviteParticipant(gatewayUsername, session, {
					audio: true,
					video: call.hasRemoteVideo(),
				}, true, event.id, false);
			},
			_disconnect: () => {
				event.reject();
			},
		};
		this._userManager.getUsername(gatewayUsername).then((username) => {
			return this._userManager.resolveUser(username);
		}).then((user) => {
			// Detect if an invitation request cancel has arrived while we were resolving the user
			if (!this._incomingInvitationRequestIds.has(event.id)) {
				return;
			}
			this._incomingInvitationRequestIds.delete(event.id);

			call.remoteUser = user || gatewayUsername;
			this.emitter.emit('conferenceInvitationRequest', call);
		});
	}

	/**
	 * @private
	 */
	_onConferenceInvitationRequestCancelled(event) {
		this._incomingInvitationRequestIds.delete(event.id);

		this.emitter.emit('conferenceInvitationRequestCancelled', event);
	}

	/**
	 * @return {MediaStreamConstraints}
	 */
	getDefaultMediaConstraints() {
		return cloneDeep(this._defaultMediaConstraints);
	}

	/**
	 * Sets the constraints that will be used as initial value for media requests when
	 * initializing conferences. For more information about valid values see
	 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints}
	 * @example <caption>Enabling noise reduction techniques</caption>
	 * conferenceManager.setDefaultMediaConstraints({
	 *   audio: {
	 *     autoGainControl: true,
	 *     echoCancellation: true,
	 *     noiseSuppression: true,
	 *   },
	 *   video: true,
	 * });
	 * @param {MediaStreamConstraints} defaultMediaConstraints
	 */
	setDefaultMediaConstraints(defaultMediaConstraints) {
		this._defaultMediaConstraints = cloneDeep(defaultMediaConstraints);
	}

	/**
	 * Creates a new conference room
	 * @return {Promise<Conference>}
	 */
	createConference() {
		return this._wacProxy.conference.create().then((room) => {
			return Conference.create(this._userManager, room, this._defaultMediaConstraints);
		});
	}

	/**
	 * @return {ConferenceLog}
	 */
	getConferenceLog() {
		if (this._conferenceLog === null) {
			this._conferenceLog = new ConferenceLog(this._wacProxy.getConferenceLogService());
		}
		return this._conferenceLog;
	}

	/**
	 * @param {string} participant
	 * @param {object} mediaTypes
	 * @returns {Promise<object>} fulfilled when the response is received
	 */
	requestConferenceInvitation(participant, mediaTypes = {audio: true, video: true}) {
		return this._userManager.resolveUser(participant).then((user) => {
			let gwUsername = user && user.gatewayUsername ? user.gatewayUsername : participant;
			let call = {
				getRemoteUser: () => user,
				to: gwUsername,
				hasLocalVideo: () => !!mediaTypes.video,
				hasLocalOnlyVideo: () => false,
				_disconnect: () => {
					this._wacProxy.conference.cancelInvitationRequest(gwUsername);
				},
				_connect: () => {
					return this._wacProxy.conference.requestInvitation(gwUsername, mediaTypes);
				},
			};
			return call;
		});
	}

	async _getConferenceForRoom(room) {
		const conference = await Conference.create(this._userManager, room, this._defaultMediaConstraints);
		await conference.getLocalMediaHandler().setMediaTypes(room.invite.mediatypes);
		this._conferences.set(room.id, conference);
		return conference;
	}

	/**
	 * Asks the server for the current invitations available to the user
	 * @return {Promise<Conference[]>}
	 */
	getCurrentInvitations() {
		log.debug('calls/ConferenceManager.js/getCurrentInvitations');
		return this._wacProxy.conference.getCurrentInvitations().then((rooms) => {
			log.debug('calls/ConferenceManager.js/getCurrentInvitations/rooms', rooms);
			let conferences = rooms.map(async (room) => {
				if (room.isRequest) {
					delete room.isRequest;
					this._onConferenceInvitationRequest(room);
					return room;
				} else {
					const conf = await this._getConferenceForRoom(room);
					this.emitter.emit('conferenceInvitation', conf);
					return conf;
				}
			});
			return Promise.all(conferences);
		});
	}

	/**
	 * Get a Conference by uri
	 *
	 * @param {string} uri
	 * @returns {Promise<Conference>} fulfilled when the response is received
	 */
	async getConferenceByUri(uri) {
		const room = await this._wacProxy.conference.getRoomByUri(uri);
		return await Conference.create(this._userManager, room, this._defaultMediaConstraints);
	}
}