@viewar/call
Version:
ViewAR Call
537 lines (467 loc) • 13.5 kB
JavaScript
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();
}
}
};