diff --git a/app/CallManager.js b/app/CallManager.js index 4a6ac40..b7f913c 100644 --- a/app/CallManager.js +++ b/app/CallManager.js @@ -1,607 +1,608 @@ import events from 'events'; import Logger from '../Logger'; import uuid from 'react-native-uuid'; import { Platform, PermissionsAndroid } from 'react-native'; import utils from './utils'; const logger = new Logger('CallManager'); import { CONSTANTS as CK_CONSTANTS } from 'react-native-callkeep'; // https://github.com/react-native-webrtc/react-native-callkeep /* const CONSTANTS = { END_CALL_REASONS: { FAILED: 1, REMOTE_ENDED: 2, UNANSWERED: 3, ANSWERED_ELSEWHERE: 4, DECLINED_ELSEWHERE: 5, MISSED: 6 } }; */ const options = { ios: { appName: 'Sylk', maximumCallGroups: 1, maximumCallsPerCallGroup: 2, supportsVideo: true, includesCallsInRecents: true, imageName: "Image-1" }, android: { alertTitle: 'Calling account permission', alertDescription: 'Please allow Sylk inside All calling accounts', cancelButton: 'Deny', okButton: 'Allow', imageName: 'phone_account_icon', additionalPermissions: [PermissionsAndroid.PERMISSIONS.CAMERA, PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, PermissionsAndroid.PERMISSIONS.READ_CONTACTS] } }; export default class CallManager extends events.EventEmitter { constructor(RNCallKeep, acceptFunc, rejectFunc, hangupFunc, timeoutFunc, conferenceCallFunc, startCallFromCallKeeper, muteFunc, getConnectionFunct, missedCallFunc, changeRouteFunc, respawnConnection) { //logger.debug('constructor()'); super(); this.setMaxListeners(Infinity); this._RNCallKeep = RNCallKeep; this._calls = new Map(); this._pushCalls = new Map(); this._conferences = new Map(); this._rejectedCalls = new Map(); this._acceptedCalls = new Map(); this._cancelledCalls = new Map(); this._alertedCalls = new Map(); this._terminatedCalls = new Map(); this.webSocketActions = new Map(); this.pushNotificationsActions = new Map(); this._timeouts = new Map(); this.sylkAcceptCall = acceptFunc; this.sylkRejectCall = rejectFunc; this.sylkHangupCall = hangupFunc; this.timeoutCall = timeoutFunc; this.logMissedCall = missedCallFunc; this.getConnection = getConnectionFunct; this.changeRoute = changeRouteFunc; this.respawnConnection = respawnConnection; this.toggleMute = muteFunc; this.conferenceCall = conferenceCallFunc; this.startCallFromOutside = startCallFromCallKeeper; this._boundRnAccept = this._rnAccept.bind(this); this._boundRnEnd = this._rnEnd.bind(this); this._boundRnMute = this._rnMute.bind(this); this._boundRnActiveAudioCall = this._rnActiveAudioSession.bind(this); this._boundRnDeactiveAudioCall = this._rnDeactiveAudioSession.bind(this); this._boundRnDTMF = this._rnDTMF.bind(this); this._boundRnProviderReset = this._rnProviderReset.bind(this); this.boundRnStartAction = this._startedCall.bind(this); this.boundRnDisplayIncomingCall = this._displayIncomingCall.bind(this); this._RNCallKeep.addEventListener('answerCall', this._boundRnAccept); this._RNCallKeep.addEventListener('endCall', this._boundRnEnd); this._RNCallKeep.addEventListener('didPerformSetMutedCallAction', this._boundRnMute); this._RNCallKeep.addEventListener('didActivateAudioSession', this._boundRnActiveAudioCall); this._RNCallKeep.addEventListener('didDeactivateAudioSession', this._boundRnDeactiveAudioCall.bind(this)); this._RNCallKeep.addEventListener('didPerformDTMFAction', this._boundRnDTMF); this._RNCallKeep.addEventListener('didResetProvider', this._boundRnProviderReset); this._RNCallKeep.addEventListener('didReceiveStartCallAction', this.boundRnStartAction); this._RNCallKeep.addEventListener('didDisplayIncomingCall', this.boundRnDisplayIncomingCall); this._RNCallKeep.setup(options); this._RNCallKeep.addEventListener('checkReachability', () => { this._RNCallKeep.setReachable(); }); } get callKeep() { return this._RNCallKeep; } get countCalls() { return this._calls.size; } get countPushCalls() { return this._pushCalls.size; } get waitingCount() { return this._timeouts.size; } get callUUIDS() { return Array.from( this._calls.keys() ); } get calls() { return [...this._calls.values()]; } setAvailable(available) { this.callKeep.setAvailable(available); } heartbeat() { this.callUUIDS.forEach((callUUID) => { //utils.timestampedLog('Callkeep: call active', callUUID); }); } backToForeground() { //utils.timestampedLog('Callkeep: bring app to the FOREGROUND'); this.callKeep.backToForeground(); } startOutgoingCall(callUUID, targetUri, hasVideo) { utils.timestampedLog('Callkeep: WILL START CALL outgoing', callUUID); if (Platform.OS === 'ios') { this.callKeep.startCall(callUUID, targetUri, targetUri, 'email', hasVideo); } else if (Platform.OS === 'android') { this.callKeep.startCall(callUUID, targetUri, targetUri); } } updateDisplay(callUUID, displayName, uri) { utils.timestampedLog('Callkeep: update display', displayName, uri); this.callKeep.updateDisplay(callUUID, displayName, uri); } sendDTMF(callUUID, digits) { utils.timestampedLog('Callkeep: send DTMF: ', digits); this.callKeep.sendDTMF(callUUID, digits); } setCurrentCallActive(callUUID) { utils.timestampedLog('Callkeep: CALL ACTIVE', callUUID); this.callKeep.setCurrentCallActive(callUUID); } endCalls() { utils.timestampedLog('Callkeep: end all calls'); this.callKeep.endAllCalls(); } endCall(callUUID, reason) { if (reason) { utils.timestampedLog('Callkeep: END CALL', callUUID, 'with reason', reason); } else { utils.timestampedLog('Callkeep: END CALL', callUUID); } if (this._pushCalls.has(callUUID)) { this._pushCalls.delete(callUUID); } if (this._rejectedCalls.has(callUUID)) { // return; } if (this._cancelledCalls.has(callUUID)) { //utils.timestampedLog('Callkeep: CALL', callUUID, 'already cancelled'); return; } if (reason === 2) { this._cancelledCalls.set(callUUID, Date.now()); } if (reason) { this.callKeep.reportEndCallWithUUID(callUUID, reason); if (this._timeouts.has(callUUID)) { clearTimeout(this._timeouts.get(callUUID)); this._timeouts.delete(callUUID); } } else { this.callKeep.endCall(callUUID); } } terminateCall(callUUID) { if (this._calls.has(callUUID)) { this._calls.delete(callUUID); } this._terminatedCalls.set(callUUID, Date.now()); if (this._pushCalls.has(callUUID)) { this._pushCalls.delete(callUUID); } } _rnActiveAudioSession() { utils.timestampedLog('Callkeep: activated audio call'); } _rnDeactiveAudioSession() { utils.timestampedLog('Callkeep: deactivated audio call'); } _rnAccept(data) { let callUUID = data.callUUID.toLowerCase(); utils.timestampedLog('Callkeep: accept callback', callUUID); if (this._pushCalls.has(callUUID)) { this._pushCalls.delete(callUUID); } if (!this._rejectedCalls.has(callUUID)) { utils.timestampedLog('Callkeep: accept call', callUUID); this.acceptCall(callUUID); } else { utils.timestampedLog('Callkeep: cannot accept because we already rejected', callUUID); } } _rnEnd(data) { // this is called both when user touches Reject and when the call ends let callUUID = data.callUUID.toLowerCase(); utils.timestampedLog('Callkeep: end callback', callUUID); if (this._pushCalls.has(callUUID)) { this._pushCalls.delete(callUUID); } if (this._terminatedCalls.has(callUUID)) { return; } let call = this._calls.get(callUUID); if (!call && !this._conferences.has(callUUID)) { utils.timestampedLog('Callkeep: add call', callUUID, 'reject to the waitings list'); this.webSocketActions.set(callUUID, 'reject'); this.changeRoute('/ready', 'new_call_rejected'); return; } if (call && call.state === 'incoming') { if (!this._acceptedCalls.has(callUUID)) { this.rejectCall(callUUID); } } else { if (this._conferences.has(callUUID)) { const conference = this._conferences.get(callUUID); this.logMissedCall(conference.room, callUUID, direction='received', participants=[conference.from]); this._conferences.delete(callUUID); } else { this.sylkHangupCall(callUUID, 'user_press_hangup'); } } } acceptCall(callUUID) { if (this._acceptedCalls.has(callUUID)) { //utils.timestampedLog('Callkeep: already accepted call', callUUID); utils.timestampedLog('Callkeep: accept call again', callUUID); //return; } else { utils.timestampedLog('Callkeep: accept call', callUUID); } this.setCurrentCallActive(callUUID); this._acceptedCalls.set(callUUID, Date.now()); if (this._timeouts.has(callUUID)) { clearTimeout(this._timeouts.get(callUUID)); this._timeouts.delete(callUUID); } if (this._conferences.has(callUUID)) { let conference = this._conferences.get(callUUID); utils.timestampedLog('Callkeep: accept incoming conference', callUUID); this.endCall(callUUID, 4); this.backToForeground(); utils.timestampedLog('Callkeep: will start conference to', conference.room); this.conferenceCall(conference.room); this._conferences.delete(callUUID); } else if (this._calls.has(callUUID)) { this.backToForeground(); this.sylkAcceptCall(callUUID); } else { this.backToForeground(); utils.timestampedLog('Callkeep: add call', callUUID, 'accept to the waitings list'); // We accepted the call before it arrived on web socket - if (Platform.OS === 'ios') { - this.respawnConnection(); - } + this.respawnConnection(); this.webSocketActions.set(callUUID, 'accept'); utils.timestampedLog('Callkeep: check over 12 seconds if call', callUUID, 'arrived over web socket'); setTimeout(() => { const connection = this.getConnection(); if (this.callUUIDS) { utils.timestampedLog('Callkeep: current calls:', this.callUUIDS); } if (!this._calls.has(callUUID) && !this._terminatedCalls.has(callUUID)) { utils.timestampedLog('Callkeep: call', callUUID, 'did not arrive over web socket', connection); this.webSocketActions.delete(callUUID); this.endCall(callUUID, 1); this.sylkHangupCall(callUUID, 'timeout'); } else { utils.timestampedLog('Callkeep: call', callUUID, 'did arrive over web socket', connection); } }, 12000); } } rejectCall(callUUID) { if (this._rejectedCalls.has(callUUID)) { utils.timestampedLog('Callkeep: already rejected call', callUUID); return; } utils.timestampedLog('Callkeep: reject call', callUUID); this._rejectedCalls.set(callUUID, Date.now()); if (this._timeouts.has(callUUID)) { clearTimeout(this._timeouts.get(callUUID)); this._timeouts.delete(callUUID); } this.callKeep.rejectCall(callUUID); if (this._conferences.has(callUUID)) { utils.timestampedLog('Callkeep: reject conference invite', callUUID); let room = this._conferences.get(callUUID); this._conferences.delete(callUUID); } else if (this._calls.has(callUUID)) { let call = this._calls.get(callUUID); if (call.state === 'incoming') { this.sylkRejectCall(callUUID); } else { this.sylkHangupCall(callUUID, 'user_press_hangup'); } } else { // We rejected the call before it arrived on web socket // from iOS push notifications utils.timestampedLog('Callkeep: add call', callUUID, 'reject to the waitings list'); this.webSocketActions.set(callUUID, 'reject'); utils.timestampedLog('Callkeep: check over 20 seconds if call', callUUID, 'arrived on web socket'); setTimeout(() => { if (!this._calls.has(callUUID)) { utils.timestampedLog('Callkeep: call', callUUID, 'did not arrive on web socket'); this.webSocketActions.delete(callUUID); this.endCall(callUUID, 1); } }, 20000); } this.endCall(callUUID); } setMutedCall(callUUID, mute=false) { //utils.timestampedLog('Callkeep: set call', callUUID, 'muted =', mute); if (this._calls.has(callUUID)) { this.callKeep.setMutedCall(callUUID, mute); let call = this._calls.get(callUUID); const localStream = call.getLocalStreams()[0]; if (mute) { utils.timestampedLog('Callkeep: local stream audio track disabled'); } else { utils.timestampedLog('Callkeep: local stream audio track enabled'); } localStream.getAudioTracks()[0].enabled = !mute; } } _rnMute(data) { utils.timestampedLog('Callkeep: mute ' + data.muted + ' for call', data.callUUID); this.toggleMute(data.callUUID, data.muted); } _rnDTMF(data) { utils.timestampedLog('Callkeep: got dtmf for call', data.callUUID); if (this._calls.has(data.callUUID.toLowerCase())) { let call = this._calls.get(data.callUUID.toLowerCase()); utils.timestampedLog('sending webrtc dtmf', data.digits) call.sendDtmf(data.digits); } } _rnProviderReset() { utils.timestampedLog('Callkeep: got a provider reset, clearing down all calls'); this._calls.forEach((call) => { call.terminate(); }); } addWebsocketCall(call) { const connection = this.getConnection(); if (this._calls.has(call.id)) { return; } utils.timestampedLog('Callkeep: added call', call.id, 'for connection', connection); this._calls.set(call.id, call); } incomingCallFromPush(callUUID, from, force=false, skipNativePanel=false) { utils.timestampedLog('Callkeep: handle new incoming push call', callUUID, 'from', from); if (this._pushCalls.has(callUUID)) { utils.timestampedLog('Callkeep: push call already handled', callUUID); return; } this._pushCalls.set(callUUID, true); if (this._rejectedCalls.has(callUUID)) { utils.timestampedLog('Callkeep: call already rejected', callUUID); this.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.UNANSWERED); return; } if (this._acceptedCalls.has(callUUID)) { utils.timestampedLog('Callkeep: call already accepted', callUUID); return; } // if user does not decide anything this will be handled later this._timeouts.set(callUUID, setTimeout(() => { utils.timestampedLog('Callkeep: incoming call', callUUID, 'timeout'); let reason = this.webSocketActions.has(callUUID) ? CK_CONSTANTS.END_CALL_REASONS.FAILED : CK_CONSTANTS.END_CALL_REASONS.UNANSWERED; if (!this._terminatedCalls.has(callUUID) && !this._calls.has(callUUID)) { const connection = this.getConnection(); utils.timestampedLog('Callkeep: call', callUUID, 'did not arive on web socket', connection); reason = CK_CONSTANTS.END_CALL_REASONS.FAILED; } else if (this._calls.has(callUUID)) { utils.timestampedLog('Callkeep: user did not accept or reject', callUUID); } this.endCall(callUUID, reason); this._timeouts.delete(callUUID); }, 45000)); if (Platform.OS === 'ios') { if (this._calls.has(callUUID)) { utils.timestampedLog('Callkeep: call', callUUID, 'already received on web socket'); } this.showAlertPanel(callUUID, from); } else { if (this._calls.has(callUUID) || force) { // on Android display alert panel only after websocket call arrives // force is required when Android is locked, if we do not bring up the panel, the app will not wake up if (!skipNativePanel || force) { this.showAlertPanel(callUUID, from); } else { utils.timestampedLog('Callkeep: call', callUUID, 'skipped display of native panel'); } } else { utils.timestampedLog('Callkeep: waiting for call', callUUID, 'on web socket'); } } } incomingCallFromWebSocket(call, accept=false, skipNativePanel=false) { this.addWebsocketCall(call); utils.timestampedLog('Callkeep: handle incoming websocket call', call.id); // if the call came via push and was already accepted or rejected if (this.webSocketActions.get(call.id)) { let action = this.webSocketActions.get(call.id); utils.timestampedLog('Callkeep: execute action decided earlier', action); if (action === 'accept') { this.sylkAcceptCall(call.id); } else { this.sylkRejectCall(call.id); } this.webSocketActions.delete(call.id); } else { if (accept) { this.acceptCall(call.id); } else if (!skipNativePanel) { - this.showAlertPanelforCall(call); + if (Platform.OS === 'ios') { + this.showAlertPanelforCall(call); + } } } // Emit event. this._emitSessionsChange(true); } handleConference(callUUID, room, from_uri) { if (this._conferences.has(callUUID)) { return; } this._conferences.set(callUUID, {room: room, from: from_uri}); utils.timestampedLog('CallKeep: handle conference', callUUID, 'from', from_uri, 'to room', room); this.showAlertPanel(callUUID, from_uri); this._timeouts.set(callUUID, setTimeout(() => { utils.timestampedLog('Callkeep: conference timeout', callUUID); this.timeoutCall(callUUID, from_uri); this.endCall(callUUID, CK_CONSTANTS.END_CALL_REASONS.MISSED); this._timeouts.delete(callUUID); }, 45000)); this._emitSessionsChange(true); } showAlertPanelforCall(call, force=false) { const callUUID = call.id; const uri = call.remoteIdentity.uri; const username = uri.split('@')[0]; const isPhoneNumber = username.match(/^(\+|0)(\d+)$/); const from = isPhoneNumber ? username: uri; const hasVideo = call.mediaTypes && call.mediaTypes.video; this.showAlertPanel(callUUID, from, hasVideo); } showAlertPanel(callUUID, uri, hasVideo=false) { if (this._alertedCalls.has(callUUID)) { + //utils.timestampedLog('Callkeep: call', callUUID, 'was already alerted'); return; } const username = uri.split('@')[0]; const isPhoneNumber = username.match(/^(\+|0)(\d+)$/); const from = isPhoneNumber ? username: uri; utils.timestampedLog('Callkeep: ALERT PANEL for', callUUID, 'from', from); this._alertedCalls.set(callUUID, Date.now()); if (Platform.OS === 'ios') { this.callKeep.displayIncomingCall(callUUID, from, from, 'email', hasVideo); } else if (Platform.OS === 'android') { this.callKeep.displayIncomingCall(callUUID, from, from); } } _startedCall(data) { utils.timestampedLog("Callkeep: STARTED CALL", data.callUUID); if (!this._calls.has(data.callUUID)) { // call has started from OS native dialer this.startCallFromOutside(data); } } _displayIncomingCall(data) { utils.timestampedLog('Callkeep: Incoming alert panel displayed'); } _emitSessionsChange(countChanged) { this.emit('sessionschange', countChanged); } destroy() { this._RNCallKeep.removeEventListener('acceptCall', this._boundRnAccept); this._RNCallKeep.removeEventListener('endCall', this._boundRnEnd); this._RNCallKeep.removeEventListener('didPerformSetMutedCallAction', this._boundRnMute); this._RNCallKeep.removeEventListener('didActivateAudioSession', this._boundRnActiveAudioCall); this._RNCallKeep.removeEventListener('didDeactivateAudioSession', this._boundRnDeactiveAudioCall); this._RNCallKeep.removeEventListener('didPerformDTMFAction', this._boundRnDTMF); this._RNCallKeep.removeEventListener('didResetProvider', this._boundRnProviderReset); this._RNCallKeep.removeEventListener('didReceiveStartCallAction', this.boundRnStartAction); this._RNCallKeep.removeEventListener('didDisplayIncomingCall', this.boundRnDisplayIncomingCall); } } diff --git a/app/components/Call.js b/app/components/Call.js index bac97d4..926bcae 100644 --- a/app/components/Call.js +++ b/app/components/Call.js @@ -1,546 +1,551 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; import assert from 'assert'; import debug from 'react-native-debug'; import autoBind from 'auto-bind'; import uuid from 'react-native-uuid'; import AudioCallBox from './AudioCallBox'; import LocalMedia from './LocalMedia'; import VideoBox from './VideoBox'; import config from '../config'; import utils from '../utils'; class Call extends Component { constructor(props) { super(props); autoBind(this); this.defaultWaitInterval = 60; // until we can connect or reconnect this.waitCounter = 0; this.waitInterval = this.defaultWaitInterval; let callUUID; let remoteUri = ''; let remoteDisplayName = ''; let callState = null; let direction = null; let callEnded = false; this.mediaIsPlaying = false; this.ended = false; this.answering = false; if (this.props.call) { // If current call is available on mount we must have incoming this.props.call.on('stateChanged', this.callStateChanged); remoteUri = this.props.call.remoteIdentity.uri; remoteDisplayName = this.props.call.remoteIdentity.displayName || this.props.call.remoteIdentity.uri; direction = this.props.call.direction; callUUID = this.props.call.id; } else { remoteUri = this.props.targetUri; remoteDisplayName = this.props.targetUri; callUUID = this.props.callUUID; - direction = this.props.callUUID ? 'outgoing' : null; + direction = callUUID ? 'outgoing' : 'incoming'; } if (this.props.connection) { this.props.connection.on('stateChanged', this.connectionStateChanged); } let audioOnly = false; if (this.props.localMedia && this.props.localMedia.getVideoTracks().length === 0) { audioOnly = true; } this.state = { call: this.props.call, targetUri: this.props.targetUri, audioOnly: audioOnly, boo: false, remoteUri: remoteUri, remoteDisplayName: remoteDisplayName, localMedia: this.props.localMedia, connection: this.props.connection, accountId: this.props.account ? this.props.account.id : null, callState: callState, direction: direction, callUUID: callUUID, reconnectingCall: this.props.reconnectingCall } } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { // Needed for switching to incoming call while in a call if (this.ended) { return; } + //console.log('Call got props...'); - this.setState({accountId: nextProps.account ? nextProps.account.id : null}); + this.setState({connection: nextProps.connection, + accountId: nextProps.account ? nextProps.account.id : null}); if (nextProps.call !== null) { if (this.state.call !== nextProps.call) { nextProps.call.on('stateChanged', this.callStateChanged); this.setState({ call: nextProps.call, remoteUri: nextProps.call.remoteIdentity.uri, direction: nextProps.call.direction, callUUID: nextProps.call.id, remoteDisplayName: nextProps.call.remoteIdentity.displayName }); if (nextProps.call.direction === 'incoming') { this.mediaPlaying(); } this.lookupContact(); } } else { if (nextProps.callUUID !== null && this.state.callUUID !== nextProps.callUUID) { this.setState({'callUUID': nextProps.callUUID, 'direction': 'outgoing', 'call': null }); this.startCallWhenReady(nextProps.callUUID); } } if (nextProps.reconnectingCall !== this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.targetUri !== this.state.targetUri && this.state.direction === 'outgoing') { this.setState({targetUri: nextProps.targetUri}); } this.setState({registrationState: nextProps.registrationState}); if (nextProps.localMedia !== null && nextProps.localMedia !== this.state.localMedia) { let audioOnly = false; if (nextProps.localMedia.getVideoTracks().length === 0) { audioOnly = true; } this.setState({localMedia: nextProps.localMedia, audioOnly: audioOnly}); this.mediaPlaying(nextProps.localMedia); } } mediaPlaying(localMedia) { if (this.state.direction === 'incoming') { const media = localMedia ? localMedia : this.state.localMedia; this.answerCall(media); } else { this.mediaIsPlaying = true; } } answerCall(localMedia) { const media = localMedia ? localMedia : this.state.localMedia; if (this.state.call && this.state.call.state === 'incoming' && media) { let options = {pcConfig: {iceServers: config.iceServers}}; options.localStream = media; if (!this.answering) { this.answering = true; const connectionState = this.state.connection.state ? this.state.connection.state : null; utils.timestampedLog('Call: answering call in connection state', connectionState); this.state.call.answer(options); } else { utils.timestampedLog('Call: answering call in progress...'); } } else { if (!media) { utils.timestampedLog('Call: waiting for local media'); } if (!this.state.call) { utils.timestampedLog('Call: waiting for incoming call data'); } } } componentDidMount() { this.lookupContact(); if (this.state.direction === 'outgoing' && this.state.callUUID) { this.startCallWhenReady(this.state.callUUID); } } componentWillUnmount() { this.ended = true; + this.answering = false; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } if (this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); } } lookupContact() { let photo = null; let remoteUri = this.state.remoteUri || ''; let remoteDisplayName = this.state.remoteDisplayName || ''; if (!remoteUri) { return; } if (remoteUri.indexOf('3333@') > -1) { remoteDisplayName = 'Video Test'; } else if (remoteUri.indexOf('4444@') > -1) { remoteDisplayName = 'Echo Test'; } else if (this.props.contacts) { let username = remoteUri.split('@')[0]; let isPhoneNumber = username.match(/^(\+|0)(\d+)$/); if (isPhoneNumber) { var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', username); } else { var contact_obj = this.findObjectByKey(this.props.contacts, 'remoteParty', remoteUri); } if (contact_obj) { remoteDisplayName = contact_obj.displayName; photo = contact_obj.photo; if (isPhoneNumber) { remoteUri = username; } } else { if (isPhoneNumber) { remoteUri = username; remoteDisplayName = username; } } } this.setState({remoteDisplayName: remoteDisplayName, remoteUri: remoteUri, photo: photo }); } callStateChanged(oldState, newState, data) { //console.log('Call: callStateChanged', oldState, '->', newState); if (this.ended) { return; } let remoteHasNoVideoTracks; let remoteIsRecvOnly; let remoteIsInactive; let remoteStreams; + this.answering = false; + if (newState === 'established') { this.setState({reconnectingCall: false}); const currentCall = this.state.call; if (currentCall) { remoteStreams = currentCall.getRemoteStreams(); if (remoteStreams) { if (remoteStreams.length > 0) { const remotestream = remoteStreams[0]; remoteHasNoVideoTracks = remotestream.getVideoTracks().length === 0; remoteIsRecvOnly = currentCall.remoteMediaDirections.video[0] === 'recvonly'; remoteIsInactive = currentCall.remoteMediaDirections.video[0] === 'inactive'; } } } if (remoteStreams && (remoteHasNoVideoTracks || remoteIsRecvOnly || remoteIsInactive) && !this.state.audioOnly) { //console.log('Media type changed to audio'); // Stop local video if (this.state.localMedia.getVideoTracks().length !== 0) { currentCall.getLocalStreams()[0].getVideoTracks()[0].stop(); } this.setState({audioOnly: true}); } else { this.forceUpdate(); } } else if (newState === 'accepted') { // Switch if we have audioOnly and local videotracks. This means // the call object switched and we are transitioning to an // incoming call. if (this.state.audioOnly && this.state.localMedia && this.state.localMedia.getVideoTracks().length !== 0) { //console.log('Media type changed to video on accepted'); this.setState({audioOnly: false}); } } this.forceUpdate(); } connectionStateChanged(oldState, newState) { switch (newState) { case 'closed': break; case 'ready': break; case 'disconnected': if (oldState === 'ready' && this.state.direction === 'outgoing') { utils.timestampedLog('Call: reconnecting the call...'); this.waitInterval = this.defaultWaitInterval; } break; default: break; } } findObjectByKey(array, key, value) { for (var i = 0; i < array.length; i++) { if (array[i][key] === value) { return array[i]; } } return null; } canConnect() { if (!this.state.connection) { console.log('Call: no connection yet'); return false; } if (this.state.connection.state !== 'ready') { console.log('Call: connection is not ready'); return false; } if (this.props.registrationState !== 'registered') { console.log('Call: account not ready yet'); return false; } if (!this.mediaIsPlaying) { console.log('Call: media is not playing'); return false; } return true; } async startCallWhenReady(callUUID) { utils.timestampedLog('Call: start call', callUUID, 'when ready to', this.state.targetUri); this.waitCounter = 0; let diff = 0; while (this.waitCounter < this.waitInterval) { if (this.waitCounter === 1) { utils.timestampedLog('Call: waiting for establishing call', this.waitInterval, 'seconds'); } if (this.userHangup) { this.hangupCall('user_cancelled'); return; } if (this.ended) { return; } if (this.waitCounter >= this.waitInterval - 1) { this.hangupCall('timeout'); } if (!this.canConnect()) { //utils.timestampedLog('Call: waiting for connection', this.waitInterval - this.waitCounter, 'seconds'); if (this.state.call && this.state.call.id === callUUID && this.state.call.state !== 'terminated') { return; } console.log('Wait', this.waitCounter); await this._sleep(1000); } else { this.waitCounter = 0; this.start(); return; } this.waitCounter++; } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } start() { utils.timestampedLog('Call: starting call', this.state.callUUID); if (this.state.localMedia === null) { console.log('Call: cannot create new call without local media'); return; } let options = {pcConfig: {iceServers: config.iceServers}, id: this.state.callUUID}; options.localStream = this.state.localMedia; let call = this.props.account.call(this.state.targetUri, options); if (call) { call.on('stateChanged', this.callStateChanged); } } hangupCall(reason) { let callUUID = this.state.call ? this.state.call.id : this.state.callUUID; this.waitInterval = this.defaultWaitInterval; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } if (this.props.connection) { this.props.connection.removeListener('stateChanged', this.connectionStateChanged); } if (this.waitCounter > 0) { this.waitCounter = this.waitInterval; } this.props.hangupCall(callUUID, reason); } render() { let box = null; if (this.state.localMedia !== null) { if (this.state.audioOnly) { box = ( ); } else { if (this.state.call !== null && (this.state.call.state === 'established' || (this.state.call.state === 'terminated' && this.state.reconnectingCall))) { box = ( ); } else { if (this.state.call && this.state.call.state === 'terminated' && this.state.reconnectingCall) { //console.log('Skip render local media because we will reconnect'); } else { box = ( ); } } } } else { box = ( ); } return box; } } Call.propTypes = { targetUri : PropTypes.string, account : PropTypes.object, hangupCall : PropTypes.func, connection : PropTypes.object, registrationState : PropTypes.string, call : PropTypes.object, localMedia : PropTypes.object, shareScreen : PropTypes.func, escalateToConference : PropTypes.func, generatedVideoTrack : PropTypes.bool, callKeepSendDtmf : PropTypes.func, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, callUUID : PropTypes.string, contacts : PropTypes.array, intercomDtmfTone : PropTypes.string, orientation : PropTypes.string, isTablet : PropTypes.bool, reconnectingCall : PropTypes.bool, muted : PropTypes.bool }; export default Call;