UNPKG

@viewar/call

Version:

ViewAR Call

537 lines (467 loc) 13.5 kB
import { Subject } from 'rxjs'; import { createSocketConnection } from '../socket-connection'; import { CLIENT_CHANGE_LEFT } from '../socket-connection/constants/client-change-types'; import { MESSAGE_TYPE_BUSY, MESSAGE_TYPE_CALL_ACCEPTED, MESSAGE_TYPE_CALL_REFUSED, MESSAGE_TYPE_END_CALL, MESSAGE_TYPE_INCOMING_CALL, } from '../socket-connection/constants/message-types'; import { logDebug, logError } from '../utils'; import { CALL_ROLE_SUPPORT_AGENT, CALL_ROLE_USER, } from './constants/call-roles'; import { CALL_STATUS_ACTIVE, CALL_STATUS_CONNECTING, CALL_STATUS_INCOMING, CALL_STATUS_NONE, } from './constants/call-status'; import createSyncManager from './sync-manager'; export const createCallClient = (viewarApi, args = {}) => { const { cameras, coreInterface, sceneManager, trackers } = viewarApi; const { host, debug } = args; const incomingCallObservable = new Subject(); const acceptedCallObservable = new Subject(); const refusedCallObservable = new Subject(); const endedCallObservable = new Subject(); const lineBusyObservable = new Subject(); const disconnectObservable = new Subject(); const syncManager = createSyncManager(viewarApi, sendData, getData); let socketConnection = createSocketConnection({ host, debug }); let currentCall; let callStatus = CALL_STATUS_NONE; let callRole = CALL_ROLE_USER; return { connect, // Session handling join, leave, setData, // Call handling call, endCall, answerCall, rejectCall, // Data handling sendData, getData, reconnect, // Observables get incomingCall() { return incomingCallObservable; }, get acceptedCall() { return acceptedCallObservable; }, get refusedCall() { return refusedCallObservable; }, get endedCall() { return endedCallObservable; }, get lineBusy() { return lineBusyObservable; }, get disconnect() { return disconnectObservable; }, get clientsUpdate() { return socketConnection.clientChanged; }, get clients() { return socketConnection.clients; }, get error() { return socketConnection.error; }, get isSupportAgent() { return callRole === CALL_ROLE_SUPPORT_AGENT; }, get isUser() { return callRole === CALL_ROLE_USER; }, // State get connected() { return !!socketConnection && socketConnection.connected; }, get session() { return !!socketConnection && socketConnection.room; }, get id() { return !!socketConnection && socketConnection.id; }, get callActive() { return callStatus === CALL_STATUS_ACTIVE; }, }; // --------------------------------------------------------------------------------------------------------------------- // PUBLIC // --------------------------------------------------------------------------------------------------------------------- /** * Connect to the socket connection and subscribe to incoming messages. * * @returns {*} */ async function connect() { const success = await socketConnection.connect(host); if (success) { subscribe(); } return success; } /** * Join a session with a specific session id. * * @param sessionId The session id to join. * @param username Credentials: username (optional). * @param password Credentials: password (optional). * @param userData Object with app specific data. * @returns {boolean} Returns false if the operation failed (e.g. socket is not connected. */ async function join({ sessionId, username, password, userData }) { if (socketConnection.room !== sessionId) { return await socketConnection.joinRoom( sessionId, username, password, viewarApi.appConfig.appId, viewarApi.versionInfo.app, Object.assign({}, userData) ); } } /** * Leaves the current session. * * @returns {boolean} Success. */ async function leave() { return await socketConnection.leaveRoom(); } /** * Set own personal data (exposed to other clients in the same room). * * @param data Data as json object. */ function setData(data) { socketConnection.setClientData(data); } /** * Send a call to an existing client. * * @param args * @returns {boolean} Returns false if calling fails (e.g. no valid client id given. */ async function call(args) { const { id, role = CALL_ROLE_USER } = args; const client = socketConnection.clients.findIndex( (client) => client.id === id ); if (callStatus !== CALL_STATUS_NONE) { logError('Already in an existing call.'); return false; } if (client === -1) { logError('Invalid client id given as argument.'); return false; } const connectionId = generateConnectionId(); const ownRole = role === CALL_ROLE_USER ? CALL_ROLE_SUPPORT_AGENT : CALL_ROLE_USER; // Call the other client. await socketConnection.send( { id: socketConnection.id, connectionId, calleeId: id, calleeRole: role, ownRole, userData: socketConnection.clientData, }, MESSAGE_TYPE_INCOMING_CALL, id ); callStatus = CALL_STATUS_CONNECTING; callRole = ownRole; currentCall = { id, connectionId, }; // Set own status to busy. await socketConnection.setClientData({ available: false }); return true; } /** * End current call. * * @returns {boolean} Returns true if there is an active call. */ async function endCall() { if (callStatus === CALL_STATUS_CONNECTING) { callStatus = CALL_STATUS_NONE; } if (callStatus !== CALL_STATUS_ACTIVE) { logError('No active call.'); return false; } await socketConnection.send( { id: currentCall.id, }, MESSAGE_TYPE_END_CALL, currentCall.id ); syncManager.stop(); callStatus = CALL_STATUS_NONE; await socketConnection.setClientData({ available: true }); stopStreaming(); return true; } /** * Answer an incoming call request. * * @returns {boolean} True if call successfully accepted. */ async function answerCall(args = {}) { const { syncScene = true, data } = args; if (callStatus !== CALL_STATUS_INCOMING) { logError( 'No incoming call available. Listen for new calls with incomingCall observer.' ); return false; } let sceneState; if (syncScene) { sceneState = await sceneManager.getSceneStateSafe(); } await socketConnection.send( { sceneState, data }, MESSAGE_TYPE_CALL_ACCEPTED, currentCall.id ); callStatus = CALL_STATUS_ACTIVE; if (syncScene) { syncManager.start(); } callRole = currentCall.calleeRole; return await startStreaming(); } /** * Reject an incoming call request. * * @returns {boolean} True if call successfully rejected. */ async function rejectCall() { if (callStatus !== CALL_STATUS_INCOMING) { logError( 'No incoming call available. Listen for new calls with incomingCall observer.' ); return false; } await socketConnection.send({}, MESSAGE_TYPE_CALL_REFUSED, currentCall.id); callStatus = CALL_STATUS_NONE; return true; } /** * Send data of a specific type to the call partner. * * @param type Type of data * @param data Data to be sent of any serializable type. * @returns {boolean} Returns false if no call is active. */ async function sendData(type, data) { if (callStatus !== CALL_STATUS_ACTIVE) { logError('No active call. Start or accept a call first.'); return false; } await socketConnection.send(data, type, currentCall.id); return true; } /** * Subscribe to a specific type of message. * * @param type The message type. * @returns {Observable} Observable */ function getData(type) { return socketConnection.getData(type); } /** * Tries to re-establisch apprtc connection * @param {number} attempt Reconnect attempt count. */ async function reconnect(attempt) { let newConnectionId = `${currentCall.connectionId}_${attempt}`; await coreInterface.call( 'stopStreaming', currentCall.connectionId, callRole ); currentCall.connectionId = newConnectionId; await coreInterface.call('startStreaming', newConnectionId, callRole); } // --------------------------------------------------------------------------------------------------------------------- // PRIVATE // --------------------------------------------------------------------------------------------------------------------- /** * Subscribe to the socket connection's observables. */ function subscribe() { socketConnection .getData(MESSAGE_TYPE_INCOMING_CALL) .subscribe(onIncomingCall); socketConnection .getData(MESSAGE_TYPE_CALL_ACCEPTED) .subscribe(onCallAccepted); socketConnection .getData(MESSAGE_TYPE_CALL_REFUSED) .subscribe(onCallRefused); socketConnection.getData(MESSAGE_TYPE_BUSY).subscribe(onLineBusy); socketConnection.getData(MESSAGE_TYPE_END_CALL).subscribe(onCallEnded); socketConnection.clientChanged.subscribe(onClientChanged); socketConnection.disconnect.subscribe(onDisconnect); } /** * Handles incoming call requests. If already in a call notify the caller that we are busy. * * @param id Id of the callee's client. * @param connectionId The connectionId used to stream the camera picture. * @param sceneState The callee's scene state. * @param calleeRole The callee's role. */ async function onIncomingCall({ id, connectionId, sceneState, calleeRole, userData, }) { if (callStatus !== CALL_STATUS_NONE) { await socketConnection.send({}, MESSAGE_TYPE_BUSY); } else { currentCall = { id, calleeRole, connectionId, sceneState, }; callStatus = CALL_STATUS_INCOMING; incomingCallObservable.next({ id, userData }); await socketConnection.setClientData({ available: false }); } } /** * Handle accepted call. Sets scene state and starts camera picture streaming. * * @param data The initial data to setup the scene (contains the callee's scene state). */ async function onCallAccepted(receivedData) { callStatus = CALL_STATUS_ACTIVE; const { sceneState, data } = receivedData; if (sceneState) { await sceneManager.setSceneState(sceneState); syncManager.start(); } const success = await startStreaming(); acceptedCallObservable.next({ id: currentCall.id, data }); if (!success) { endCall(); } } /** * Handle refused call. */ async function onCallRefused() { callStatus = CALL_STATUS_NONE; refusedCallObservable.next(); await socketConnection.setClientData({ available: true }); } /** * Handle ended call. */ async function onCallEnded() { logDebug(debug, 'onCallEnded', callStatus); if (callStatus === CALL_STATUS_ACTIVE) { stopStreaming(); } callStatus = CALL_STATUS_NONE; syncManager.stop(); endedCallObservable.next(); await socketConnection.setClientData({ available: true }); } /** * Handle line busy messages. */ async function onLineBusy() { callStatus = CALL_STATUS_NONE; lineBusyObservable.next(); await socketConnection.setClientData({ available: true }); } /** * Triggered when a client joined or left the session. * * @param id The client's id. * @param type Wether the client joined or left. */ function onClientChanged({ id, type }) { logDebug(debug, 'onClientChanged', id, type); switch (callStatus) { case CALL_STATUS_ACTIVE: if (id === currentCall.id && type === CLIENT_CHANGE_LEFT) { onCallEnded(); } break; case CALL_STATUS_CONNECTING: if (id === currentCall.id && type === CLIENT_CHANGE_LEFT) { onLineBusy(); } break; } } /** * Handle connection lost. */ function onDisconnect() { logDebug(debug, 'onDisconnect'); onCallEnded(); disconnectObservable.next(); } /** * Generate a random unique id for the stream transmission. * @returns {string} The generated id. */ function generateConnectionId() { return Math.random().toString(36).slice(2); } /** * Starts streaming of the camera picture. */ async function startStreaming() { // Activate ar camera. await cameras.arCamera.activate(); if (callRole === CALL_ROLE_SUPPORT_AGENT) { // Start remote tracker. if (trackers.Remote) { await trackers.Remote.activate(); } else { logError('Remote tracker not activated in app config.'); } } coreInterface.call('startStreaming', currentCall.connectionId, callRole); return true; } /** * Stops streaming of the camera picture. */ async function stopStreaming() { await cameras.arCamera.hidePointCloud(); await coreInterface.call('stopStreaming', currentCall.connectionId); // Stop remote tracker. if (trackers.Remote) { await trackers.Remote.deactivate(); } } };