diff --git a/app/components/Call.js b/app/components/Call.js index 84b1ebf..3e11003 100644 --- a/app/components/Call.js +++ b/app/components/Call.js @@ -1,467 +1,467 @@ 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 = 'outgoing'; let callEnded = false; this.mediaIsPlaying = false; this.ended = 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; } 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, 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; } if (nextProps.call !== null && 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 (this.state.direction === 'outgoing' && nextProps.call.direction === 'incoming') { this.mediaPlaying(); } this.lookupContact(); } 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}); } if (nextProps.localMedia !== this.state.localMedia) { let audioOnly = false; if (nextProps.localMedia && nextProps.localMedia.getVideoTracks().length === 0) { audioOnly = true; } this.setState({localMedia: nextProps.localMedia, audioOnly: audioOnly}); } } mediaPlaying() { if (this.state.direction === 'incoming') { this.answerCall(); } else { this.mediaIsPlaying = true; } } componentDidMount() { this.lookupContact(); if (this.state.direction === 'outgoing') { this.startCallWhenReady(); } } componentWillUnmount() { this.ended = true; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } if (this.props.connection) { this.props.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); let remoteHasNoVideoTracks; let remoteIsRecvOnly; let remoteIsInactive; let remoteStreams; 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) { utils.timestampedLog('Call: connection state changed:', 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; this.startCallWhenReady(); } 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; } async startCallWhenReady() { if (!this.state.callUUID || !this.state.targetUri) { return; } utils.timestampedLog('Call: start call', this.state.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) { utils.timestampedLog('Call: terminating call', this.state.callUUID, 'that did not start yet'); this.hangupCall('timeout'); } if (!this.props.connection || this.props.connection.state !== 'ready' || this.props.registrationState !== 'registered' || !this.mediaIsPlaying ) { utils.timestampedLog('Call: waiting for connection', this.waitInterval - this.waitCounter, 'seconds'); if (this.state.call && this.state.call.state !== 'terminated') { return; } await this._sleep(1000); } else { this.waitCounter = 0; this.call(); return; } this.waitCounter++; } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } call() { 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); } } answerCall() { if (this.state.call && this.state.call.state === 'incoming') { let options = {pcConfig: {iceServers: config.iceServers}}; options.localStream = this.state.localMedia; this.state.call.answer(options); } } hangupCall(reason) { let callUUID = this.state.call ? this.state.call.id : this.state.callUUID; this.waitInterval = this.defaultWaitInterval; this.state.callUUID || this.state.call.id; 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 = ( ); } } } } return box; } } Call.propTypes = { targetUri : PropTypes.string.isRequired, 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; diff --git a/ios/sylkUITests/Info.plist b/ios/sylkUITests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/ios/sylkUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/ios/sylkUITests/sylkUITests.m b/ios/sylkUITests/sylkUITests.m new file mode 100644 index 0000000..a293428 --- /dev/null +++ b/ios/sylkUITests/sylkUITests.m @@ -0,0 +1,48 @@ +// +// sylkUITests.m +// sylkUITests +// +// Created by GA Georgescu on 24/08/2020. +// Copyright © 2020 Facebook. All rights reserved. +// + +#import + +@interface sylkUITests : XCTestCase + +@end + +@implementation sylkUITests + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + self.continueAfterFailure = NO; + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testExample { + // UI tests must launch the application that they test. + XCUIApplication *app = [[XCUIApplication alloc] init]; + [app launch]; + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. +} + +- (void)testLaunchPerformance { + if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { + // This measures how long it takes to launch your application. + [self measureWithMetrics:@[XCTOSSignpostMetric.applicationLaunchMetric] block:^{ + [[[XCUIApplication alloc] init] launch]; + }]; + } +} + +@end