vertinho
Version:
Library to make conference apps and softphones through WebSockets with FreeSWITCH mod_verto.
449 lines (374 loc) • 12.3 kB
JavaScript
/**
* _ _ _
* | | (_) | |
* __ _____ _ __| |_ _ _ __ | |__ ___
* \ \ / / _ \ '__| __| | '_ \| '_ \ / _ \
* \ V / __/ | | |_| | | | | | | | (_) |
* \_/ \___|_| \__|_|_| |_|_| |_|\___/
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* And see https://github.com/Mazuh/vertinho for the full license details.
*/
import VertoRTC from '../webrtc/VertoRTC';
import { printError, printWarning } from '../common/utils';
import { generateGUID, ENUM } from './utils';
export default class Call {
constructor(direction, verto, params, mediaHandlers = {}) {
this.direction = direction;
this.verto = verto;
this.params = {
callID: generateGUID(),
useVideo: verto.options.useVideo,
useStereo: verto.options.useStereo,
screenShare: false,
useCamera: false,
useMic: verto.options.deviceParams.useMic,
useSpeak: verto.options.deviceParams.useSpeak,
remoteVideo: verto.options.remoteVideo,
remoteAudioId: verto.options.remoteAudioId,
localVideo: verto.options.localVideo,
login: verto.options.login,
videoParams: verto.options.videoParams,
...params,
};
this.mediaHandlers = mediaHandlers;
if (!this.params.screenShare) {
this.params.useCamera = verto.options.deviceParams.useCamera;
}
this.answered = false;
this.lastState = ENUM.state.new;
this.state = this.lastState;
this.verto.calls[this.params.callID] = this;
if (this.direction === ENUM.direction.inbound) {
if (this.params.display_direction === 'outbound') {
this.params['remote_caller_id_name'] = this.params['caller_id_name'] || 'NOBODY';
this.params['remote_caller_id_number'] = this.params['caller_id_number'] || 'UNKNOWN';
} else {
this.params['remote_caller_id_name'] = this.params['callee_id_name'] || 'NOBODY';
this.params['remote_caller_id_number'] = this.params['callee_id_number'] || 'UNKNOWN';
}
} else {
this.params['remote_caller_id_name'] = 'OUTBOUND CALL';
this.params['remote_caller_id_number'] = this.params['destination_number'];
}
this.bootstrapRealtimeConnection();
if (this.direction === ENUM.direction.inbound) {
if (this.params.attach) {
this.answer();
} else {
this.ring();
}
}
}
bootstrapRealtimeConnection() {
const callbacks = {
onICESDP: () => {
const { requesting, answering, active } = ENUM.state;
if ([requesting, answering, active].includes(this.state)) {
printError('This ICE SDP should not being received, reload your page!');
return;
}
const isActivelyCalling = this.rtc.type === 'offer';
const options = { sdp: this.rtc.mediaData.SDP };
if (isActivelyCalling) {
if (this.state === active) {
this.setState(requesting);
this.broadcastMethod('verto.attach', options);
} else {
this.setState(requesting);
this.broadcastMethod('verto.invite', options);
}
} else {
this.setState(answering);
this.broadcastMethod(this.params.attach ? 'verto.attach' : 'verto.answer', options);
}
},
onPeerStreaming: (stream) => {
this.verto.options.onPeerStreaming(stream);
},
onPeerStreamingError: (error) => {
this.verto.options.onPeerStreamingError(error);
this.hangup({ cause: 'Device or Permission Error' });
},
};
this.rtc = new VertoRTC({
callbacks,
mediaHandlers: this.mediaHandlers,
localVideo: this.params.screenShare ? null : this.params.localVideo,
useVideo: this.params.remoteVideo,
useAudio: this.params.remoteAudioId,
videoParams: this.params.videoParams || {},
audioParams: this.verto.options.audioParams || {},
iceServers: this.verto.options.iceServers,
screenShare: this.params.screenShare,
useCamera: this.params.useCamera,
useMic: this.params.useMic,
useSpeak: this.params.useSpeak,
});
}
broadcastMethod(method, options) {
const { noDialogParams, ...methodParams } = options;
const dialogParams = Object.keys(this.params).reduce((accumulator, currentKey) => {
if (currentKey === 'sdp' && method !== 'verto.invite' && method !== 'verto.attach') {
return accumulator;
}
if (currentKey === 'callID' && noDialogParams === true) {
return accumulator;
}
return { ...accumulator, [currentKey]: this.params[currentKey] };
}, {});
const handleMethodResponseFn = success => x => this.handleMethodResponse(method, success, x);
this.verto.publish(method, {
...methodParams,
dialogParams,
}, handleMethodResponseFn(true), handleMethodResponseFn(false));
}
setState(state) {
if (this.state === ENUM.state.ringing) {
this.stopRinging();
}
const checkStateChange = state === ENUM.state.purge || ENUM.states[this.state.name][state.name];
if (this.state === state || !checkStateChange) {
printError(`Invalid call state change from ${this.state.name} to ${state.name}. ${this}`);
this.hangup();
return false;
}
this.lastState = this.state;
this.state = state;
this.verto.callbacks.onCallStateChange({
previous: this.lastState,
current: this.state,
});
const speaker = this.params.useSpeak;
const useCustomSpeaker = speaker && speaker !== 'any' && speaker !== 'none';
const isAfterRequesting = this.lastState.val > ENUM.state.requesting.val;
const isBeforeHangup = this.lastState.val < ENUM.state.hangup.val;
switch (this.state) {
case ENUM.state.early:
case ENUM.state.active:
if (useCustomSpeaker) {
printWarning('Custom speaker not supported, ignoring.');
}
break;
case ENUM.state.trying:
setTimeout(() => {
if (this.state === ENUM.state.trying) {
printWarning(`Turning off after 3s of trying. ${this}`);
this.setState(ENUM.state.hangup);
}
}, 3000);
break;
case ENUM.state.purge:
this.setState(ENUM.state.destroy);
break;
case ENUM.state.hangup:
if (isAfterRequesting && isBeforeHangup) {
this.broadcastMethod('verto.bye', {});
}
this.setState(ENUM.state.destroy);
break;
case ENUM.state.destroy:
delete this.verto.calls[this.params.callID];
if (this.params.screenShare) {
this.rtc.stopPeer();
} else {
this.rtc.stop();
}
break;
default:
break;
}
return true;
}
handleMethodResponse(method, success, response) {
switch (method) {
case 'verto.answer':
case 'verto.attach':
if (success) {
this.setState(ENUM.state.active);
} else {
this.hangup();
}
break;
case 'verto.invite':
if (success) {
this.setState(ENUM.state.trying);
} else {
this.setState(ENUM.state.destroy);
}
break;
case 'verto.bye':
this.hangup();
break;
case 'verto.modify':
if (response.holdState === 'held' && this.state !== ENUM.state.held) {
this.setState(ENUM.state.held);
}
if (response.holdState === 'active' && this.state !== ENUM.state.active) {
this.setState(ENUM.state.active);
}
break;
default:
break;
}
}
hangup(params) {
if (params) {
this.causeCode = params.causeCode;
this.cause = params.cause;
}
if (!this.cause && !this.causeCode) {
this.cause = 'NORMAL_CLEARING';
}
const isNotNew = this.state.val >= ENUM.state.new.val;
const didntHangupYet = this.state.val < ENUM.state.hangup.val;
if (isNotNew && didntHangupYet) {
this.setState(ENUM.state.hangup);
}
const didntDestroyYet = this.state.val < ENUM.state.destroy;
if (didntDestroyYet) {
this.setState(ENUM.state.destroy);
}
}
stopRinging() {
if (!this.verto.ringer) {
return;
}
this.verto.ringer.getTracks().forEach(ringer => ringer.stop());
}
indicateRing() {
if (!this.verto.ringer) {
printWarning(`Call is ringing, but no ringer set. ${this}`);
return;
}
if (!this.verto.ringer.src && this.verto.options.ringFile) {
this.verto.ringer.src = this.verto.options.ringFile;
}
this.verto.ringer.play();
setTimeout(() => {
this.stopRinging();
if (this.state === ENUM.state.ringing) {
this.indicateRing();
} else {
printWarning(`Call stopped ringing, but no ringer set. ${this}`);
}
}, this.verto.options.ringSleep);
}
ring() {
this.setState(ENUM.state.ringing);
this.indicateRing();
}
sendTouchtone(digit) {
this.broadcastMethod('verto.info', { dtmf: digit });
}
sendRealTimeText({ code, chars }) {
this.broadcastMethod('verto.info', { txt: { code, chars }, noDialogParams: true });
}
transferTo(destination) {
this.broadcastMethod('verto.modify', { action: 'transfer', destination });
}
hold() {
this.broadcastMethod('verto.modify', { action: 'hold' });
}
unhold() {
this.broadcastMethod('verto.modify', { action: 'unhold' });
}
toggleHold() {
this.broadcastMethod('verto.modify', { action: 'toggleHold' });
}
sendMessageTo(to, body) {
this.broadcastMethod('verto.info', { msg: { from: this.params.login, to, body } });
}
answer() {
if (this.answered) {
return;
}
this.rtc.createAnswer(this.params);
this.answered = true;
}
handleAnswer(sdp) {
this.gotAnswer = true;
if (this.state.val >= ENUM.state.active.val) {
return;
}
const afterOrAtEarly = this.state.val >= ENUM.state.early.val;
if (afterOrAtEarly) {
this.setState(ENUM.state.active);
return;
}
const shouldDelayForNow = this.gotEarly;
if (shouldDelayForNow) {
return;
}
this.rtc.answer(sdp, () => {
this.setState(ENUM.state.active);
}, (error) => {
printError('Error while answering', error);
this.hangup();
});
}
getDestinationNumber() {
return this.params['destination_number'];
}
getId() {
return this.params.callID;
}
getCallerIdentification({ useCaracterEntities }) {
return [
this.params['remote_caller_id_name'],
' ',
useCaracterEntities ? '<' : '<',
this.params['remote_caller_id_number'],
useCaracterEntities ? '>' : '>',
].join('');
}
handleInfo(params) {
this.verto.callbacks.onInfo(params);
}
handleDisplay(displayName, displayNumber) {
if (displayName !== undefined) {
this.params['remote_caller_id_name'] = displayName;
}
if (displayNumber !== undefined) {
this.params['remote_caller_id_number'] = displayNumber;
}
this.verto.callbacks.onDisplay({
name: displayName,
number: displayNumber,
});
}
handleMedia(sdp) {
if (this.state.val >= ENUM.state.early.val) {
return;
}
this.gotEarly = true;
this.rtc.answer(sdp, () => {
this.setState(ENUM.state.early);
if (this.gotAnswer) {
this.setState(ENUM.state.active);
}
}, (error) => {
printError('Error on answering early', error);
this.hangup();
});
}
toString() {
const {
callID: id,
destination_number: destination,
caller_id_name: callerName,
caller_id_number: callerNumber,
remote_caller_id_name: calleeName,
remote_caller_id_number: calleeNumber,
} = this.params;
const attributes = [
{ key: 'id', value: id },
{ key: 'destination', value: destination },
{ key: 'from', value: `${callerName} (${callerNumber})` },
{ key: 'to', value: `${calleeName} (${calleeNumber})` },
].map(({ key, value }) => `${key}: "${value}"`).join(', ');
return `Call<${attributes}>`;
}
}