diff --git a/app/assets/styles/blink/_AudioCallBox.scss b/app/assets/styles/blink/_AudioCallBox.scss index e735706..f999d26 100644 --- a/app/assets/styles/blink/_AudioCallBox.scss +++ b/app/assets/styles/blink/_AudioCallBox.scss @@ -1,46 +1,53 @@ .container { flex: 1; } .userIconContainer { padding-top: 50px; margin: 0 auto; } .buttonContainer { justify-self: flex-end; flex-direction: row; margin: 0 auto; margin-top:auto; - bottom: 30; + bottom: 20; } .button { background-color: white; margin: 10px; padding-top: 5px; padding-left: 0px; } .iosButton { background-color: rgba(#F9F9F9, .7); margin: 10px; padding-top: 5px; } .androidButton { background-color: rgba(#F9F9F9, .7); margin: 10px; padding-top: 1px; } .hangupButton { background-color: rgba(#a94442, .8); } -.address { - padding: 0px; +.displayName { + padding-top: 10px; font-size: 30px; text-align: center; color: white; } +.uri { + padding: 0px; + font-size: 18px; + text-align: center; + color: white; +} + diff --git a/app/components/AudioCallBox.js b/app/components/AudioCallBox.js index 004e14d..a9defb3 100644 --- a/app/components/AudioCallBox.js +++ b/app/components/AudioCallBox.js @@ -1,218 +1,230 @@ import React, { Component } from 'react'; import { View, Platform } from 'react-native'; -import { IconButton, Dialog } from 'react-native-paper'; +import { IconButton, Dialog, Text } from 'react-native-paper'; import PropTypes from 'prop-types'; import autoBind from 'auto-bind'; import Logger from "../../Logger"; import CallOverlay from './CallOverlay'; import DTMFModal from './DTMFModal'; import EscalateConferenceModal from './EscalateConferenceModal'; import UserIcon from './UserIcon'; +import utils from '../utils'; + import styles from '../assets/styles/blink/_AudioCallBox.scss'; const logger = new Logger("AudioCallBox"); class AudioCallBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { active : false, audioMuted : false, showDtmfModal : false, showEscalateConferenceModal : false }; // this.speechEvents = null; this.remoteAudio = React.createRef(); } 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.props.call != null) { switch (this.props.call.state) { case 'established': this.attachStream(this.props.call); break; case 'incoming': this.props.mediaPlaying(); // fall through default: this.props.call.on('stateChanged', this.callStateChanged); break; } } else { this.props.mediaPlaying(); } + } componentWillReceiveProps(nextProps) { if (this.props.call == null && nextProps.call) { if (nextProps.call.state === 'established') { this.attachStream(nextProps.call); } else { nextProps.call.on('stateChanged', this.callStateChanged); } } } componentWillUnmount() { clearTimeout(this.callTimer); // if (this.speechEvents !== null) { // this.speechEvents.stop(); // this.speechEvents = null; // } } callStateChanged(oldState, newState, data) { if (newState === 'established') { this.attachStream(this.props.call); } } attachStream(call) { this.setState({stream: call.getRemoteStreams()[0]}); //we dont use it anywhere though as audio gets automatically piped // const options = { // interval: 225, // play: false // }; // this.speechEvents = hark(remoteStream, options); // this.speechEvents.on('speaking', () => { // this.setState({active: true}); // }); // this.speechEvents.on('stopped_speaking', () => { // this.setState({active: false}); // }); } escalateToConference(participants) { this.props.escalateToConference(participants); } hangupCall(event) { event.preventDefault(); this.props.hangupCall(); } muteAudio(event) { event.preventDefault(); //const localStream = this.props.call.getLocalStreams()[0]; if(this.state.audioMuted) { logger.debug('Unmute microphone'); this.props.callKeepToggleMute(false); //localStream.getAudioTracks()[0].enabled = true; this.setState({audioMuted: false}); } else { logger.debug('Mute microphone'); //localStream.getAudioTracks()[0].enabled = false; this.props.callKeepToggleMute(true); this.setState({audioMuted: true}); } } showDtmfModal() { this.setState({showDtmfModal: true}); } hideDtmfModal() { this.setState({showDtmfModal: false}); } toggleEscalateConferenceModal() { this.setState({ showEscalateConferenceModal: !this.state.showEscalateConferenceModal }); } + render() { + let remoteIdentity; - const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; if (this.props.call !== null) { remoteIdentity = this.props.call.remoteIdentity; } else { - remoteIdentity = {uri: this.props.remoteIdentity}; + remoteIdentity = {uri: this.props.remoteUri}; } + const buttonClass = (Platform.OS === 'ios') ? styles.iosButton : styles.androidButton; + return ( - {this.props.remoteIdentity} + {this.props.remoteDisplayName} + + { (this.props.remoteUri !== this.props.remoteDisplayName) ? + {this.props.remoteUri} + : null } + ); } } AudioCallBox.propTypes = { call : PropTypes.object, escalateToConference : PropTypes.func, hangupCall : PropTypes.func, mediaPlaying : PropTypes.func, - remoteIdentity : PropTypes.string, + remoteUri : PropTypes.string, + remoteDisplayName : PropTypes.string, callKeepSendDtmf : PropTypes.func, callKeepToggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool }; export default AudioCallBox; diff --git a/app/components/Call.js b/app/components/Call.js index 8ef576c..8624557 100644 --- a/app/components/Call.js +++ b/app/components/Call.js @@ -1,196 +1,204 @@ 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 Logger from "../../Logger"; import AudioCallBox from './AudioCallBox'; import LocalMedia from './LocalMedia'; import VideoBox from './VideoBox'; import config from '../config'; const logger = new Logger("Call"); class Call extends Component { constructor(props) { super(props); autoBind(this); if (this.props.localMedia && this.props.localMedia.getVideoTracks().length === 0) { logger.debug('Will send audio only'); this.state = {audioOnly: true}; } else { this.state = {audioOnly: false}; } // If current call is available on mount we must have incoming if (this.props.currentCall != null) { this.props.currentCall.on('stateChanged', this.callStateChanged); } } componentWillReceiveProps(nextProps) { // Needed for switching to incoming call while in a call if (this.props.currentCall != null && this.props.currentCall != nextProps.currentCall) { if (nextProps.currentCall != null) { nextProps.currentCall.on('stateChanged', this.callStateChanged); } else { this.props.currentCall.removeListener('stateChanged', this.callStateChanged); } } } callStateChanged(oldState, newState, data) { // console.log('Call: callStateChanged', newState, '->', newState); if (newState === 'established') { // Check the media type again, remote can choose to not accept all offered media types const currentCall = this.props.currentCall; const remoteHasStreams = currentCall.getRemoteStreams().length > 0; const remoteHasNoVideoTracks = currentCall.getRemoteStreams()[0].getVideoTracks().length === 0; const remoteIsRecvOnly = currentCall.remoteMediaDirections.video[0] === 'recvonly'; const remoteIsInactive = currentCall.remoteMediaDirections.video[0] === 'inactive'; if (remoteHasStreams && (remoteHasNoVideoTracks || remoteIsRecvOnly || remoteIsInactive) && !this.state.audioOnly) { console.log('Media type changed to audio'); // Stop local video if (this.props.localMedia.getVideoTracks().length !== 0) { currentCall.getLocalStreams()[0].getVideoTracks()[0].stop(); } this.setState({audioOnly: true}); this.props.speakerphoneOff(); } else { this.forceUpdate(); } currentCall.removeListener('stateChanged', this.callStateChanged); // Switch to video earlier. The callOverlay has a handle on // 'established'. It starts a timer. To prevent a state updating on // unmounted component we try to switch on 'accept'. This means we get // to localMedia first. } 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.props.localMedia && this.props.localMedia.getVideoTracks().length !== 0) { console.log('Media type changed to video on accepted'); this.setState({audioOnly: false}); this.props.speakerphoneOn(); } } this.forceUpdate(); } call() { assert(this.props.currentCall === null, 'currentCall is not null'); //console.log('Call: starting call', this.props.callUUID, 'to', this.props.targetUri); let options = {pcConfig: {iceServers: config.iceServers}, id: this.props.callUUID}; options.localStream = this.props.localMedia; let call = this.props.account.call(this.props.targetUri, options); call.on('stateChanged', this.callStateChanged); } answerCall() { console.log('Call: answer call'); assert(this.props.currentCall !== null, 'currentCall is null'); let options = {pcConfig: {iceServers: config.iceServers}}; options.localStream = this.props.localMedia; this.props.currentCall.answer(options); } hangupCall() { console.log('Call: hangup call'); let callUUID = this.props.currentCall._callkeepUUID; this.props.hangupCall(callUUID); } mediaPlaying() { if (this.props.currentCall === null) { this.call(); } else { this.answerCall(); } } render() { //console.log('Call: render'); let box = null; - let remoteIdentity; + + let remoteUri; + let remoteDisplayName; if (this.props.currentCall !== null) { - remoteIdentity = this.props.currentCall.remoteIdentity.displayName || this.props.currentCall.remoteIdentity.uri; + remoteUri = this.props.currentCall.remoteIdentity.uri; + remoteDisplayName = this.props.currentCall.remoteIdentity.displayName || this.props.currentCall.remoteIdentity.uri; + } else { - remoteIdentity = this.props.targetUri; + remoteUri = this.props.targetUri; + remoteDisplayName = this.props.targetUri; } if (this.props.localMedia !== null) { if (this.state.audioOnly) { box = ( ); } else { if (this.props.currentCall != null && this.props.currentCall.state === 'established') { box = ( ); } else { if (this.props.currentCall && this.props.currentCall.state && this.props.currentCall.state === 'terminated') { // do not render } else { box = ( ); } } } } return box; } } Call.propTypes = { account : PropTypes.object.isRequired, hangupCall : PropTypes.func.isRequired, shareScreen : PropTypes.func, currentCall : PropTypes.object, escalateToConference : PropTypes.func, localMedia : PropTypes.object, targetUri : PropTypes.string, generatedVideoTrack : PropTypes.bool, callKeepSendDtmf : PropTypes.func, callKeepToggleMute : PropTypes.func, speakerphoneOn : PropTypes.func, speakerphoneOff : PropTypes.func, callUUID : PropTypes.string }; export default Call; diff --git a/app/components/UserIcon.js b/app/components/UserIcon.js index 2728e58..b7950c8 100644 --- a/app/components/UserIcon.js +++ b/app/components/UserIcon.js @@ -1,67 +1,67 @@ import React, { useEffect, useState } from'react'; import PropTypes from 'prop-types'; import utils from '../utils'; import { Avatar } from 'react-native-paper'; const UserIcon = (props) => { const [photo, setPhoto] = useState(''); useEffect(() => { // You need to restrict it at some point // This is just dummy code and should be replaced by actual if (!photo && props.identity.uri) { getPhoto(); } }, []); const getPhoto = async () => { try { let contacts = await utils.findContact(props.identity.uri); console.log('contacts', contacts) contacts.some((contact) => { if (contact.hasThumbnail) { setPhoto(contact.thumbnailPath); return true; } }); } catch (err) { console.log('error getting contacts', err); } } const name = props.identity.displayName || props.identity.uri; let initials = name.split(' ', 2).map(x => x[0]).join(''); const color = utils.generateMaterialColor(props.identity.uri)['300']; const avatarSize = props.large ? 120: 60; if (photo) { console.log('got an image', photo) - return + return } if (props.identity.uri.search('anonymous') !== -1) { return ( - + ) } if (props.identity.uri.search('videoconference') !== -1) { return ( - + ) } return ( ); }; UserIcon.propTypes = { identity: PropTypes.object.isRequired, large: PropTypes.bool, card: PropTypes.bool, active: PropTypes.bool }; export default UserIcon; diff --git a/app/components/VideoBox.js b/app/components/VideoBox.js index 4744d08..8d4d172 100644 --- a/app/components/VideoBox.js +++ b/app/components/VideoBox.js @@ -1,402 +1,404 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import debug from 'react-native-debug'; import autoBind from 'auto-bind'; import { IconButton } from 'react-native-paper'; import { View, Dimensions, TouchableOpacity, 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 dtmf from 'react-native-dtmf'; import styles from '../assets/styles/blink/_VideoBox.scss'; const DEBUG = debug('blinkrtc:Video'); debug.enable('*'); class VideoBox extends Component { constructor(props) { super(props); autoBind(this); this.state = { callOverlayVisible: true, audioMuted: false, videoMuted: false, localVideoShow: false, remoteVideoShow: false, remoteSharesScreen: false, showEscalateConferenceModal: false, localStream: null, remoteStream: null, showDtmfModal: false, doorOpened: false }; this.overlayTimer = null; this.localVideo = React.createRef(); this.remoteVideo = React.createRef(); } callStateChanged(oldState, newState, data) { DEBUG(`Call state changed ${oldState} -> ${newState}`); if (newState === 'established') { this.forceUpdate(); } } openDoor() { const tone = config.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.props.call !== null && this.props.call.state === 'established') { this.props.call.sendDtmf(tone); /*this.props.notificationCenter.postSystemNotification('Door opened', {timeout: 5});*/ } } componentDidMount() { console.log('localStreams', this.props.call.getLocalStreams()); console.log('remoteStreams', this.props.call.getRemoteStreams()); this.setState({localStream: this.props.call.getLocalStreams()[0], localVideoShow: true, remoteStream: this.props.call.getRemoteStreams()[0], remoteVideoShow: true}); this.props.call.on('stateChanged', this.callStateChanged); // sylkrtc.utils.attachMediaStream(, this.localVideo.current, {disableContextMenu: true}); // let promise = this.localVideo.current.play() // if (promise !== undefined) { // promise.then(_ => { // this.setState({localVideoShow: true}); // eslint-disable-line react/no-did-mount-set-state // // Autoplay started! // }).catch(error => { // // Autoplay was prevented. // // Show a "Play" button so that user can start playback. // }); // } else { // this.localVideo.current.addEventListener('playing', () => { // this.setState({}); // eslint-disable-line react/no-did-mount-set-state // }); // } // this.remoteVideo.current.addEventListener('playing', this.handleRemoteVideoPlaying); // sylkrtc.utils.attachMediaStream(this.props.call.getRemoteStreams()[0], this.remoteVideo.current, {disableContextMenu: true}); } componentWillUnmount() { // clearTimeout(this.overlayTimer); // this.remoteVideo.current.removeEventListener('playing', this.handleRemoteVideoPlaying); // this.exitFullscreen(); } showDtmfModal() { this.setState({showDtmfModal: true}); } hideDtmfModal() { this.setState({showDtmfModal: false}); } handleFullscreen(event) { event.preventDefault(); // this.toggleFullscreen(); } handleRemoteVideoPlaying() { this.setState({remoteVideoShow: true}); // this.remoteVideo.current.onresize = (event) => { // this.handleRemoteResize(event) // }; // this.armOverlayTimer(); } handleRemoteResize(event, target) { //DEBUG("%o", event); 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(); const localStream = this.state.localStream; if (localStream.getAudioTracks().length > 0) { //const track = localStream.getAudioTracks()[0]; if(this.state.audioMuted) { DEBUG('Unmute microphone'); //track.enabled = true; this.props.callKeepToggleMute(false); this.setState({audioMuted: false}); } else { DEBUG('Mute microphone'); // track.enabled = false; this.props.callKeepToggleMute(true); this.setState({audioMuted: true}); } } } 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(); } } hangupCall(event) { event.preventDefault(); this.props.hangupCall(); } escalateToConference(participants) { this.props.escalateToConference(participants); } armOverlayTimer() { clearTimeout(this.overlayTimer); this.overlayTimer = setTimeout(() => { this.setState({callOverlayVisible: false}); }, 4000); } showCallOverlay() { if (this.state.remoteVideoShow) { this.setState({callOverlayVisible: true}); this.armOverlayTimer(); } } toggleEscalateConferenceModal() { this.setState({ callOverlayVisible : false, showEscalateConferenceModal : !this.state.showEscalateConferenceModal }); } render() { if (this.props.call == null) { return null; } const localVideoClasses = classNames({ 'video-thumbnail' : true, 'mirror' : !this.props.call.sharingScreen && !this.props.generatedVideoTrack, 'hidden' : !this.state.localVideoShow, 'animated' : true, 'fadeIn' : this.state.localVideoShow || this.state.videoMuted, 'fadeOut' : this.state.videoMuted, 'fit' : this.props.call.sharingScreen }); const remoteVideoClasses = classNames({ 'poster' : !this.state.remoteVideoShow, 'animated' : true, 'fadeIn' : this.state.remoteVideoShow, 'large' : true, 'fit' : this.state.remoteSharesScreen }); let callButtons; let watermark; if (this.state.callOverlayVisible) { // const screenSharingButtonIcons = classNames({ // 'fa' : true, // 'fa-clone' : true, // 'fa-flip-horizontal' : true, // 'text-warning' : this.props.call.sharingScreen // }); // const fullScreenButtonIcons = classNames({ // 'fa' : true, // 'fa-expand' : !this.isFullScreen(), // 'fa-compress' : this.isFullScreen() // }); // const commonButtonClasses = classNames({ // 'btn' : true, // 'btn-round' : true, // 'btn-default' : true // }); const buttons = []; // buttons.push(); // if (this.isFullscreenSupported()) { // buttons.push(); // } // buttons.push(
); // callButtons = ( // // // // // ); } else { // watermark = ( // // // // ); } console.log('local media stream in videobox', this.state); 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; return ( {/*onMouseMove={this.showCallOverlay}*/} {/* */} {/* {watermark} */} {/* */} {this.state.remoteVideoShow ? : null } { this.state.localVideoShow ? : null } { config.intercomDtmfTone ? : } ); } } VideoBox.propTypes = { call : PropTypes.object, + remoteUri : PropTypes.string, + remoteDisplayName : PropTypes.string, localMedia : PropTypes.object, hangupCall : PropTypes.func, shareScreen : PropTypes.func, escalateToConference : PropTypes.func, generatedVideoTrack : PropTypes.bool, callKeepSendDtmf : PropTypes.func, callKeepToggleMute : PropTypes.func, toggleSpeakerPhone : PropTypes.func, speakerPhoneEnabled : PropTypes.bool }; export default VideoBox;