diff --git a/app/components/AudioCallBox.js b/app/components/AudioCallBox.js index 5a4d7b4..9fa3f6e 100644 --- a/app/components/AudioCallBox.js +++ b/app/components/AudioCallBox.js @@ -1,334 +1,335 @@ import React, { Component } from 'react'; import { View, Platform } from 'react-native'; import { IconButton, Dialog, Text, ActivityIndicator, Colors } from 'react-native-paper'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import EscalateConferenceModal from './EscalateConferenceModal'; import CallOverlay from './CallOverlay'; import DTMFModal from './DTMFModal'; import UserIcon from './UserIcon'; import styles from '../assets/styles/blink/_AudioCallBox.scss'; import utils from '../utils'; import TrafficStats from './BarChart'; function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } class AudioCallBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { remoteUri : this.props.remoteUri, remoteDisplayName : this.props.remoteDisplayName, photo : this.props.photo, active : false, audioMuted : this.props.muted, showDtmfModal : false, showEscalateConferenceModal : false, call : this.props.call, reconnectingCall : this.props.reconnectingCall, info : this.props.info, packetLossQueue : [], audioBandwidthQueue : [], latencyQueue : [] }; this.remoteAudio = React.createRef(); this.userHangup = false; } componentDidMount() { // This component is used both for as 'local media' and as the in-call component. // Thus, if the call is not null it means we are beyond the 'local media' phase // so don't call the mediaPlaying prop. if (this.state.call != null) { switch (this.state.call.state) { case 'established': this.attachStream(this.state.call); break; case 'incoming': this.props.mediaPlaying(); // fall through default: this.state.call.on('stateChanged', this.callStateChanged); break; } } else { this.props.mediaPlaying(); } } componentWillUnmount() { if (this.state.call != null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.call !== null && nextProps.call !== this.state.call) { if (nextProps.call.state === 'established') { this.attachStream(nextProps.call); this.setState({reconnectingCall: false}); } else if (nextProps.call.state === 'incoming') { this.props.mediaPlaying(); } nextProps.call.on('stateChanged', this.callStateChanged); if (this.state.call !== null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } this.setState({call: nextProps.call}); } if (nextProps.reconnectingCall != this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.hasOwnProperty('muted')) { this.setState({audioMuted: nextProps.muted}); } if (nextProps.hasOwnProperty('info')) { this.setState({info: nextProps.info}); } if (nextProps.hasOwnProperty('packetLossQueue')) { this.setState({packetLossQueue: nextProps.packetLossQueue}); } if (nextProps.hasOwnProperty('audioBandwidthQueue')) { this.setState({audioBandwidthQueue: nextProps.audioBandwidthQueue}); } if (nextProps.hasOwnProperty('latencyQueue')) { this.setState({latencyQueue: nextProps.latencyQueue}); } this.setState({remoteUri: nextProps.remoteUri, remoteDisplayName: nextProps.remoteDisplayName, photo: nextProps.photo }); } componentWillUnmount() { if (this.state.call != null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.callTimer); } callStateChanged(oldState, newState, data) { if (newState === 'established') { this.attachStream(this.state.call); this.setState({reconnectingCall: false}); } } attachStream(call) { this.setState({stream: call.getRemoteStreams()[0]}); //we dont use it anywhere though as audio gets automatically piped } escalateToConference(participants) { this.props.escalateToConference(participants); } hangupCall(event) { event.preventDefault(); this.props.hangupCall('user_hangup_call'); this.userHangup = true; } cancelCall(event) { event.preventDefault(); this.props.hangupCall('user_cancelled_call'); } muteAudio(event) { event.preventDefault(); this.props.toggleMute(this.props.call.id, !this.state.audioMuted); } showDtmfModal() { this.setState({showDtmfModal: true}); } hideDtmfModal() { this.setState({showDtmfModal: false}); } toggleEscalateConferenceModal() { this.setState({ showEscalateConferenceModal: !this.state.showEscalateConferenceModal }); } render() { let buttonContainerClass; let userIconContainerClass; let remoteIdentity = {uri: this.state.remoteUri || '', displayName: this.state.remoteDisplayName || '', photo: this.state.photo }; let displayName = this.state.remoteUri ? toTitleCase(this.state.remoteUri.split('@')[0]) : ''; if (this.state.remoteDisplayName && this.state.remoteUri !== this.state.remoteDisplayName) { displayName = this.state.remoteDisplayName; } if (this.props.isTablet) { buttonContainerClass = this.props.orientation === 'landscape' ? styles.tabletLandscapeButtonContainer : styles.tabletPortraitButtonContainer; userIconContainerClass = styles.tabletUserIconContainer; } else { buttonContainerClass = this.props.orientation === 'landscape' ? styles.landscapeButtonContainer : styles.portraitButtonContainer; userIconContainerClass = styles.userIconContainer; } const buttonSize = this.props.isTablet ? 40 : 34; const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; return ( {displayName} {this.state.remoteUri} {this.props.orientation !== 'landscape' && this.state.reconnectingCall ? : null } {this.state.call && ((this.state.call.state === 'accepted' || this.state.call.state === 'established') && !this.state.reconnectingCall) ? : } ); } } AudioCallBox.propTypes = { remoteUri : PropTypes.string, remoteDisplayName : PropTypes.string, photo : PropTypes.string, call : PropTypes.object, connection : PropTypes.object, accountId : PropTypes.string, escalateToConference : PropTypes.func, info : PropTypes.string, hangupCall : PropTypes.func, mediaPlaying : PropTypes.func, callKeepSendDtmf : PropTypes.func, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, orientation : PropTypes.string, isTablet : PropTypes.bool, reconnectingCall : PropTypes.bool, muted : PropTypes.bool, packetLossQueue : PropTypes.array, videoBandwidthQueue : PropTypes.array, audioBandwidthQueue : PropTypes.array, latencyQueue : PropTypes.array }; export default AudioCallBox; diff --git a/app/components/CallOverlay.js b/app/components/CallOverlay.js index ca402e4..949ca29 100644 --- a/app/components/CallOverlay.js +++ b/app/components/CallOverlay.js @@ -1,221 +1,225 @@ import React from 'react'; import { View, Text } from 'react-native'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import moment from 'moment'; import momentFormat from 'moment-duration-format'; import autoBind from 'auto-bind'; import { Appbar } from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import { Colors } from 'react-native-paper'; import styles from '../assets/styles/blink/_AudioCallBox.scss'; function toTitleCase(str) { return str.replace( /\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); } ); } class CallOverlay extends React.Component { constructor(props) { super(props); autoBind(this); this.state = { call: this.props.call, + media: this.props.media ? this.props.media : 'audio', callState: this.props.call ? this.props.call.state : null, direction: this.props.call ? this.props.call.direction: null, remoteUri: this.props.remoteUri, remoteDisplayName: this.props.remoteDisplayName, reconnectingCall: this.props.reconnectingCall } this.duration = null; this.finalDuration = null; this.timer = null; this._isMounted = true; } componentDidMount() { if (this.state.call) { if (this.state.call.state === 'established') { this.startTimer(); } this.state.call.on('stateChanged', this.callStateChanged); this.setState({callState: this.state.call.state}); } } componentWillUnmount() { this._isMounted = false; if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.timer); } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (!this._isMounted) { return; } if (nextProps.reconnectingCall != this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.call !== null && nextProps.call !== this.state.call) { nextProps.call.on('stateChanged', this.callStateChanged); if (this.state.call !== null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } this.setState({call: nextProps.call}); } this.setState({remoteDisplayName: nextProps.remoteDisplayName, - remoteUri: nextProps.remoteUri}); + remoteUri: nextProps.remoteUri, + media: nextProps.media + }); } callStateChanged(oldState, newState, data) { if (newState === 'established' && this._isMounted && !this.props.terminated) { this.startTimer(); } if (newState === 'terminated') { if (this.state.call) { this.state.call.removeListener('stateChanged', this.callStateChanged); } clearTimeout(this.timer); this.finalDuration = this.duration; this.duration = null; this.timer = null; } if (!this._isMounted) { return; } this.setState({callState: newState}); } startTimer() { if (this.timer !== null) { // already armed return; } // TODO: consider using window.requestAnimationFrame const startTime = new Date(); this.timer = setInterval(() => { const duration = moment.duration(new Date() - startTime); if (this.duration > 3600) { this.duration = duration.format('hh:mm:ss', {trim: false}); } else { this.duration = duration.format('mm:ss', {trim: false}); } if (this.props.show) { this.forceUpdate(); } }, 1000); } render() { let header = null; if (this.props.terminated) { clearTimeout(this.timer); this.duration = null; this.timer = null; } let displayName = this.state.remoteUri; if (this.state.remoteDisplayName && this.state.remoteDisplayName !== this.state.remoteUri) { displayName = this.state.remoteDisplayName; } if (this.props.show) { let callDetail; if (this.duration) { callDetail = {this.duration}; callDetail = 'Duration: ' + this.duration; } else { if (this.state.reconnectingCall) { callDetail = 'Reconnecting the call...'; } else if (this.props.terminated) { callDetail = 'Call ended'; } else if (this.state.callState === 'terminated') { callDetail = this.finalDuration ? 'Call ended after ' + this.finalDuration : 'Call ended'; } else { if (this.state.callState) { if (this.state.callState === 'incoming') { callDetail = 'Waiting for incoming call...'; } else if (this.state.callState === 'accepted') { - callDetail = 'Accepted. Waiting for media...'; + callDetail = 'Waiting for ' + this.state.media + '...'; } else { callDetail = toTitleCase(this.state.callState); } } else if (this.state.direction) { callDetail = 'Connecting', this.state.direction, 'call...'; } else { callDetail = 'Connecting...'; } } } if (this.props.info) { callDetail = callDetail + ' - ' + this.props.info; } if (this.state.remoteUri && this.state.remoteUri.search('videoconference') > -1) { displayName = this.state.remoteUri.split('@')[0]; header = ( ); } else { header = ( ); } } return header } } CallOverlay.propTypes = { show: PropTypes.bool.isRequired, remoteUri: PropTypes.string, remoteDisplayName: PropTypes.string, call: PropTypes.object, connection: PropTypes.object, reconnectingCall: PropTypes.bool, terminated : PropTypes.bool, + media: PropTypes.string, info: PropTypes.string }; export default CallOverlay; diff --git a/app/components/Conference.js b/app/components/Conference.js index 25236b9..f41d613 100644 --- a/app/components/Conference.js +++ b/app/components/Conference.js @@ -1,407 +1,414 @@ import React 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 ConferenceBox from './ConferenceBox'; import LocalMedia from './LocalMedia'; import config from '../config'; import utils from '../utils'; const DEBUG = debug('blinkrtc:Conference'); debug.enable('*'); class Conference extends React.Component { constructor(props) { super(props); autoBind(this); this.defaultWaitInterval = 90; // until we can connect or reconnect this.waitCounter = 0; this.waitInterval = this.defaultWaitInterval; this.userHangup = false; this.ended = false; this.started = false; this.participants = []; this.state = { currentCall: null, callState: null, targetUri: this.props.targetUri, callUUID: this.props.callUUID, localMedia: this.props.localMedia, connection: this.props.connection, account: this.props.account, registrationState: this.props.registrationState, startedByPush: this.props.startedByPush, reconnectingCall: this.props.reconnectingCall, myInvitedParties: this.props.myInvitedParties, isFavorite: this.props.favoriteUris.indexOf(this.props.targetUri) > -1 } if (this.props.connection) { this.props.connection.on('stateChanged', this.connectionStateChanged); } if (this.props.participantsToInvite) { this.props.participantsToInvite.forEach((p) => { if (this.participants.indexOf(p) === -1) { this.participants.push(p); } }); } } componentWillUnmount() { this.ended = true; if (this.state.currentCall) { this.state.currentCall.removeListener('stateChanged', this.callStateChanged); } if (this.state.connection) { this.state.connection.removeListener('stateChanged', this.connectionStateChanged); } } callStateChanged(oldState, newState, data) { //utils.timestampedLog('Conference: callStateChanged', oldState, '->', newState); if (newState === 'established') { this.setState({reconnectingCall: false}); } this.setState({callState: newState}); } connectionStateChanged(oldState, newState) { switch (newState) { case 'disconnected': if (oldState === 'ready') { utils.timestampedLog('Conference: connection failed, reconnecting the call...'); this.waitInterval = this.defaultWaitInterval; } break; default: break; } } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { //console.log('Conference got props'); if (nextProps.account !== null && nextProps.account !== this.props.account) { this.setState({account: nextProps.account}); } this.setState({registrationState: nextProps.registrationState}); if (nextProps.connection !== null && nextProps.connection !== this.state.connection) { this.setState({connection: nextProps.connection}); nextProps.connection.on('stateChanged', this.connectionStateChanged); } if (nextProps.reconnectingCall !== this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } if (nextProps.localMedia !== null && nextProps.localMedia !== this.state.localMedia) { this.setState({localMedia: nextProps.localMedia}); } if (nextProps.callUUID !== null && this.state.callUUID !== nextProps.callUUID) { this.setState({callUUID: nextProps.callUUID, reconnectingCall: true, currentCall: null}); this.startCallWhenReady(); } this.setState({myInvitedParties: nextProps.myInvitedParties, isFavorite: nextProps.favoriteUris.indexOf(this.props.targetUri) > -1 }); } mediaPlaying() { this.startCallWhenReady(); } canConnect() { if (!this.state.localMedia) { console.log('Conference: no local media'); return false; } if (!this.state.connection) { console.log('Conference: no connection yet'); return false; } if (this.state.connection.state !== 'ready') { console.log('Conference: connection is not ready'); return false; } if (!this.state.account) { console.log('Conference: no account yet'); return false; } if (this.state.registrationState !== 'registered') { console.log('Conference: account not ready yet'); return false; } if (this.state.currentCall) { console.log('Conference: call already in progress'); return false; } return true; } async startCallWhenReady() { utils.timestampedLog('Conference: start conference', this.state.callUUID, 'when ready to', this.props.targetUri); this.waitCounter = 0; //utils.timestampedLog('Conference: waiting for connecting to the conference', this.waitInterval, 'seconds'); let diff = 0; while (this.waitCounter < this.waitInterval) { if (this.userHangup) { this.props.hangupCall(this.state.callUUID, 'user_cancelled_conference'); return; } if (this.state.currentCall) { return; } if (this.waitCounter >= this.waitInterval - 1) { utils.timestampedLog('Conference: cancelling conference', this.state.callUUID); this.props.hangupCall(this.state.callUUID, 'timeout'); } if (!this.canConnect()) { console.log('Retrying for', (this.waitInterval - this.waitCounter), 'seconds'); await this._sleep(1000); } else { this.waitCounter = 0; this.start(); return; } this.waitCounter++; } } _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } start() { const options = { id: this.state.callUUID, pcConfig: {iceServers: config.iceServers}, localStream: this.state.localMedia, audio: this.props.proposedMedia.audio, video: this.props.proposedMedia.video, offerOptions: { offerToReceiveAudio: false, offerToReceiveVideo: false }, initialParticipants: this.props.participantsToInvite }; utils.timestampedLog('Conference: Sylkrtc.js will start conference call', this.state.callUUID, 'to', this.props.targetUri.toLowerCase()); confCall = this.state.account.joinConference(this.props.targetUri.toLowerCase(), options); if (confCall) { confCall.on('stateChanged', this.callStateChanged); this.setState({currentCall: confCall}); } } saveParticipant(callUUID, room, uri) { console.log('Save saveParticipant', uri); if (this.participants.indexOf(uri) === -1) { this.participants.push(uri); } this.props.saveParticipant(callUUID, room, uri); } showSaveDialog() { if (!this.userHangup) { return false; } if (this.state.reconnectingCall) { console.log('No save dialog because call is reconnecting') return false; } if (this.participants.length === 0) { console.log('No show dialog because there are no participants') return false; } if (this.state.isFavorite) { let room = this.state.targetUri.split('@')[0]; let must_display = false; if (this.props.myInvitedParties.hasOwnProperty(room)) { let old_participants = this.state.myInvitedParties[room]; this.participants.forEach((p) => { if (old_participants.indexOf(p) === -1) { console.log(p, 'is not in', old_participants); must_display = true; } }); } if (must_display) { console.log('Show save dialog because we have new participants'); return true; } else { console.log('No save dialog because is already favorite with same participants') return false; } } else { console.log('Show save dialog because is not in favorites'); return true; } return true; } saveConference() { if (!this.state.isFavorite) { this.props.setFavoriteUri(this.props.targetUri); } let room = this.state.targetUri.split('@')[0]; if (this.props.myInvitedParties.hasOwnProperty(room)) { let participants = this.state.myInvitedParties[room]; this.participants.forEach((p) => { if (participants.indexOf(p) === -1) { participants.push(p); } }); this.props.saveInvitedParties(this.props.targetUri, participants); } else { this.props.saveInvitedParties(this.props.targetUri, this.participants); } this.props.hangupCall(this.state.callUUID, 'user_hangup_conference_confirmed'); } hangup(reason='user_hangup_conference') { this.userHangup = true; if (!this.showSaveDialog()) { reason = 'user_hangup_conference_confirmed'; } this.props.hangupCall(this.state.callUUID, reason); if (this.waitCounter > 0) { this.waitCounter = this.waitInterval; } } render() { let box = null; if (this.state.localMedia !== null) { + let media = 'audio' + if (this.props.proposedMedia && this.props.proposedMedia.video === true) { + media = 'audio and video'; + } + if (this.state.currentCall != null && (this.state.callState === 'established')) { box = ( ); } else { box = ( + ); } } else { console.log('Error: cannot start conference without local media'); } return box; } } Conference.propTypes = { notificationCenter : PropTypes.func, account : PropTypes.object, connection : PropTypes.object, registrationState : PropTypes.string, hangupCall : PropTypes.func, saveParticipant : PropTypes.func, saveInvitedParties : PropTypes.func, previousParticipants : PropTypes.array, currentCall : PropTypes.object, localMedia : PropTypes.object, targetUri : PropTypes.string, participantsToInvite : PropTypes.array, generatedVideoTrack : PropTypes.bool, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, callUUID : PropTypes.string, proposedMedia : PropTypes.object, isLandscape : PropTypes.bool, isTablet : PropTypes.bool, muted : PropTypes.bool, defaultDomain : PropTypes.string, startedByPush : PropTypes.bool, inFocus : PropTypes.bool, setFavoriteUri : PropTypes.func, saveInvitedParties : PropTypes.func, reconnectingCall : PropTypes.bool, contacts : PropTypes.array, favoriteUris : PropTypes.array, myDisplayNames : PropTypes.object }; export default Conference; diff --git a/app/components/LocalMedia.js b/app/components/LocalMedia.js index cc4fbed..3d3175e 100644 --- a/app/components/LocalMedia.js +++ b/app/components/LocalMedia.js @@ -1,144 +1,145 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import { View, Dimensions } from 'react-native'; import { RTCView } from 'react-native-webrtc'; import { IconButton, Button, Text} from 'react-native-paper'; import CallOverlay from './CallOverlay'; import styles from '../assets/styles/blink/_LocalMedia.scss'; class LocalMedia extends Component { constructor(props) { super(props); autoBind(this); this.localVideo = React.createRef(); this.state = { localMedia: this.props.localMedia, historyEntry: this.props.historyEntry, participants: this.props.participants, reconnectingCall: this.props.reconnectingCall }; } componentDidMount() { this.props.mediaPlaying(); } //getDerivedStateFromProps(nextProps, state) UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.localMedia && nextProps.localMedia !== this.state.localMedia) { this.props.mediaPlaying(); } this.setState({historyEntry: nextProps.historyEntry, participants: nextProps.participants, reconnectingCall: nextProps.reconnectingCall}); } saveConference(event) { event.preventDefault(); this.props.saveConference(); } showSaveDialog() { if (!this.props.showSaveDialog) { return false; } return this.props.showSaveDialog(); } hangupCall(event) { event.preventDefault(); this.props.hangupCall('user_hangup_conference_confirmed'); } render() { let {height, width} = Dimensions.get('window'); let videoStyle = { height, width }; const streamUrl = this.props.localMedia ? this.props.localMedia.toURL() : null; const buttonSize = this.props.isTablet ? 40 : 34; const buttonContainerClass = this.props.isTablet ? styles.tabletButtonContainer : styles.buttonContainer; return ( {this.showSaveDialog() ? Save conference maybe? Would you like to save participants {this.state.participants.toString().replace(/,/g, ', ')} for having another conference later? You can find later it in your Favorites. : } ); } } LocalMedia.propTypes = { call : PropTypes.object, remoteUri : PropTypes.string, remoteDisplayName : PropTypes.string, localMedia : PropTypes.object.isRequired, mediaPlaying : PropTypes.func.isRequired, hangupCall : PropTypes.func, showSaveDialog : PropTypes.func, saveConference : PropTypes.func, reconnectingCall : PropTypes.bool, connection : PropTypes.object, participants : PropTypes.array, + media : PropTypes.string, terminated : PropTypes.bool }; export default LocalMedia; diff --git a/app/components/VideoBox.js b/app/components/VideoBox.js index 9ea10a6..12d2227 100644 --- a/app/components/VideoBox.js +++ b/app/components/VideoBox.js @@ -1,405 +1,406 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import dtmf from 'react-native-dtmf'; import debug from 'react-native-debug'; import autoBind from 'auto-bind'; import { IconButton, ActivityIndicator, Colors } from 'react-native-paper'; import { View, Dimensions, TouchableWithoutFeedback, Platform } from 'react-native'; import { RTCView } from 'react-native-webrtc'; import CallOverlay from './CallOverlay'; import EscalateConferenceModal from './EscalateConferenceModal'; import DTMFModal from './DTMFModal'; import config from '../config'; import styles from '../assets/styles/blink/_VideoBox.scss'; //import TrafficStats from './BarChart'; const DEBUG = debug('blinkrtc:Video'); debug.enable('*'); class VideoBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { call: this.props.call, reconnectingCall: this.props.reconnectingCall, audioMuted: this.props.muted, mirror: true, callOverlayVisible: true, videoMuted: false, localVideoShow: true, remoteVideoShow: true, remoteSharesScreen: false, showEscalateConferenceModal: false, localStream: this.props.call.getLocalStreams()[0], remoteStream: this.props.call.getRemoteStreams()[0], info: this.props.info, showDtmfModal: false, doorOpened: false, packetLossQueue : [], audioBandwidthQueue : [], latencyQueue : [] }; this.overlayTimer = null; this.localVideo = React.createRef(); this.remoteVideo = React.createRef(); this.userHangup = false; } //getDerivedStateFromProps(nextProps, state) { UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.hasOwnProperty('muted')) { this.setState({audioMuted: nextProps.muted}); } if (nextProps.hasOwnProperty('info')) { this.setState({info: nextProps.info}); } if (nextProps.hasOwnProperty('packetLossQueue')) { this.setState({packetLossQueue: nextProps.packetLossQueue}); } if (nextProps.hasOwnProperty('audioBandwidthQueue')) { this.setState({audioBandwidthQueue: nextProps.audioBandwidthQueue}); } if (nextProps.hasOwnProperty('latencyQueue')) { this.setState({latencyQueue: nextProps.latencyQueue}); } if (nextProps.call && nextProps.call !== this.state.call) { nextProps.call.on('stateChanged', this.callStateChanged); if (this.state.call !== null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } this.setState({call: nextProps.call, localStream: nextProps.call.getLocalStreams()[0], remoteStream: nextProps.call.getRemoteStreams()[0] }); } if (nextProps.reconnectingCall != this.state.reconnectingCall) { this.setState({reconnectingCall: nextProps.reconnectingCall}); } } callStateChanged(oldState, newState, data) { this.forceUpdate(); } openDoor() { const tone = this.props.intercomDtmfTone; DEBUG('DTMF tone sent to intercom: ' + tone); this.setState({doorOpened: true}); this.forceUpdate(); dtmf.stopTone(); //don't play a tone at the same time as another dtmf.playTone(dtmf['DTMF_' + tone], 1000); if (this.state.call !== null && this.state.call.state === 'established') { this.state.call.sendDtmf(tone); } } componentDidMount() { if (this.state.call) { this.state.call.on('stateChanged', this.callStateChanged); } this.armOverlayTimer(); } componentWillUnmount() { if (this.state.call != null) { this.state.call.removeListener('stateChanged', this.callStateChanged); } } showDtmfModal() { this.setState({showDtmfModal: true}); } hideDtmfModal() { this.setState({showDtmfModal: false}); } handleFullscreen(event) { event.preventDefault(); // this.toggleFullscreen(); } handleRemoteVideoPlaying() { this.setState({remoteVideoShow: true}); } handleRemoteResize(event, target) { const resolutions = [ '1280x720', '960x540', '640x480', '640x360', '480x270','320x180']; const videoResolution = event.target.videoWidth + 'x' + event.target.videoHeight; if (resolutions.indexOf(videoResolution) === -1) { this.setState({remoteSharesScreen: true}); } else { this.setState({remoteSharesScreen: false}); } } muteAudio(event) { event.preventDefault(); this.props.toggleMute(this.state.call.id, !this.state.audioMuted); } muteVideo(event) { event.preventDefault(); const localStream = this.state.localStream; if (localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; if(this.state.videoMuted) { DEBUG('Unmute camera'); track.enabled = true; this.setState({videoMuted: false}); } else { DEBUG('Mute camera'); track.enabled = false; this.setState({videoMuted: true}); } } } toggleCamera(event) { event.preventDefault(); const localStream = this.state.localStream; if (localStream.getVideoTracks().length > 0) { const track = localStream.getVideoTracks()[0]; track._switchCamera(); this.setState({mirror: !this.state.mirror}); } } hangupCall(event) { event.preventDefault(); this.props.hangupCall('user_hangup_call'); this.userHangup = true; } cancelCall(event) { event.preventDefault(); this.props.hangupCall('user_cancelled_call'); } escalateToConference(participants) { this.props.escalateToConference(participants); } armOverlayTimer() { clearTimeout(this.overlayTimer); this.overlayTimer = setTimeout(() => { this.setState({callOverlayVisible: false}); }, 4000); } toggleCallOverlay() { this.setState({callOverlayVisible: !this.state.callOverlayVisible}); } toggleEscalateConferenceModal() { this.setState({ callOverlayVisible : false, showEscalateConferenceModal : !this.state.showEscalateConferenceModal }); } render() { if (this.state.call === null) { return null; } // 'mirror' : !this.state.call.sharingScreen && !this.props.generatedVideoTrack, // we do not want mirrored local video once the call has started, just in preview const localVideoClasses = classNames({ 'video-thumbnail' : true, 'hidden' : !this.state.localVideoShow, 'animated' : true, 'fadeIn' : this.state.localVideoShow || this.state.videoMuted, 'fadeOut' : this.state.videoMuted, 'fit' : this.state.call.sharingScreen }); const remoteVideoClasses = classNames({ 'poster' : !this.state.remoteVideoShow, 'animated' : true, 'fadeIn' : this.state.remoteVideoShow, 'large' : true, 'fit' : this.state.remoteSharesScreen }); let buttonContainerClass; let buttons; const muteButtonIcons = this.state.audioMuted ? 'microphone-off' : 'microphone'; const muteVideoButtonIcons = this.state.videoMuted ? 'video-off' : 'video'; const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; const buttonSize = this.props.isTablet ? 40 : 34; if (this.props.isTablet) { buttonContainerClass = this.props.orientation === 'landscape' ? styles.tabletLandscapeButtonContainer : styles.tabletPortraitButtonContainer; userIconContainerClass = styles.tabletUserIconContainer; } else { buttonContainerClass = this.props.orientation === 'landscape' ? styles.landscapeButtonContainer : styles.portraitButtonContainer; } if (this.state.callOverlayVisible) { let content = ( ); if (this.props.intercomDtmfTone) { content = ( ); } buttons = ({content}); } const remoteStreamUrl = this.state.remoteStream ? this.state.remoteStream.toURL() : null const show = this.state.callOverlayVisible || this.state.reconnectingCall; return ( {this.state.remoteVideoShow && !this.state.reconnectingCall ? : null } { this.state.localVideoShow ? : null } {this.state.reconnectingCall ? : null } {buttons} ); } } VideoBox.propTypes = { call : PropTypes.object, connection : PropTypes.object, photo : PropTypes.string, accountId : PropTypes.string, remoteUri : PropTypes.string, remoteDisplayName : PropTypes.string, localMedia : PropTypes.object, hangupCall : PropTypes.func, info : PropTypes.string, shareScreen : PropTypes.func, escalateToConference : PropTypes.func, generatedVideoTrack : PropTypes.bool, callKeepSendDtmf : PropTypes.func, toggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool, intercomDtmfTone : PropTypes.string, orientation : PropTypes.string, isTablet : PropTypes.bool, reconnectingCall : PropTypes.bool, muted : PropTypes.bool }; export default VideoBox;