Home Reference Source

src/calls/CallManager.js

import {List, Set} from 'immutable';
import {cloneDeep} from 'lodash-es';

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

import {Call} from './Call';
import {CallDirection} from './CallDirection';
import {CallLog} from './CallLog';
import {CallStatus} from './CallStatus';
import {RemoteCall} from './RemoteCall';

const log = Logs.instance.getLogger('SippoJS/CallManager');

/**
 * This class provides access to every call related feature
 * This class must never be directly instantiated, instances of this class
 * can be obtained using {@link Session#getCallManager}.
 *
 * ## Events
 *
 * - `incomingCall` ({@link Call}) - Emitted every time a new incoming call is received.
 */
export class CallManager {
	/**
	 * @protected
	 * @param {WacProxy} wacProxy
	 * @param {UserManager} userManager
	 * @param {string} ownGatewayUsername
	 */
	static newInstance(wacProxy, userManager, ownGatewayUsername) {
		return new CallManager(wacProxy, userManager, ownGatewayUsername);
	}

	/**
	 * Creates a new call manager
	 * @protected
	 */
	constructor(wacProxy, userManager, ownGatewayUsername) {
		this._wacProxy = wacProxy;
		this._callAttemptService = wacProxy.getCallAttemptService();
		this._userManager = userManager;
		this._defaultMediaConstraints = {
			audio: true,
			video: true,
		};
		this._ownGatewayUsername = ownGatewayUsername;

		this._pollingCalls = new List();
		this._incomingCalls = new Set();
		this._outgoingCalls = new Set();
		this._callLog = null;

		/** @type {EventEmitter} */
		this.emitter = new EventEmitter();

		bindMethods(this, [
			'_onCallAttemptCreated',
			'_onRinging',
		]);
		this._bindEventHandlers();
	}

	/**
	 * @private
	 */
	_bindEventHandlers() {
		this._callAttemptService.emitter.on('created', this._onCallAttemptCreated);
		this._wacProxy.on('qs-ringing', this._onRinging);
	}

	/**
	 * @private
	 */
	_onCallAttemptCreated(event) {
		this.userManager.resolveUser(event.caller).then(user => Call.newInstance(this.wacProxy, this.userManager, {
			gatewayCaller: event.caller,
			gatewayCallee: this._ownGatewayUsername,
			remoteUser: user,
			pollingCall: true,
		})).then((call) => {
			call.setContext(event.context);
			this._addIncomingCall(call);
			this.emitter.emit('incomingCall', call);
		}).catch(error => log.error(error));
	}

	/**
	 * @private
	 */
	async _onRinging(event) {
		const isDatapipe = event.media && event.media.data;
		const isReconnection = this._incomingCalls.find(c => c.getRemoteGatewayUser() === event.userid);
		if (isDatapipe || isReconnection) {
			return;
		}
		try {
			const user = await this._userManager.resolveUser(event.userid);
			const call = await Call.newInstance(this._wacProxy, this._userManager, {
				id: event.callid,
				gatewayCaller: event.userid,
				gatewayCallee: this._ownGatewayUsername,
				remoteUser: user,
				mediaConstraints: this._defaultMediaConstraints,
			});
			await call.getLocalMediaHandler().setMediaTypes({
				audio: event.media.audio,
				video: event.media.video,
				screen: false,
			});
			this._addIncomingCall(call);
			this.emitter.emit('incomingCall', call);
		} catch (error) {
			log.error(error);
		}
	}

	_addIncomingCall(call) {
		this._incomingCalls = this._incomingCalls.add(call);
		call.waitForStatus(CallStatus.DISCONNECTED).then(() => {
			this._incomingCalls = this._incomingCalls.delete(call);
		});
	}

	_addOutgoingCall(call) {
		this._outgoingCalls = this._outgoingCalls.add(call);
		call.waitForStatus(CallStatus.DISCONNECTED).then(() => {
			this._outgoingCalls = this._outgoingCalls.delete(call);
		});
	}

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

	/**
	 * Sets the constraints that will be used as initial value for media requests when
	 * initializing calls. 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>
	 * callManager.setDefaultMediaConstraints({
	 *   audio: {
	 *     autoGainControl: true,
	 *     echoCancellation: true,
	 *     noiseSuppression: true,
	 *   },
	 *   video: true,
	 * });
	 * @param {MediaStreamConstraints} defaultMediaConstraints
	 */
	setDefaultMediaConstraints(defaultMediaConstraints) {
		this._defaultMediaConstraints = cloneDeep(defaultMediaConstraints);
	}

	/**
	 * @return {Promise<CallLog>}
	 */
	getCallLog() {
		if (this._callLog === null) {
			const callService = this._wacProxy.getCallService();
			this._callLog = CallLog.create(callService, this._userManager);
		}
		return this._callLog;
	}

	/**
	 * Creates a new call instance for communication with the specified recipient.
	 * @param {String} to The user identifier of the call recipient.
	 * @return {Promise<Call, TypeError>}
	 */
	createCall(to) {
		if (typeof to !== 'string') {
			return Promise.reject(new TypeError(`${to} is not a string`));
		}
		return this._userManager.resolveUser(to).then((user) => {
			const gwUsername = user ? user.gatewayUsername : to;
			return Call.newInstance(this._wacProxy, this._userManager, {
				direction: CallDirection.OUTGOING,
				gatewayCaller: this._ownGatewayUsername,
				gatewayCallee: gwUsername,
				remoteUser: user,
				mediaConstraints: this._defaultMediaConstraints,
			});
		}).then((call) => {
			this._addOutgoingCall(call);
			return call;
		});
	}

	/**
	 * Get a list of remote calls where user is currently participating
	 * @return {Promise<RemoteCall[]>}
	 */
	getRemoteCalls() {
		return Promise.resolve().then(() => this._wacProxy.getRemoteSessions()).then((sessions) => {
			const calls = [];
			sessions.forEach((session) => {
				session.calls.forEach((call) => {
					const dev = new RemoteSession(session.id, session.name);
					const item = new RemoteCall(call.id, call.participants, dev);
					calls.push(item);
				});
			});
			return calls;
		});
	}

	/**
	 * Pulls remote call from remote session.
	 *
	 * @example
	 *  const callManager = await session.getCallManager();
	 *  const calls = await callManager.getRemoteCalls();
	 *  console.log('got remote calls', calls);
	 *  if (!calls) {
	 *    return;
	 *  }
	 *  const call = await cm.pull(calls[0]);
	 *  await call.connect();
	 *
	 * @param {RemoteCall} remoteCall
	 * @return {Promise<Call, Error>}
	 */
	async pull(remoteCall) {
		const user = await this._userManager.resolveUser(remoteCall.participants[0]);
		const call = await Call.newInstance(this._wacProxy, this._userManager, {
			direction: CallDirection.OUTGOING,
			gatewayCaller: this._ownGatewayUsername,
			gatewayCallee: remoteCall.participants[0],
			remoteUser: user,
			defaultMediaConstraints: this._defaultMediaConstraints,
			pullFrom: remoteCall,
		});
		return call;
	}
}