diff options
Diffstat (limited to 'webapp/components/webrtc')
-rw-r--r-- | webapp/components/webrtc/components/webrtc_header.jsx | 100 | ||||
-rw-r--r-- | webapp/components/webrtc/components/webrtc_notification.jsx | 320 | ||||
-rw-r--r-- | webapp/components/webrtc/components/webrtc_sidebar.jsx | 133 | ||||
-rw-r--r-- | webapp/components/webrtc/webrtc_controller.jsx | 1214 |
4 files changed, 1767 insertions, 0 deletions
diff --git a/webapp/components/webrtc/components/webrtc_header.jsx b/webapp/components/webrtc/components/webrtc_header.jsx new file mode 100644 index 000000000..a4a9c740c --- /dev/null +++ b/webapp/components/webrtc/components/webrtc_header.jsx @@ -0,0 +1,100 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Constants from 'utils/constants.jsx'; + +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class WebrtcHeader extends React.Component { + render() { + const title = ( + <FormattedMessage + id='webrtc.header' + defaultMessage='Call with {username}' + values={{ + username: this.props.username + }} + /> + ); + + const closeSidebarTooltip = ( + <Tooltip id='closeSidebarTooltip'> + <FormattedMessage + id='rhs_header.closeTooltip' + defaultMessage='Close Sidebar' + /> + </Tooltip> + ); + + const expandSidebarTooltip = ( + <Tooltip id='expandSidebarTooltip'> + <FormattedMessage + id='rhs_header.expandTooltip' + defaultMessage='Expand Sidebar' + /> + </Tooltip> + ); + + const shrinkSidebarTooltip = ( + <Tooltip id='shrinkSidebarTooltip'> + <FormattedMessage + id='rhs_header.expandTooltip' + defaultMessage='Shrink Sidebar' + /> + </Tooltip> + ); + + return ( + <div className='sidebar--right__header'> + <span className='sidebar--right__title'>{title}</span> + <div className='pull-right'> + <button + type='button' + className='sidebar--right__expand' + aria-label='Expand' + onClick={this.props.toggleSize} + > + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={expandSidebarTooltip} + > + <i className='fa fa-expand'/> + </OverlayTrigger> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={shrinkSidebarTooltip} + > + <i className='fa fa-compress'/> + </OverlayTrigger> + </button> + <button + type='button' + className='sidebar--right__close' + aria-label='Close' + title='Close' + onClick={this.props.onClose} + > + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={closeSidebarTooltip} + > + <i className='fa fa-sign-out'/> + </OverlayTrigger> + </button> + </div> + </div> + ); + } +} + +WebrtcHeader.propTypes = { + username: React.PropTypes.string.isRequired, + onClose: React.PropTypes.func.isRequired, + toggleSize: React.PropTypes.function +}; diff --git a/webapp/components/webrtc/components/webrtc_notification.jsx b/webapp/components/webrtc/components/webrtc_notification.jsx new file mode 100644 index 000000000..5456d6cb8 --- /dev/null +++ b/webapp/components/webrtc/components/webrtc_notification.jsx @@ -0,0 +1,320 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Client from 'client/web_client.jsx'; +import WebSocketClient from 'client/web_websocket_client.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; + +import * as GlobalActions from 'actions/global_actions.jsx'; +import * as WebrtcActions from 'actions/webrtc_actions.jsx'; +import * as Utils from 'utils/utils.jsx'; +import {Constants, WebrtcActionTypes} from 'utils/constants.jsx'; + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; + +import ring from 'images/ring.mp3'; + +const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES; + +export default class WebrtcNotification extends React.Component { + constructor() { + super(); + + this.mounted = false; + + this.closeNotification = this.closeNotification.bind(this); + this.onIncomingCall = this.onIncomingCall.bind(this); + this.onCancelCall = this.onCancelCall.bind(this); + this.onRhs = this.onRhs.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleAnswer = this.handleAnswer.bind(this); + this.handleTimeout = this.handleTimeout.bind(this); + this.stopRinging = this.stopRinging.bind(this); + this.closeRightHandSide = this.closeRightHandSide.bind(this); + + this.state = { + userCalling: null, + rhsOpened: false + }; + } + + componentDidMount() { + WebrtcStore.addNotifyListener(this.onIncomingCall); + WebrtcStore.addChangedListener(this.onCancelCall); + WebrtcStore.addRhsChangedListener(this.onRhs); + this.mounted = true; + } + + componentWillUnmount() { + WebrtcStore.removeNotifyListener(this.onIncomingCall); + WebrtcStore.removeChangedListener(this.onCancelCall); + WebrtcStore.removeRhsChangedListener(this.onRhs); + if (this.refs.ring) { + this.refs.ring.removeListener('ended', this.handleTimeout); + } + this.mounted = false; + } + + componentDidUpdate() { + if (this.state.userCalling) { + this.refs.ring.addEventListener('ended', this.handleTimeout); + } + } + + closeNotification() { + this.setState({ + userCalling: null + }); + } + + stopRinging() { + if (this.refs.ring) { + this.refs.ring.pause(); + this.refs.ring.currentTime = 0; + } + this.setState({userCalling: null}); + } + + closeRightHandSide(e) { + e.preventDefault(); + GlobalActions.emitCloseRightHandSide(); + } + + onIncomingCall(incoming) { + if (this.mounted) { + const userId = incoming.from_user_id; + const userMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + const featureEnabled = Utils.isFeatureEnabled(PreReleaseFeatures.WEBRTC_PREVIEW); + + if (featureEnabled) { + if (WebrtcStore.isBusy()) { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.BUSY, + from_user_id: UserStore.getCurrentId(), + to_user_id: userId + }); + this.stopRinging(); + } else if (userMedia) { + WebrtcStore.setVideoCallWith(userId); + this.setState({ + userCalling: UserStore.getProfile(userId) + }); + } else { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.UNSUPPORTED, + from_user_id: UserStore.getCurrentId(), + to_user_id: userId + }); + this.stopRinging(); + } + } else { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.DISABLED, + from_user_id: UserStore.getCurrentId(), + to_user_id: userId + }); + this.stopRinging(); + } + } + } + + onCancelCall(message) { + if (message && message.action !== WebrtcActionTypes.CANCEL) { + return; + } else if (message && message.action === WebrtcActionTypes.CANCEL && this.state.userCalling && message.from_user_id !== this.state.userCalling.id) { + return; + } + + WebrtcStore.setVideoCallWith(null); + this.closeNotification(); + } + + onRhs(rhsOpened) { + this.setState({rhsOpened}); + } + + handleTimeout() { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.NO_ANSWER, + from_user_id: UserStore.getCurrentId(), + to_user_id: this.state.userCalling.id + }); + + this.onCancelCall(); + } + + handleAnswer(e) { + if (e) { + e.preventDefault(); + } + + const caller = this.state.userCalling; + if (caller) { + const callerId = caller.id; + const currentUserId = UserStore.getCurrentId(); + const message = { + action: WebrtcActionTypes.ANSWER, + from_user_id: currentUserId, + to_user_id: callerId + }; + + GlobalActions.emitCloseRightHandSide(); + WebrtcActions.initWebrtc(callerId, false); + WebSocketClient.sendMessage('webrtc', message); + + // delay till next tick (this will give time to listen for events + setTimeout(() => { + //we switch from and to user to handle the event locally + message.from_user_id = callerId; + message.to_user_id = currentUserId; + WebrtcActions.handle(message); + }, 0); + + this.closeNotification(); + } + } + + handleClose(e) { + if (e) { + e.preventDefault(); + } + if (this.state.userCalling) { + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.DECLINE, + from_user_id: UserStore.getCurrentId(), + to_user_id: this.state.userCalling.id + }); + } + + this.onCancelCall(); + } + + render() { + const user = this.state.userCalling; + if (user) { + const username = Utils.displayUsername(user.id); + const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.update_at || new Date().getTime()); + const profileImg = ( + <img + className='user-popover__image' + src={profileImgSrc} + height='128' + width='128' + key='user-popover-image' + /> + ); + const answerBtn = ( + <svg + className='webrtc-icons__pickup' + xmlns='http://www.w3.org/2000/svg' + width='48' + height='48' + viewBox='-10 -10 68 68' + onClick={this.handleAnswer} + > + <circle + cx='24' + cy='24' + r='34' + > + <title> + <FormattedMessage + id='webrtc.notification.answer' + defaultMessage='Answer' + /> + </title> + </circle> + <path + transform='translate(-10,-10)' + fill='#fff' + d='M29.854,37.627c1.723,1.904 3.679,3.468 5.793,4.684l3.683,-3.334c0.469,-0.424 1.119,-0.517 1.669,-0.302c1.628,0.63 3.331,1.021 5.056,1.174c0.401,0.026 0.795,0.199 1.09,0.525c0.295,0.326 0.433,0.741 0.407,1.153l-0.279,5.593c-0.02,0.418 -0.199,0.817 -0.525,1.112c-0.326,0.296 -0.741,0.434 -1.159,0.413c-6.704,-0.504 -13.238,-3.491 -18.108,-8.87c-4.869,-5.38 -7.192,-12.179 -7.028,-18.899c0.015,-0.413 0.199,-0.817 0.526,-1.113c0.326,-0.295 0.74,-0.433 1.153,-0.407l5.593,0.279c0.407,0.02 0.812,0.193 1.107,0.519c0.29,0.32 0.428,0.735 0.413,1.137c-0.018,1.732 0.202,3.464 0.667,5.147c0.159,0.569 0.003,1.207 -0.466,1.631l-3.683,3.334c1.005,2.219 2.368,4.32 4.091,6.224Z' + /> + </svg> + ); + + const rejectBtn = ( + <svg + className='webrtc-icons__cancel' + xmlns='http://www.w3.org/2000/svg' + width='48' + height='48' + viewBox='-10 -10 68 68' + onClick={this.handleClose} + > + <circle + cx='24' + cy='24' + r='34' + > + <title> + <FormattedMessage + id='webrtc.notification.decline' + defaultMessage='Decline' + /> + </title> + </circle> + <path + transform='scale(0.7), translate(11,10)' + d='M24 18c-3.21 0-6.3.5-9.2 1.44v6.21c0 .79-.46 1.47-1.12 1.8-1.95.98-3.74 2.23-5.33 3.7-.36.35-.85.57-1.4.57-.55 0-1.05-.22-1.41-.59L.59 26.18c-.37-.37-.59-.87-.59-1.42 0-.55.22-1.05.59-1.42C6.68 17.55 14.93 14 24 14s17.32 3.55 23.41 9.34c.37.36.59.87.59 1.42 0 .55-.22 1.05-.59 1.41l-4.95 4.95c-.36.36-.86.59-1.41.59-.54 0-1.04-.22-1.4-.57-1.59-1.47-3.38-2.72-5.33-3.7-.66-.33-1.12-1.01-1.12-1.8v-6.21C30.3 18.5 27.21 18 24 18z' + fill='white' + /> + </svg> + ); + + const msg = ( + <div> + <FormattedMessage + id='webrtc.notification.incoming_call' + defaultMessage='{username} is calling you.' + values={{ + username + }} + /> + <div + className='webrtc-buttons webrtc-icons active' + style={{marginTop: '5px'}} + > + {answerBtn} + {rejectBtn} + </div> + </div> + ); + + return ( + <div className='webrtc-notification'> + <audio + ref='ring' + src={ring} + autoPlay={true} + /> + <div> + {profileImg} + </div> + {msg} + </div> + ); + } else if (this.state.rhsOpened && WebrtcStore.isBusy()) { + return ( + <div + className='webrtc__notification--rhs' + onClick={this.closeRightHandSide} + > + <i className='fa fa-phone'/> + <FormattedMessage + id='webrtc.notification.returnToCall' + defaultMessage='Return to ongoing call with {username}' + values={{ + username: Utils.displayUsername(WebrtcStore.getVideoCallWith()) + }} + /> + </div> + ); + } + + return <div/>; + } +} diff --git a/webapp/components/webrtc/components/webrtc_sidebar.jsx b/webapp/components/webrtc/components/webrtc_sidebar.jsx new file mode 100644 index 000000000..59c25890b --- /dev/null +++ b/webapp/components/webrtc/components/webrtc_sidebar.jsx @@ -0,0 +1,133 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; + +import WebrtcController from '../webrtc_controller.jsx'; +import UserStore from 'stores/user_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import React from 'react'; + +export default class SidebarRight extends React.Component { + constructor(props) { + super(props); + + this.plScrolledToBottom = true; + + this.onShrink = this.onShrink.bind(this); + this.toggleSize = this.toggleSize.bind(this); + this.onInitializeVideoCall = this.onInitializeVideoCall.bind(this); + + this.doStrangeThings = this.doStrangeThings.bind(this); + + this.state = { + expanded: false, + currentUser: UserStore.getCurrentUser(), + videoCallVisible: false, + isCaller: false, + videoCallWithUserId: null + }; + } + + componentDidMount() { + WebrtcStore.addInitListener(this.onInitializeVideoCall); + this.doStrangeThings(); + } + + componentWillUnmount() { + WebrtcStore.removeInitListener(this.onInitializeVideoCall); + } + + shouldComponentUpdate(nextProps, nextState) { + return !Utils.areObjectsEqual(nextState, this.state); + } + + doStrangeThings() { + // We should have a better way to do this stuff + // Hence the function name. + $('.app__body .inner-wrap').removeClass('move--right'); + $('.app__body .inner-wrap').addClass('webrtc--show'); + $('.app__body .sidebar--left').removeClass('move--right'); + $('.app__body .webrtc').addClass('webrtc--show'); + + //$('.sidebar--right').prepend('<div class="sidebar__overlay"></div>'); + if (!this.state.videoCallVisible) { + $('.app__body .inner-wrap').removeClass('webrtc--show').removeClass('move--right'); + $('.app__body .webrtc').removeClass('webrtc--show'); + return ( + <div></div> + ); + } + return null; + } + + componentDidUpdate() { + this.doStrangeThings(); + } + + onShrink() { + this.setState({expanded: false}); + } + + toggleSize(e) { + if (e) { + e.preventDefault(); + } + this.setState({expanded: !this.state.expanded}); + } + + onInitializeVideoCall(userId, isCaller) { + let expanded = this.state.expanded; + if (userId === null) { + expanded = false; + } + this.setState({ + videoCallVisible: (userId !== null), + isCaller, + videoCallWithUserId: userId, + expanded + }); + + if (userId !== null) { + this.forceUpdate(); + } + } + + render() { + let content = null; + let expandedClass = ''; + + if (this.state.expanded) { + expandedClass = 'sidebar--right--expanded'; + } + + if (this.state.videoCallVisible) { + content = ( + <WebrtcController + currentUser={this.state.currentUser} + userId={this.state.videoCallWithUserId} + isCaller={this.state.isCaller} + expanded={this.state.expanded} + toggleSize={this.toggleSize} + /> + ); + } + + return ( + <div + className={'sidebar--right webrtc ' + expandedClass} + id='sidebar-webrtc' + > + <div + onClick={this.onShrink} + className='sidebar--right__bg' + /> + <div className='sidebar-right-container'> + {content} + </div> + </div> + ); + } +} diff --git a/webapp/components/webrtc/webrtc_controller.jsx b/webapp/components/webrtc/webrtc_controller.jsx new file mode 100644 index 000000000..f9cf241d5 --- /dev/null +++ b/webapp/components/webrtc/webrtc_controller.jsx @@ -0,0 +1,1214 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import WebrtcStore from 'stores/webrtc_store.jsx'; + +import Client from 'client/web_client.jsx'; +import WebSocketClient from 'client/web_websocket_client.jsx'; +import WebrtcSession from 'client/webrtc_session.jsx'; + +import SearchBox from '../search_bar.jsx'; +import WebrtcHeader from './components/webrtc_header.jsx'; +import ConnectingScreen from 'components/loading_screen.jsx'; + +import * as WebrtcActions from 'actions/webrtc_actions.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import {Constants, UserStatuses, WebrtcActionTypes} from 'utils/constants.jsx'; + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; + +import ring from 'images/ring.mp3'; + +const VIDEO_WIDTH = 640; +const VIDEO_HEIGHT = 360; +const MIN_ASPECT = 1.777; +const MAX_ASPECT = 1.778; +const ALREADY_REGISTERED_ERROR = 477; +const USERNAME_TAKEN = 476; + +export default class WebrtcController extends React.Component { + constructor(props) { + super(props); + + this.mounted = false; + this.localMedia = null; + this.session = null; + this.videocall = null; + + this.handleResize = this.handleResize.bind(this); + this.handleClose = this.handleClose.bind(this); + this.close = this.close.bind(this); + this.clearError = this.clearError.bind(this); + + this.previewVideo = this.previewVideo.bind(this); + this.stopRinging = this.stopRinging.bind(this); + + this.handleMakeOffer = this.handleMakeOffer.bind(this); + this.handleCancelOffer = this.handleCancelOffer.bind(this); + this.handleWebrtcEvent = this.handleWebrtcEvent.bind(this); + this.handleVideoCallEvent = this.handleVideoCallEvent.bind(this); + this.handleRemoteStream = this.handleRemoteStream.bind(this); + + this.onStatusChange = this.onStatusChange.bind(this); + this.onCallDeclined = this.onCallDeclined.bind(this); + this.onUnsupported = this.onUnsupported.bind(this); + this.onNoAnswer = this.onNoAnswer.bind(this); + this.onBusy = this.onBusy.bind(this); + this.onDisabled = this.onDisabled.bind(this); + this.onFailed = this.onFailed.bind(this); + this.onCancelled = this.onCancelled.bind(this); + this.onConnectCall = this.onConnectCall.bind(this); + + this.onSessionCreated = this.onSessionCreated.bind(this); + this.onSessionError = this.onSessionError.bind(this); + + this.doCall = this.doCall.bind(this); + this.doAnswer = this.doAnswer.bind(this); + this.doHangup = this.doHangup.bind(this); + this.doCleanup = this.doCleanup.bind(this); + + this.renderButtons = this.renderButtons.bind(this); + this.onToggleVideo = this.onToggleVideo.bind(this); + this.onToggleAudio = this.onToggleAudio.bind(this); + this.onToggleRemoteMute = this.onToggleRemoteMute.bind(this); + this.toggleIcons = this.toggleIcons.bind(this); + + const currentUser = UserStore.getCurrentUser(); + const remoteUser = UserStore.getProfile(props.userId); + const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at; + + this.state = { + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight(), + channelId: ChannelStore.getCurrentId(), + currentUser, + currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.update_at, + remoteUserImage, + localMediaLoaded: false, + isPaused: false, + isMuted: false, + isRemotePaused: false, + isRemoteMuted: false, + isCalling: false, + isAnswering: false, + callInProgress: false, + error: null, + ended: null + }; + } + + componentDidMount() { + window.addEventListener('resize', this.handleResize); + WebrtcStore.addChangedListener(this.handleWebrtcEvent); + UserStore.addStatusesChangeListener(this.onStatusChange); + + this.mounted = true; + this.previewVideo(); + + if (this.props.isCaller) { + this.handleMakeOffer(); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.handleResize); + WebrtcStore.removeChangedListener(this.handleWebrtcEvent); + UserStore.removeStatusesChangeListener(this.onStatusChange); + this.mounted = false; + this.close(); + } + + componentWillReceiveProps(nextProps) { + if ((nextProps.currentUser !== this.props.currentUser) || + (nextProps.userId !== this.props.userId) || + (nextProps.isCaller !== this.props.isCaller)) { + const remoteUser = UserStore.getProfile(nextProps.userId); + const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at; + this.setState({ + error: null, + remoteUserImage + }); + } + + if (nextProps.isCaller && nextProps.expanded === this.props.expanded) { + this.startCall = true; + } + } + + componentDidUpdate() { + if (this.props.isCaller && this.startCall) { + this.startCall = false; + this.handleMakeOffer(); + } + } + + handleResize() { + this.setState({ + windowWidth: Utils.windowWidth(), + windowHeight: Utils.windowHeight() + }); + } + + clearError() { + setTimeout(() => { + this.setState({error: null, ended: null}); + }, Constants.WEBRTC_CLEAR_ERROR_DELAY); + } + + previewVideo() { + if (this.mounted) { + if (this.localMedia) { + this.setState({ + localMediaLoaded: true, + error: null + }); + this.localMedia.enabled = true; + } else { + WebrtcSession.getLocalMedia( + { + audio: true, + video: { + mandatory: { + minAspectRatio: MIN_ASPECT, + maxAspectRatio: MAX_ASPECT + }, + width: VIDEO_WIDTH, + height: VIDEO_HEIGHT + } + }, + this.refs['local-video'], + (error, stream) => { + if (error) { + this.setState({ + error: ( + <FormattedMessage + id='webrtc.mediaError' + defaultMessage='Unable to access Camera and Microphone' + /> + ) + }); + return; + } + this.localMedia = stream; + this.setState({ + localMediaLoaded: true + }); + }); + } + } + } + + stopRinging() { + if (this.refs.ring) { + this.refs.ring.pause(); + this.refs.ring.currentTime = 0; + } + } + + handleMakeOffer() { + if (UserStore.getStatus(this.props.userId) === UserStatuses.OFFLINE) { + this.onStatusChange(); + } else { + const connectingMsg = ( + <FormattedMessage + id='calling_screen' + defaultMessage='Calling' + /> + ); + + this.setState({ + isCalling: true, + isAnswering: false, + callInProgress: false, + error: null, + ended: null, + connectingMsg + }); + + WebrtcStore.setVideoCallWith(this.props.userId); + + const user = this.state.currentUser; + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.NOTIFY, + from_user_id: user.id, + to_user_id: this.props.userId + }); + } + } + + handleCancelOffer() { + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + error: null, + ended: null + }); + + const user = this.state.currentUser; + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.CANCEL, + from_user_id: user.id, + to_user_id: this.props.userId + }); + + this.doCleanup(); + } + + handleWebrtcEvent(message) { + switch (message.action) { + case WebrtcActionTypes.DECLINE: + this.onCallDeclined(); + this.clearError(); + break; + case WebrtcActionTypes.UNSUPPORTED: + this.onUnsupported(); + this.clearError(); + break; + case WebrtcActionTypes.BUSY: + this.onBusy(); + this.clearError(); + break; + case WebrtcActionTypes.NO_ANSWER: + this.onNoAnswer(); + this.clearError(); + break; + case WebrtcActionTypes.FAILED: + this.onFailed(); + this.clearError(); + break; + case WebrtcActionTypes.ANSWER: + this.onConnectCall(); + break; + case WebrtcActionTypes.CANCEL: + this.onCancelled(); + this.clearError(); + break; + case WebrtcActionTypes.MUTED: + this.onToggleRemoteMute(message); + break; + case WebrtcActionTypes.IN_PROGRESS: + this.setState({ + error: ( + <FormattedMessage + id='webrtc.inProgress' + defaultMessage='You have a call in progress. Please hangup first.' + /> + ) + }); + this.clearError(); + break; + case WebrtcActionTypes.DISABLED: + this.onDisabled(); + this.clearError(); + break; + } + } + + handleVideoCallEvent(msg, jsep) { + const result = msg.result; + + if (result) { + const event = result.event; + switch (event) { + case 'registered': + if (this.state.isCalling) { + this.doCall(); + } + break; + case 'incomingcall': + this.doAnswer(jsep); + break; + case 'accepted': + this.stopRinging(); + + if (jsep) { + this.videocall.handleRemoteJsep({jsep}); + } + break; + case 'hangup': + this.doHangup(false); + break; + } + } else { + const errorCode = msg.error_code; + if (errorCode !== ALREADY_REGISTERED_ERROR && errorCode !== USERNAME_TAKEN) { + this.doHangup(true); + } else if (this.state.isCalling) { + this.doCall(); + } + } + } + + handleRemoteStream(stream) { + // attaching stream to where they belong + this.refs['main-video'].srcObject = stream; + + let isRemotePaused = false; + let isRemoteMuted = false; + const videoTracks = stream.getVideoTracks(); + const audioTracks = stream.getAudioTracks(); + if (!videoTracks || videoTracks.length === 0 || videoTracks[0].muted || !videoTracks[0].enabled) { + isRemotePaused = true; + } + + if (!audioTracks || audioTracks.length === 0 || audioTracks[0].muted || !audioTracks[0].enabled) { + isRemoteMuted = true; + } + + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: true, + isMuted: false, + isPaused: false, + error: null, + ended: null, + isRemotePaused, + isRemoteMuted + }); + this.toggleIcons(); + } + + handleClose(e) { + e.preventDefault(); + if (this.state.callInProgress) { + this.setState({ + error: ( + <FormattedMessage + id='webrtc.inProgress' + defaultMessage='You have a call in progress. Please hangup first.' + /> + ) + }); + } else if (this.state.isCalling) { + this.handleCancelOffer(); + this.close(); + } else { + this.close(); + } + } + + close() { + this.doCleanup(); + + if (this.session) { + this.session.destroy(); + this.session.disconnect(); + this.session = null; + } + + if (this.localMedia) { + WebrtcSession.stopMediaStream(this.localMedia); + this.localMedia = null; + } + + WebrtcActions.initWebrtc(null, false); + } + + onStatusChange() { + const status = UserStore.getStatus(this.props.userId); + + if (status === UserStatuses.OFFLINE) { + const error = ( + <FormattedMessage + id='webrtc.offline' + defaultMessage='{username} is offline' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ); + + if (this.state.isCalling || this.state.isAnswering) { + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + error + }); + } else { + this.setState({ + error + }); + } + } else if (status !== UserStatuses.OFFLINE && this.state.error) { + this.setState({ + error: null, + ended: null + }); + } + } + + onCallDeclined() { + let error = null; + + if (this.state.isCalling) { + error = ( + <FormattedMessage + id='webrtc.declined' + defaultMessage='Your call has been declined by {username}' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ); + } + + this.stopRinging(); + + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + error + }); + + this.doCleanup(); + } + + onUnsupported() { + if (this.mounted) { + this.stopRinging(); + + this.setState({ + error: ( + <FormattedMessage + id='webrtc.unsupported' + defaultMessage='Call to {username} not successful. Their client does not support video calls.' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ), + callInProgress: false, + isCalling: false, + isAnswering: false + }); + } + + this.doCleanup(); + } + + onNoAnswer() { + let error = null; + + if (this.state.isCalling) { + error = ( + <FormattedMessage + id='webrtc.noAnswer' + defaultMessage='{username} is not answering the call' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ); + } + this.stopRinging(); + + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + error + }); + + this.doCleanup(); + } + + onBusy() { + let error = null; + + if (this.state.isCalling) { + error = ( + <FormattedMessage + id='webrtc.busy' + defaultMessage='{username} is busy' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ); + } + this.stopRinging(); + + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + error + }); + + this.doCleanup(); + } + + onDisabled() { + let error = null; + + if (this.state.isCalling) { + error = ( + <FormattedMessage + id='webrtc.disabled' + defaultMessage='{username} has WebRTC disabled, and cannot receive calls. To enable the feature, they must go to Account Settings > Advanced > Preview pre-release features and turn on WebRTC.' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ); + } + + this.stopRinging(); + + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + error + }); + + this.doCleanup(); + } + + onFailed() { + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + isPaused: false, + isMuted: false, + isRemotePaused: false, + isRemoteMuted: false, + error: ( + <FormattedMessage + id='webrtc.failed' + defaultMessage='There was a problem connecting the video call' + /> + ) + }); + + this.stopRinging(); + + this.doCleanup(); + } + + onCancelled() { + if (this.mounted && this.state.isAnswering) { + this.stopRinging(); + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + error: ( + <FormattedMessage + id='webrtc.cancelled' + defaultMessage='{username} cancelled the call' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ) + }); + } + + this.doCleanup(); + } + + onConnectCall() { + Client.webrtcToken( + (info) => { + const connectingMsg = ( + <FormattedMessage + id='connecting_screen' + defaultMessage='Connecting' + /> + ); + + this.setState({isAnswering: !this.state.isCalling, connectingMsg}); + if (this.session) { + this.onSessionCreated(); + } else { + const iceServers = []; + + if (info.stun_uri) { + iceServers.push({ + urls: [info.stun_uri] + }); + } + + if (info.turn_uri) { + iceServers.push({ + urls: [info.turn_uri], + username: info.turn_username, + credential: info.turn_password + }); + } + + this.session = new WebrtcSession({ + debug: global.mm_config.EnableDeveloper === 'true', + server: info.gateway_url, + iceServers, + token: info.token, + success: this.onSessionCreated, + error: this.onSessionError + }); + } + }, + () => { + this.onSessionError(); + }); + } + + onSessionCreated() { + if (this.videocall) { + this.doCall(); + } else { + this.session.attach({ + plugin: 'janus.plugin.videocall', + success: (plugin) => { + this.videocall = plugin; + this.videocall.send({message: {request: 'register', username: this.state.currentUser.id}}); + }, + error: this.onSessionError, + onmessage: this.handleVideoCallEvent, + onremotestream: this.handleRemoteStream + }); + } + } + + onSessionError() { + const user = this.state.currentUser; + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.FAILED, + from_user_id: user.id, + to_user_id: this.props.userId + }); + + this.onFailed(); + } + + doCall() { + // delay call so receiver has time to register + setTimeout(() => { + this.videocall.createOffer({ + stream: this.localMedia, + success: (jsep) => { + const body = {request: 'call', username: this.props.userId}; + this.videocall.send({message: body, jsep}); + }, + error: () => { + this.doHangup(true); + } + }); + }, Constants.WEBRTC_TIME_DELAY); + } + + doAnswer(jsep) { + this.videocall.createAnswer({ + jsep, + stream: this.localMedia, + success: (jsepSuccess) => { + const body = {request: 'accept'}; + this.videocall.send({message: body, jsep: jsepSuccess}); + }, + error: () => { + this.doHangup(true); + } + }); + } + + doHangup(error, manual) { + if (this.videocall && this.state.callInProgress) { + this.videocall.send({message: {request: 'hangup'}}); + this.videocall.hangup(); + this.toggleIcons(); + + this.localMedia.getVideoTracks()[0].enabled = true; + this.localMedia.getAudioTracks()[0].enabled = true; + } + + if (error) { + this.onSessionError(); + return this.doCleanup(); + } + WebrtcStore.setVideoCallWith(null); + + if (manual) { + return this.close(); + } + + this.setState({ + isCalling: false, + isAnswering: false, + callInProgress: false, + isPaused: false, + isMuted: false, + isRemotePaused: false, + isRemoteMuted: false, + error: null, + ended: ( + <FormattedMessage + id='webrtc.callEnded' + defaultMessage='Call with {username} ended.' + values={{ + username: Utils.displayUsername(this.props.userId) + }} + /> + ) + }); + this.clearError(); + return this.doCleanup(); + } + + doCleanup() { + WebrtcStore.setVideoCallWith(null); + + if (this.videocall) { + this.videocall.detach(); + this.videocall = null; + } + } + + onToggleVideo() { + const shouldPause = !this.state.isPaused; + if (shouldPause) { + this.videocall.unmuteVideo(); + } else { + this.videocall.muteVideo(); + } + + const user = this.state.currentUser; + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.MUTED, + from_user_id: user.id, + to_user_id: this.props.userId, + type: 'video', + mute: shouldPause + }); + + this.setState({ + isPaused: shouldPause, + error: null, + ended: null + }); + } + + onToggleAudio() { + const shouldMute = !this.state.isMuted; + if (shouldMute) { + this.videocall.unmuteAudio(); + } else { + this.videocall.muteAudio(); + } + + const user = this.state.currentUser; + WebSocketClient.sendMessage('webrtc', { + action: WebrtcActionTypes.MUTED, + from_user_id: user.id, + to_user_id: this.props.userId, + type: 'audio', + mute: shouldMute + }); + + this.setState({ + isMuted: shouldMute, + error: null, + ended: null + }); + } + + onToggleRemoteMute(message) { + if (message.type === 'video') { + this.setState({ + isRemotePaused: message.mute + }); + } else { + this.setState({isRemoteMuted: message.mute, error: null, ended: null}); + } + } + + toggleIcons() { + const icons = this.refs.icons; + if (icons) { + icons.classList.toggle('hidden'); + icons.classList.toggle('active'); + } + } + + renderButtons() { + let buttons; + if (this.state.isCalling) { + buttons = ( + <svg + id='cancel' + className='webrtc-icons__cancel' + xmlns='http://www.w3.org/2000/svg' + width='48' + height='48' + viewBox='-10 -10 68 68' + onClick={() => this.handleCancelOffer()} + > + <circle + cx='24' + cy='24' + r='34' + > + <title> + <FormattedMessage + id='webrtc.cancel' + defaultMessage='Cancel Call' + /> + </title> + </circle> + <path + transform='scale(0.8), translate(6,10)' + d='M24 18c-3.21 0-6.3.5-9.2 1.44v6.21c0 .79-.46 1.47-1.12 1.8-1.95.98-3.74 2.23-5.33 3.7-.36.35-.85.57-1.4.57-.55 0-1.05-.22-1.41-.59L.59 26.18c-.37-.37-.59-.87-.59-1.42 0-.55.22-1.05.59-1.42C6.68 17.55 14.93 14 24 14s17.32 3.55 23.41 9.34c.37.36.59.87.59 1.42 0 .55-.22 1.05-.59 1.41l-4.95 4.95c-.36.36-.86.59-1.41.59-.54 0-1.04-.22-1.4-.57-1.59-1.47-3.38-2.72-5.33-3.7-.66-.33-1.12-1.01-1.12-1.8v-6.21C30.3 18.5 27.21 18 24 18z' + fill='white' + /> + </svg> + ); + } else if (!this.state.callInProgress && this.state.localMediaLoaded) { + buttons = ( + <svg + id='call' + className='webrtc-icons__call' + xmlns='http://www.w3.org/2000/svg' + width='48' + height='48' + viewBox='-10 -10 68 68' + onClick={() => this.handleMakeOffer()} + disabled={UserStore.getStatus(this.props.userId) === 'offline'} + > + <circle + cx='24' + cy='24' + r='34' + > + <title> + <FormattedMessage + id='webrtc.call' + defaultMessage='Call' + /> + </title> + </circle> + <path + transform='translate(-10,-10)' + fill='#fff' + d='M29.854,37.627c1.723,1.904 3.679,3.468 5.793,4.684l3.683,-3.334c0.469,-0.424 1.119,-0.517 1.669,-0.302c1.628,0.63 3.331,1.021 5.056,1.174c0.401,0.026 0.795,0.199 1.09,0.525c0.295,0.326 0.433,0.741 0.407,1.153l-0.279,5.593c-0.02,0.418 -0.199,0.817 -0.525,1.112c-0.326,0.296 -0.741,0.434 -1.159,0.413c-6.704,-0.504 -13.238,-3.491 -18.108,-8.87c-4.869,-5.38 -7.192,-12.179 -7.028,-18.899c0.015,-0.413 0.199,-0.817 0.526,-1.113c0.326,-0.295 0.74,-0.433 1.153,-0.407l5.593,0.279c0.407,0.02 0.812,0.193 1.107,0.519c0.29,0.32 0.428,0.735 0.413,1.137c-0.018,1.732 0.202,3.464 0.667,5.147c0.159,0.569 0.003,1.207 -0.466,1.631l-3.683,3.334c1.005,2.219 2.368,4.32 4.091,6.224Z' + /> + </svg> + ); + } else if (this.state.callInProgress) { + const onClass = 'on'; + const offClass = 'off'; + let audioOnClass = offClass; + let audioOffClass = onClass; + let videoOnClass = offClass; + let videoOffClass = onClass; + + let audioTitle = ( + <FormattedMessage + id='webrtc.mute_audio' + defaultMessage='Mute' + /> + ); + + let videoTitle = ( + <FormattedMessage + id='webrtc.pause_video' + defaultMessage='Turn off Video' + /> + ); + + if (this.state.isMuted) { + audioOnClass = onClass; + audioOffClass = offClass; + audioTitle = ( + <FormattedMessage + id='webrtc.unmute_audio' + defaultMessage='Unmute' + /> + ); + } + + if (this.state.isPaused) { + videoOnClass = onClass; + videoOffClass = offClass; + videoTitle = ( + <FormattedMessage + id='webrtc.unpause_video' + defaultMessage='Turn on Video' + /> + ); + } + + buttons = ( + <div + ref='icons' + className='webrtc-icons hidden' + > + + <svg + id='mute-audio' + className='webrtc-icons__call' + xmlns='http://www.w3.org/2000/svg' + width='48' + height='48' + viewBox='-10 -10 68 68' + onClick={() => this.onToggleAudio()} + > + <circle + cx='24' + cy='24' + r='34' + > + <title>{audioTitle}</title> + </circle> + <path + className={audioOnClass} + transform='scale(0.6), translate(17,18)' + d='M38 22h-3.4c0 1.49-.31 2.87-.87 4.1l2.46 2.46C37.33 26.61 38 24.38 38 22zm-8.03.33c0-.11.03-.22.03-.33V10c0-3.32-2.69-6-6-6s-6 2.68-6 6v.37l11.97 11.96zM8.55 6L6 8.55l12.02 12.02v1.44c0 3.31 2.67 6 5.98 6 .45 0 .88-.06 1.3-.15l3.32 3.32c-1.43.66-3 1.03-4.62 1.03-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V42h4v-6.56c1.81-.27 3.53-.9 5.08-1.81L39.45 42 42 39.46 8.55 6z' + fill='white' + /> + <path + className={audioOffClass} + transform='scale(0.6), translate(17,18)' + d='M24 28c3.31 0 5.98-2.69 5.98-6L30 10c0-3.32-2.68-6-6-6-3.31 0-6 2.68-6 6v12c0 3.31 2.69 6 6 6zm10.6-6c0 6-5.07 10.2-10.6 10.2-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V42h4v-6.56c6.56-.97 12-6.61 12-13.44h-3.4z' + fill='white' + /> + </svg> + + <svg + id='mute-video' + className='webrtc-icons__call' + xmlns='http://www.w3.org/2000/svg' + width='48' + height='48' + viewBox='-10 -10 68 68' + onClick={() => this.onToggleVideo()} + > + <circle + cx='24' + cy='24' + r='34' + > + <title>{videoTitle}</title> + </circle> + <path + className={videoOnClass} + transform='scale(0.6), translate(17,16)' + d='M40 8H15.64l8 8H28v4.36l1.13 1.13L36 16v12.36l7.97 7.97L44 36V12c0-2.21-1.79-4-4-4zM4.55 2L2 4.55l4.01 4.01C4.81 9.24 4 10.52 4 12v24c0 2.21 1.79 4 4 4h29.45l4 4L44 41.46 4.55 2zM12 16h1.45L28 30.55V32H12V16z' + fill='white' + /> + <path + className={videoOffClass} + transform='scale(0.6), translate(17,16)' + d='M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm-4 24l-8-6.4V32H12V16h16v6.4l8-6.4v16z' + fill='white' + /> + </svg> + + <svg + id='hangup' + className='webrtc-icons__cancel' + xmlns='http://www.w3.org/2000/svg' + width='48' + height='48' + viewBox='-10 -10 68 68' + onClick={() => this.doHangup(false, true)} + > + <circle + cx='24' + cy='24' + r='34' + > + <title> + <FormattedMessage + id='webrtc.hangup' + defaultMessage='Hangup' + /> + </title> + </circle> + <path + transform='scale(0.7), translate(11,10)' + d='M24 18c-3.21 0-6.3.5-9.2 1.44v6.21c0 .79-.46 1.47-1.12 1.8-1.95.98-3.74 2.23-5.33 3.7-.36.35-.85.57-1.4.57-.55 0-1.05-.22-1.41-.59L.59 26.18c-.37-.37-.59-.87-.59-1.42 0-.55.22-1.05.59-1.42C6.68 17.55 14.93 14 24 14s17.32 3.55 23.41 9.34c.37.36.59.87.59 1.42 0 .55-.22 1.05-.59 1.41l-4.95 4.95c-.36.36-.86.59-1.41.59-.54 0-1.04-.22-1.4-.57-1.59-1.47-3.38-2.72-5.33-3.7-.66-.33-1.12-1.01-1.12-1.8v-6.21C30.3 18.5 27.21 18 24 18z' + fill='white' + /> + </svg> + + </div> + ); + } + + return buttons; + } + + render() { + const currentId = UserStore.getCurrentId(); + const remoteImage = (<img src={this.state.remoteUserImage}/>); + let localImage; + let localVideoHidden; + let remoteVideoHidden = 'hidden'; + let error; + let remoteMute; + let videoClass = ''; + let localImageHidden = 'webrtc__local-image hidden'; + let remoteImageHidden = 'webrtc__remote-image'; + + if (this.state.error) { + error = ( + <div className='webrtc__error'> + <div className='form-group has-error'> + <label className='control-label'>{this.state.error}</label> + </div> + </div> + ); + } else if (this.state.ended) { + error = ( + <div className='webrtc__error'> + <div className='form-group'> + <label className='control-label'>{this.state.ended}</label> + </div> + </div> + ); + } + + if (this.state.isRemoteMuted) { + remoteMute = ( + <div className='webrtc__remote-mute'> + <svg + xmlns='http://www.w3.org/2000/svg' + width='60' + height='60' + viewBox='-10 -10 68 68' + > + <path + className='off' + transform='scale(0.6), translate(17,18)' + d='M38 22h-3.4c0 1.49-.31 2.87-.87 4.1l2.46 2.46C37.33 26.61 38 24.38 38 22zm-8.03.33c0-.11.03-.22.03-.33V10c0-3.32-2.69-6-6-6s-6 2.68-6 6v.37l11.97 11.96zM8.55 6L6 8.55l12.02 12.02v1.44c0 3.31 2.67 6 5.98 6 .45 0 .88-.06 1.3-.15l3.32 3.32c-1.43.66-3 1.03-4.62 1.03-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V42h4v-6.56c1.81-.27 3.53-.9 5.08-1.81L39.45 42 42 39.46 8.55 6z' + fill='white' + /> + <path + className='on' + transform='scale(0.6), translate(17,18)' + d='M24 28c3.31 0 5.98-2.69 5.98-6L30 10c0-3.32-2.68-6-6-6-3.31 0-6 2.68-6 6v12c0 3.31 2.69 6 6 6zm10.6-6c0 6-5.07 10.2-10.6 10.2-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V42h4v-6.56c6.56-.97 12-6.61 12-13.44h-3.4z' + fill='white' + /> + </svg> + </div> + ); + } + + let searchForm; + if (currentId != null) { + searchForm = <SearchBox/>; + } + + const buttons = this.renderButtons(); + const calling = this.state.isCalling; + let connecting; + let audio; + if (calling || this.state.isAnswering) { + if (calling) { + audio = ( + <audio + ref='ring' + src={ring} + autoPlay={true} + /> + ); + } + + connecting = ( + <div className='connecting'> + <ConnectingScreen + position='absolute' + message={this.state.connectingMsg} + /> + {audio} + </div> + ); + } + + if (this.state.callInProgress) { + if (this.state.isPaused) { + localVideoHidden = 'hidden'; + localImageHidden = 'webrtc__local-image'; + localImage = (<img src={this.state.currentUserImage}/>); + } + + if (this.state.isRemotePaused) { + remoteVideoHidden = 'hidden'; + remoteImageHidden = 'webrtc__remote-image'; + } else { + remoteVideoHidden = ''; + remoteImageHidden = 'webrtc__remote-image hidden'; + } + } else { + videoClass = 'small'; + } + + return ( + <div className='post-right__container'> + <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div> + <div className='sidebar-right__body'> + <WebrtcHeader + username={Utils.displayUsername(this.props.userId)} + onClose={this.handleClose} + toggleSize={this.props.toggleSize} + /> + <div className='post-right__scroll'> + <div + id='videos' + className={videoClass} + > + {remoteMute} + <div + id='main-video' + className={remoteVideoHidden} + autoPlay={true} + > + <video + ref='main-video' + autoPlay={true} + /> + </div> + <div + id='local-video' + className={localVideoHidden} + > + <video + ref='local-video' + autoPlay={true} + muted={true} + /> + </div> + <div className={remoteImageHidden}> + {remoteImage} + </div> + <div className={localImageHidden}> + {localImage} + </div> + </div> + {error} + {connecting} + <div className='webrtc-buttons'> + {buttons} + </div> + </div> + </div> + </div> + ); + } +} + +WebrtcController.propTypes = { + currentUser: React.PropTypes.object, + userId: React.PropTypes.string.isRequired, + isCaller: React.PropTypes.bool.isRequired, + expanded: React.PropTypes.bool.isRequired, + toggleSize: React.PropTypes.function +};
\ No newline at end of file |