UNPKG

vertinho

Version:

Library to make conference apps and softphones through WebSockets with FreeSWITCH mod_verto.

512 lines (440 loc) 15.1 kB
/** * _ _ _ * | | (_) | | * __ _____ _ __| |_ _ _ __ | |__ ___ * \ \ / / _ \ '__| __| | '_ \| '_ \ / _ \ * \ 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 Call from './Call'; import ConferenceManager from '../conference/ConferenceManager'; import ConferenceLiveArray from '../conference/ConferenceLiveArray'; import { printError, printWarning } from '../common/utils'; import { generateGUID, ENUM } from './utils'; export default class VertinhoClient { constructor(params, vertoCallbacks = {}, conferenceCallbacks = {}) { this.params = { ...params }; const defaultCallback = x => x; this.callbacks = { onClientReady: defaultCallback, onConferenceReady: defaultCallback, onConferenceDisabled: defaultCallback, onInfo: defaultCallback, onDisplay: defaultCallback, onCallStateChange: defaultCallback, onPrivateEvent: defaultCallback, ...vertoCallbacks, }; this.conferenceCallbacks = { onReady: defaultCallback, onDestroyed: defaultCallback, onBootstrappedMembers: defaultCallback, onAddedMember: defaultCallback, onModifiedMember: defaultCallback, onRemovedMember: defaultCallback, onChatMessage: defaultCallback, onInfo: defaultCallback, onModeration: defaultCallback, ...conferenceCallbacks, }; this.webSocket = null; this.webSocketCallbacks = {}; this.retryingTimer = null; this.currentWebSocketRequestId = 0; this.options = {}; this.calls = {}; this.conference = null; this.connect(); } connect() { this.options = { webSocket: { login: '', password: '', url: '', }, videoParams: {}, audioParams: {}, loginParams: {}, deviceParams: {}, userVariables: {}, iceServers: false, ringSleep: 6000, sessid: null, onmessage: event => this.handleMessage(event.eventData), onWebSocketLoginSuccess: () => {}, onWebSocketLoginError: error => printError('Error reported by WebSocket login', error), onPeerStreaming: () => {}, onPeerStreamingError: () => {}, ...this.params, ...this.callbacks, }; if (!this.options.deviceParams.useMic) { this.options.deviceParams.useMic = 'any'; } if (!this.options.deviceParams.useSpeak) { this.options.deviceParams.useSpeak = 'any'; } if (!this.options.blockSessionRecovery) { if (this.options.sessid) { this.sessid = this.options.sessid; } else { this.sessid = localStorage.getItem('verto_session_uuid') || generateGUID(); localStorage.setItem('verto_session_uuid', this.sessid); } } else { this.sessid = generateGUID(); } this.calls = {}; this.callbacks = this.callbacks || {}; this.webSocketSubscriptions = {}; this.connectSocket(); } connectSocket() { if (this.retryingTimer) { clearTimeout(this.retryingTimer); } if (this.socketReady()) { printWarning('Tried to connect to socket but already had a ready one'); return; } this.authing = false; if (this.webSocket) { delete this.webSocket; } this.webSocket = new WebSocket(this.options.webSocket.url); this.webSocket.onmessage = this.onWebSocketMessage.bind(this); this.webSocket.onclose = () => { printWarning('WebSocket closed, attempting to connect again in 1s.'); this.retryingTimer = setTimeout(this.connectSocket.bind(this), 1000); }; this.webSocket.onopen = () => { if (this.retryingTimer) { printWarning('Successfully WebSocket attempt to reconnect.'); clearTimeout(this.retryingTimer); } this.publish('login', {}); }; } socketReady() { if (this.webSocket === null || this.webSocket.readyState > 1) { return false; } return true; } purge() { Object.keys(this.calls).forEach((callId) => { this.calls[callId].setState(ENUM.state.purge); }); this.webSocketSubscriptions = {}; } publish(method, params = {}, onSuccess = x => x, onError = x => x) { this.currentWebSocketRequestId += 1; const request = { jsonrpc: '2.0', method, params: { sessid: this.sessid, ...params }, id: this.currentWebSocketRequestId, }; const requestStringified = JSON.stringify(request); if ('id' in request && onSuccess !== undefined) { this.webSocketCallbacks[request.id] = { requestStringified, request, onSuccess, onError, }; } this.webSocket.send(requestStringified); } handleJSONRPCMessage(message) { if (message.result) { const { onSuccess } = this.webSocketCallbacks[message.id]; delete this.webSocketCallbacks[message.id]; onSuccess(message.result, this); return; } if (!message.error) { return; } if (!this.authing && parseInt(message.error.code, 10) === -32000) { this.authing = true; this.publish('login', { login: this.options.webSocket.login, passwd: this.options.webSocket.password, loginParams: this.options.loginParams, userVariables: this.options.userVariables, }, () => { this.authing = false; delete this.webSocketCallbacks[message.id]; this.options.onWebSocketLoginSuccess(); }, () => { delete this.webSocketCallbacks[message.id]; this.options.onWebSocketLoginError(message.error); }); return; } const { onError } = this.webSocketCallbacks[message.id]; delete this.webSocketCallbacks[message.id]; onError(message.error, this); } onWebSocketMessage(event) { const message = JSON.parse(event.data); if (message && message.jsonrpc === '2.0' && this.webSocketCallbacks[message.id]) { this.handleJSONRPCMessage(message); return; } if (typeof this.options.onmessage !== 'function') { return; } const fixedEvent = { ...event, eventData: message || {} }; const reply = this.options.onmessage(fixedEvent); if (typeof reply !== 'object' || !fixedEvent.eventData.id || !this.webSocket) { return; } this.webSocket.send(JSON.stringify({ jsonrpc: '2.0', id: fixedEvent.eventData.id, result: reply, })); } handleMessage(data) { if (!data || !data.method || !data.params) { printError('Invalid WebSocket message', data); return; } if (data.params.eventType === 'channelPvtData') { this.handleChannelPrivateDataMessage(data); } else if (data.params.callID) { this.handleMessageForCall(data); } else { this.handleMessageForClient(data); } } handleChannelPrivateDataMessage(data) { const { params: event } = data; const existingConference = this.conference && { ...this.conference }; if (event.pvtData.action === 'conference-liveArray-join') { if (existingConference) { printWarning('Ignoring doubled private event of live array join', event); return; } const conference = { creationEvent: event, privateEventChannel: event.eventChannel, memberId: event.pvtData.conferenceMemberID, role: event.pvtData.role, manager: new ConferenceManager(this, { chat: { channel: event.pvtData.chatChannel, handler: this.conferenceCallbacks.onChatMessage, }, info: { channel: event.pvtData.infoChannel, handler: this.conferenceCallbacks.onInfo, }, moderation: event.pvtData.modChannel ? null : { channel: event.pvtData.modChannel, handler: this.conferenceCallbacks.onModeration, }, }), liveArray: new ConferenceLiveArray( this, event.pvtData.laChannel, event.pvtData.laName, { onBootstrappedMembers: this.conferenceCallbacks.onBootstrappedMembers, onAddedMember: this.conferenceCallbacks.onAddedMember, onModifiedMember: this.conferenceCallbacks.onModifiedMember, onRemovedMember: this.conferenceCallbacks.onRemovedMember, }, ), }; this.conference = conference; this.conferenceCallbacks.onReady(conference); } else if (event.pvtData.action === 'conference-liveArray-part') { if (!existingConference) { printWarning('Ignoring event of live array part without conference instance', event); return; } existingConference.manager.destroy(); existingConference.liveArray.destroy(); this.conference = null; this.conferenceCallbacks.onDestroyed(existingConference); } else { printWarning('Not implemented private data message', data); } } handleMessageForClient(data) { const channel = data.params.eventChannel; const subscription = channel && this.webSocketSubscriptions[channel]; switch (data.method) { case 'verto.punt': this.destroy(); break; case 'verto.event': if (!subscription && channel === this.sessid) { this.callbacks.onPrivateEvent(data.params); } else if (!subscription && channel && this.calls[channel]) { this.callbacks.onPrivateEvent(data.params); } else if (!subscription) { printWarning('Ignoring event for unsubscribed channel', channel, data.params); } else if (!subscription || !subscription.ready) { printError('Ignoring event for a not ready channel', channel, data.params); } else if (subscription.handler) { subscription.handler(data.params, subscription.userData); } else if (this.callbacks.onEvent) { this.callbacks.onEvent(this, data.params, subscription.userData); } else { printWarning('Ignoring event without callback', channel, data.params); } break; case 'verto.info': this.callbacks.onInfo(data.params); break; case 'verto.clientReady': this.callbacks.onClientReady(data.params); break; default: printWarning('Ignoring invalid method with no call id', data.method); break; } } handleMessageForCall(data) { const existingCall = this.calls[data.params.callID]; if (existingCall) { switch (data.method) { case 'verto.bye': existingCall.hangup(data.params); break; case 'verto.answer': existingCall.handleAnswer(data.params.sdp); break; case 'verto.media': existingCall.handleMedia(data.params.sdp); break; case 'verto.display': existingCall.handleDisplay(data.params['display_name'], data.params['display_number']); break; case 'verto.info': existingCall.handleInfo(data.params); break; default: printWarning('Ignoring existing call event with invalid method', data.method); break; } } else if (data.method === 'verto.attach' || data.method === 'verto.invite') { const newCall = new Call(ENUM.direction.inbound, this, { ...data.params, attach: true, useVideo: data.params.sdp && data.params.sdp.indexOf('m=video') > 0, useStereo: data.params.sdp && data.params.sdp.indexOf('stereo=1') > 0, }); if (data.method === 'verto.attach') { newCall.setState(ENUM.state.recovering); } } else { printWarning('Ignoring call event with invalid method', data.method); } } processReply(method, { subscribedChannels, unauthorizedChannels }) { if (method !== 'verto.subscribe') { return; } Object.keys(subscribedChannels || {}).forEach((channelKey) => { const channel = subscribedChannels[channelKey]; this.setReadySubscription(channel); }); Object.keys(unauthorizedChannels || {}).forEach((channelKey) => { const channel = unauthorizedChannels[channelKey]; printError('Unauthorized', channel); this.setDroppedSubscription(channel); }); } setDroppedSubscription(channel) { delete this.webSocketSubscriptions[channel]; } setReadySubscription(channel) { const subscription = this.webSocketSubscriptions[channel]; if (subscription) { subscription.ready = true; } } broadcastMethod(method, params) { const reply = event => this.processReply(method, event); this.publish(method, params, reply, reply); } broadcast(eventChannel, data) { this.broadcastMethod('verto.broadcast', { eventChannel, data }); } subscribe(eventChannel, params = {}) { const eventSubscription = { eventChannel, handler: params.handler, userData: params.userData, ready: false, }; if (this.webSocketSubscriptions[eventChannel]) { printWarning('Overwriting an already subscribed channel', eventChannel); } this.webSocketSubscriptions[eventChannel] = eventSubscription; this.broadcastMethod('verto.subscribe', { eventChannel }); return eventSubscription; } unsubscribe(eventChannel) { delete this.webSocketSubscriptions[eventChannel]; this.broadcastMethod('verto.unsubscribe', { eventChannel }); } makeVideoCall({ callerName, ...params }, mediaHandlers = {}) { if (!callerName) { printError('No `callerName` parameter on making video call.'); } return this.makeCall({ callerName, useVideo: true, ...params }, mediaHandlers); } makeCall({ to, from, ...otherParams }, mediaHandlers = {}) { if (!to || !from) { printError('No `to` or `from` parameters on making call.'); return null; } const { callerName = 'Vertinho', ...params } = otherParams; params['destination_number'] = to; params['caller_id_number'] = from; params['caller_id_name'] = callerName; if (!this.socketReady()) { printError('Socket not ready.'); return null; } const call = new Call(ENUM.direction.outbound, this, params, mediaHandlers); call.rtc.inviteRemotePeerConnection(); return call; } destroy() { if (this.socketReady()) { this.webSocket.close(); this.purge(); } else { printError('Tried to close a not ready socket while destroying.'); } } hangup(callId) { if (callId) { const call = this.calls[callId]; if (call) { call.hangup(); } else { printError('Error on hanging up call', callId); } return; } Object.keys(this.calls).forEach((id) => { this.calls[id].hangup(); }); } }