UNPKG

react-native-voximplant

Version:

VoxImplant Mobile SDK for embedding voice and video communication into React Native apps.

628 lines (592 loc) 26.1 kB
/* * Copyright (c) 2011-2019, Zingaya, Inc. All rights reserved. */ 'use strict'; import { Platform, NativeModules, NativeEventEmitter, } from 'react-native'; import {LogLevel, RequestAudioFocusMode, VideoCodec} from './../Enums'; import ClientEvents from './ClientEvents'; import Call from './../call/Call'; import Endpoint from './../call/Endpoint'; import CallManager from "../call/CallManager"; import MessagingShared from "../messaging/MessagingShared"; const ClientModule = NativeModules.RNVIClientModule; const listeners = {}; const EventEmitter = new NativeEventEmitter(ClientModule); /** * @memberOf Voximplant * @class Client * @classdesc The Client class is used to control platform functions. Can't be instantiated directly (singleton), so use the {@link Voximplant#getInstance} method to get the class instance. */ export default class Client { /** * @private */ static _clientInstance = null; _loggerCallback = null; /** * @ignore */ constructor(clientConfig) { if (Client._clientInstance) { throw new Error('Error - use Voximplant.getInstance()'); } if (!clientConfig) clientConfig = {}; if (Platform.OS === 'android') { if (clientConfig.enableVideo === undefined) clientConfig.enableVideo = true; if (clientConfig.enableDebugLogging === undefined) clientConfig.enableDebugLogging = false; if (clientConfig.enableCameraMirroring === undefined) clientConfig.enableCameraMirroring = true; if (clientConfig.enableLogcatLogging === undefined) clientConfig.enableLogcatLogging = true; if (clientConfig.preferredVideoCodec === undefined) clientConfig.preferredVideoCodec = VideoCodec.VP8; if (clientConfig.bundleId === undefined) clientConfig.bundleId = null; if (clientConfig.requestAudioFocusMode === undefined) clientConfig.requestAudioFocusMode = RequestAudioFocusMode.REQUEST_ON_CALL_START; if (clientConfig.forceRelayTraffic === undefined) clientConfig.forceRelayTraffic = false if (clientConfig.logLevel !== undefined) console.log('logLevel is iOS only option'); ClientModule.init(clientConfig); } if (Platform.OS === 'ios') { if (clientConfig.logLevel === undefined) clientConfig.logLevel = LogLevel.INFO; if (clientConfig.bundleId === undefined) clientConfig.bundleId = null; if (clientConfig.forceRelayTraffic === undefined) clientConfig.forceRelayTraffic = false if (clientConfig.enableVideo !== undefined) console.log('enableVideo is Android only option'); if (clientConfig.enableDebugLogging !== undefined) console.log('enableDebugLogging is Android only option'); if (clientConfig.enableCameraMirroring !== undefined) console.log('enableCameraMirroring is Android only option'); if (clientConfig.enableLogcatLogging !== undefined) console.log('enableLogcatLogging is Android only option'); if (clientConfig.preferredVideoCodec !== undefined) console.log('preferredVideoCodec is Android only option'); if (clientConfig.requestAudioFocusMode !== undefined) console.log('requestAudioFocusMode is Android only option'); ClientModule.initWithOptions(clientConfig); } EventEmitter.addListener('VIConnectionEstablished', this._onConnectionEstablished); EventEmitter.addListener('VIConnectionClosed', this._onConnectionClosed); EventEmitter.addListener('VIConnectionFailed', this._onConnectionFailed); EventEmitter.addListener('VIAuthResult', this._onAuthResult); EventEmitter.addListener('VIAuthTokenResult', this._onAuthTokenResult); EventEmitter.addListener('VIIncomingCall', this._onIncomingCall); EventEmitter.addListener('VIReconnecting', this._onReconnecting); EventEmitter.addListener('VIReconnected', this._onReconnected); } /** * @ignore */ static getInstance(clientConfig) { if (this._clientInstance === null) { this._clientInstance = new Client(clientConfig); } return this._clientInstance; } /** * Set outer logging callback. The method allows integrating logging pipeline of the Voximplant React Native SDK into * your own logger i.e. the method call sends all events to your function. * @param {function} callback - Callback function with two arguments: {@link Voximplant.LogLevel} and message * @memberOf Voximplant.Client */ setLoggerCallback(callback) { if (!callback || !(callback instanceof Function)) { console.warn('Client: setLoggerCallback: callback is not a Function'); if (this._logMessageSubscriber) { this._loggerCallback = null; this._logMessageSubscriber.remove(); delete this._logMessageSubscriber; } return; } this._logMessageSubscriber = EventEmitter.addListener('VILogMessage', this._onLogMessage); this._loggerCallback = callback; } /** * Register handler for specified client event. * Use {@link Voximplant.Client#off} method to delete a handler. * @param {Voximplant.ClientEvents} event * @param {function} handler - Handler function * @memberOf Voximplant.Client */ on(event, handler) { if (!handler || !(handler instanceof Function)) { console.warn(`Client: on: handler is not a Function`); return; } if (Object.values(ClientEvents).indexOf(event) === -1) { console.warn(`Client: on: ClientEvents does not contain ${event} event`); return; } if (!listeners[event]) { listeners[event] = new Set(); } listeners[event].add(handler); } /** * Remove handler for specified event * @param {Voximplant.ClientEvents} event * @param {function} handler - Handler function. If not specified, all handlers for the event will be removed. * @memberOf Voximplant.Client */ off(event, handler) { if (!listeners[event]) { return; } if (Object.values(ClientEvents).indexOf(event) === -1) { console.warn(`Client: off: ClientEvents does not contain ${event} event`); return; } if (handler && handler instanceof Function) { listeners[event].delete(handler); } else { listeners[event] = new Set(); } } /** * Connect to the Voximplant Cloud * @param {Voximplant.ConnectOptions} options - Connection options * @returns {Promise<EventHandlers.ConnectionEstablished|EventHandlers.ConnectionFailed>} * @memberOf Voximplant.Client */ connect(options) { return new Promise((resolve, reject) => { if (!options) { console.error('ConnectOptions and ConnectionNode are required'); reject({'name': ClientEvents.ConnectionFailed, 'message': 'ConnectOptions and ConnectionNode are required'}); return; } if (options.connectivityCheck === undefined) options.connectivityCheck = false; if (options.servers === undefined) options.servers = []; if (!options.node) { console.error('ConnectionNode is required'); reject({'name': ClientEvents.ConnectionFailed, 'message': 'ConnectionNode is required'}); return; } let connected = (event) => { resolve(event); if (this._connectionEstablishedSubscriber) { this._connectionEstablishedSubscriber.remove(); delete this._connectionEstablishedSubscriber; } if (this._connectionFailedSubscriber) { this._connectionFailedSubscriber.remove(); delete this._connectionFailedSubscriber; } }; let failed = (event) => { reject(event); if (this._connectionEstablishedSubscriber) { this._connectionEstablishedSubscriber.remove(); delete this._connectionEstablishedSubscriber; } if (this._connectionFailedSubscriber) { this._connectionFailedSubscriber.remove(); delete this._connectionFailedSubscriber; } }; this._connectionEstablishedSubscriber = EventEmitter.addListener('VIConnectionEstablished', connected); this._connectionFailedSubscriber = EventEmitter.addListener('VIConnectionFailed', failed); ClientModule.connect(options.connectivityCheck, options.servers, options.node, (isValidState) => { if (!isValidState) { if (this._connectionEstablishedSubscriber) { this._connectionEstablishedSubscriber.remove(); delete this._connectionEstablishedSubscriber; } if (this._connectionFailedSubscriber) { this._connectionFailedSubscriber.remove(); delete this._connectionFailedSubscriber; } reject({'name': ClientEvents.ConnectionFailed, 'message': 'ALREADY_CONNECTED_TO_VOXIMPLANT'}); } }); }); }; /** * Disconnect from the Voximplant Cloud * @returns {Promise<EventHandlers.ConnectionClosed>} * @memberOf Voximplant.Client */ disconnect() { return new Promise((resolve, reject) => { let disconnected = (event) => { resolve(event); if (this._connectionClosedSubscriber) { this._connectionClosedSubscriber.remove(); delete this._connectionClosedSubscriber; } }; this._connectionClosedSubscriber = EventEmitter.addListener('VIConnectionClosed', disconnected); ClientModule.disconnect(); }); } /** * Get current client state * @returns {Promise<Voximplant.ClientState>} * @memberOf Voximplant.Client */ getClientState() { return ClientModule.getClientState(); } /** * Login to specified Voximplant application with password. * @param {string} username - Fully-qualified username that includes Voximplant user, application and account names. The format is: "username@appname.accname.voximplant.com". * @param {string} password - User password * @returns {Promise<EventHandlers.AuthResult>} * @memberOf Voximplant.Client */ login(username, password) { return new Promise((resolve, reject) => { let loginResult = (event) => { if (event.result) { MessagingShared.getInstance().setCurrentUser(username); resolve(event); } else { reject(event); } if (this._authResultSubscriber) { this._authResultSubscriber.remove(); delete this._authResultSubscriber; } }; this._authResultSubscriber = EventEmitter.addListener('VIAuthResult', loginResult); ClientModule.login(username, password); }); } /** * Login to specified Voximplant application using 'onetimekey' auth method. Hash should be calculated with the key received in AuthResult event. * Please, read {@link http://voximplant.com/docs/quickstart/24/automated-login/ howto page} * @param {string} username - Fully-qualified username that includes Voximplant user, application and account names. The format is: "username@appname.accname.voximplant.com". * @param {string} hash - Hash that was generated using following formula: MD5(oneTimeKey+"|"+MD5(user+":voximplant.com:"+password)). * Please note that here user is just a user name, without app name, account name or anything else after "@". * So if you pass myuser@myapp.myacc.voximplant.com as a username, you should only use myuser while computing this hash. * @returns {Promise<EventHandlers.AuthResult>} * @memberOf Voximplant.Client */ loginWithOneTimeKey(username, hash) { return new Promise((resolve, reject) => { let loginResult = (event) => { if (event.result) { MessagingShared.getInstance().setCurrentUser(username); resolve(event); } else { reject(event); } if (this._authResultSubscriber) { this._authResultSubscriber.remove(); delete this._authResultSubscriber; } }; this._authResultSubscriber = EventEmitter.addListener('VIAuthResult', loginResult); ClientModule.loginWithOneTimeKey(username, hash); }); } /** * Login to specified Voximplant application using accessToken * @param {string} username - Fully-qualified username that includes Voximplant user, application and account names. The format is: "username@appname.accname.voximplant.com". * @param {string} token - Access token that was obtained in AuthResult event * @returns {Promise<EventHandlers.AuthResult>} * @memberOf Voximplant.Client */ loginWithToken(username, token) { return new Promise((resolve, reject) => { let loginResult = (event) => { if (event.result) { MessagingShared.getInstance().setCurrentUser(username); resolve(event); } else { reject(event); } if (this._authResultSubscriber) { this._authResultSubscriber.remove(); delete this._authResultSubscriber; } }; this._authResultSubscriber = EventEmitter.addListener('VIAuthResult', loginResult); ClientModule.loginWithToken(username, token); }); } /** * Request a key for 'onetimekey' auth method. Server will send the key in AuthResult event with code 302 * @param {string} username Fully - qualified username that includes Voximplant user, application and account names. The format is: "username@appname.accname.voximplant.com". * @returns {Promise<EventHandlers.AuthResult>} * @memberOf Voximplant.Client */ requestOneTimeLoginKey(username) { return new Promise((resolve, reject) => { let requestResult = (event) => { if (event.result) { resolve(event); } else { reject(event); } if (this._authResultSubscriber) { this._authResultSubscriber.remove(); delete this._authResultSubscriber; } }; this._authResultSubscriber = EventEmitter.addListener('VIAuthResult', requestResult); ClientModule.requestOneTimeLoginKey(username); }); } /** * Refresh expired access token * @param {string} username - Fully-qualified username that includes Voximplant user, application and account names. The format is: "username@appname.accname.voximplant.com". * @param {string} refreshToken - Refresh token that was obtained in AuthResult event * @returns {Promise<EventHandlers.AuthTokenResult>} * @memberOf Voximplant.Client */ tokenRefresh(username, refreshToken) { return new Promise((resolve, reject) => { let refreshResult = (event) => { if (event.result) { resolve(event); } else { reject(event); } if (this._authTokenResultSubscriber) { this._authTokenResultSubscriber.remove(); delete this._authTokenResultSubscriber; } }; this._authTokenResultSubscriber = EventEmitter.addListener('VIAuthTokenResult', refreshResult); ClientModule.refreshToken(username, refreshToken); }); } /** * Register for VoIP push notifications. Only PushKit VoIP tokens are acceptable. * * Application will receive push notifications from the Voximplant Server after the first login. * @param {string} token - Push registration token * @memberOf Voximplant.Client */ registerPushNotificationsToken(token) { ClientModule.registerPushNotificationsToken(token); } /** * Register Apple Push Notifications token for IM push notifications. * * See [React Native PushNotificationIOS](https://facebook.github.io/react-native/docs/pushnotificationios#addeventlistener) * for more details. * * IM push notification token for Android is the same as VoIP push notification token and should be registered once via * {@link Voximplant.Client#registerPushNotificationsToken} * IOS ONLY. * * @param {string} token - The APNS token for IM push notifications. * @memberOf Voximplant.Client */ registerIMPushNotificationsTokenIOS(token) { if (Platform.OS === 'ios') { ClientModule.registerIMPushNotificationsTokenIOS(token); } else { console.warn('Client.registerIMPushNotificationTokenIOS is available only on ios. Please check the docs'); } } /** * Unregister from push notifications. Application will no longer receive push notifications from the Voximplant server * @param {string} token - Push registration token for VoIP calls * @memberOf Voximplant.Client */ unregisterPushNotificationsToken(token) { ClientModule.unregisterPushNotificationsToken(token); } /** * Unregister from IM push notifications. Application will no longer receive IM push notifications from the Voximplant server * * IOS ONLY. * * @param {string} token - The APNS token for IM push notifications. * @memberOf Voximplant.Client */ unregisterIMPushNotificationsTokenIOS(token) { if (Platform.OS === 'ios') { ClientModule.unregisterIMPushNotificationsTokenIOS(token); } else { console.warn('Client.unregisterIMPushNotificationsTokenIOS is available only on ios. Please check the docs'); } } /** * Handle incoming push notification * @param {object} notification - Incoming push notification * @memberOf Voximplant.Client */ handlePushNotification(notification) { ClientModule.handlePushNotification(notification); } /** * Create outgoing call. * * Important: There is a difference between resolving the Voximplant.Client.call promise and handling Voximplant.CallEvents. * If the promise is resolved, the SDK sends a call to the cloud. However, it doesn't mean that a call is connected; * to catch this call state, subscribe to the Voximplant.CallEvents.Connected event. * If the promise is rejected, that indicates the issues in the application's code (e.g., a try to make a call without login to the Voximplant cloud); * in case of the CallFailed event is triggered, that means a telecom-related issue (e.g., another participant rejects a call). * * @param {string} number - The number to call. For SIP compatibility reasons it should be a non-empty string even if the number itself is not used by a Voximplant cloud scenario. * @param {Voximplant.CallSettings} [callSettings] - Optional call settings * @returns {Promise<Voximplant.Call>} * @memberOf Voximplant.Client */ call(number, callSettings) { if (!callSettings) { callSettings = {}; } if (callSettings.preferredVideoCodec === undefined) { callSettings.preferredVideoCodec = VideoCodec.AUTO; } if (callSettings.video === undefined) { callSettings.video = {}; callSettings.video.sendVideo = false; callSettings.video.receiveVideo = true; } if (callSettings.customData === undefined) { callSettings.customData = null; } if (callSettings.extraHeaders === undefined) { callSettings.extraHeaders = {}; } if (Platform.OS === 'ios' && (callSettings.setupCallKit === undefined || callSettings.setupCallKit === null)) { callSettings.setupCallKit = false; } if (callSettings.enableSimulcast === undefined) { callSettings.enableSimulcast = false; } return new Promise((resolve, reject) => { ClientModule.createAndStartCall(number, callSettings, (callId, errorDescription) => { if (callId) { let call = new Call(callId); resolve(call); } else { reject(errorDescription); } }); }); } /** * Create call to a dedicated conference without proxy session. For details see [the video conferencing guide](https://voximplant.com/blog/video-conference-through-voximplant-media-servers). * * Important: There is a difference between resolving the Voximplant.Client.callConference promise and handling Voximplant.CallEvents. * If the promise is resolved, the SDK sends a call to the cloud. However, it doesn't mean that a call is connected; * to catch this call state, subscribe to the Voximplant.CallEvents.Connected event. * If the promise is rejected, that indicates the issues in the application's code (e.g., a try to make a call without login to the Voximplant cloud); * in case of the CallFailed event is triggered, that means a telecom-related issue (e.g., another participant rejects a call). * * @param {string} number - The number to call. For SIP compatibility reasons it should be a non-empty string even if the number itself is not used by a Voximplant cloud scenario. * @param {Voximplant.CallSettings} [callSettings] - Optional call settings * @returns {Promise<Voximplant.Call>} * @memberOf Voximplant.Client */ callConference(number, callSettings) { if (!callSettings) { callSettings = {}; } if (callSettings.preferredVideoCodec === undefined) { callSettings.preferredVideoCodec = VideoCodec.AUTO; } if (callSettings.video === undefined) { callSettings.video = {}; callSettings.video.sendVideo = false; callSettings.video.receiveVideo = true; } if (callSettings.customData === undefined) { callSettings.customData = null; } if (callSettings.extraHeaders === undefined) { callSettings.extraHeaders = {}; } if (Platform.OS === 'ios' && (callSettings.setupCallKit === undefined || callSettings.setupCallKit === null)) { callSettings.setupCallKit = false; } if (callSettings.enableSimulcast === undefined) { callSettings.enableSimulcast = false; } return new Promise((resolve, reject) => { ClientModule.createAndStartConference(number, callSettings, (callId, errorDescription) => { if (callId) { let call = new Call(callId); resolve(call); } else { reject(errorDescription); } }); }); } /** * @private */ _emit(event, ...args) { const handlers = listeners[event]; if (handlers) { for (const handler of handlers) { console.log(`Client: emit event ${event}`); handler(...args); } } else { console.log(`Client: emit: no handlers for event: ${event}`); } } /** * @private */ _onConnectionEstablished = (event) => { this._emit(ClientEvents.ConnectionEstablished, event); }; /** * @private */ _onConnectionFailed = (event) => { this._emit(ClientEvents.ConnectionFailed, event); }; /** * @private */ _onConnectionClosed = (event) => { MessagingShared.getInstance().setCurrentUser(null); this._emit(ClientEvents.ConnectionClosed, event); }; /** * * @private */ _onReconnecting = (event) => { this._emit(ClientEvents.Reconnecting, event); }; /** * * @private */ _onReconnected = (event) => { this._emit(ClientEvents.Reconnected, event); }; /** * @private */ _onAuthResult = (event) => { this._emit(ClientEvents.AuthResult, event); }; /** * @private */ _onAuthTokenResult = (event) => { this._emit(ClientEvents.RefreshTokenResult, event); }; /** * @private */ _onIncomingCall = (event) => { event.call = new Call(event.callId); let endpoint = new Endpoint(event.endpointId, event.displayName, event.sipUri, event.endpointName); CallManager.getInstance().addEndpoint(event.callId, endpoint); event.call.callKitUUID = event.callKitUUID; delete event.callKitUUID; delete event.endpointId; delete event.sipUri; delete event.displayName; delete event.endpointName; delete event.callId; this._emit(ClientEvents.IncomingCall, event); }; /** * @private */ _onLogMessage = (event) => { if (this._loggerCallback) { this._loggerCallback(event.level, event.message); } } }