// Copyright (c) 2016-present 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 Janus from 'janus';
import SearchBox from '../search_bar.jsx';
import WebrtcHeader from './components/webrtc_header.jsx';
import ConnectingScreen from 'components/loading_screen.jsx';
import {trackEvent} from 'actions/diagnostics_actions.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 PropTypes from 'prop-types';
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.getLocalMedia = this.getLocalMedia.bind(this);
this.stopMediaStream = this.stopMediaStream.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.last_picture_update;
this.state = {
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight(),
channelId: ChannelStore.getCurrentId(),
currentUser,
currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.last_picture_update,
remoteUserImage,
localMediaLoaded: false,
isPaused: false,
isMuted: false,
isRemotePaused: false,
isRemoteMuted: false,
isCalling: false,
isAnswering: false,
callInProgress: false,
error: null,
errorType: '',
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.last_picture_update;
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, errorType: ''});
}, Constants.WEBRTC_CLEAR_ERROR_DELAY);
}
getLocalMedia(constraints, element, callback) {
const media = constraints || {audio: true, video: true};
navigator.mediaDevices.getUserMedia(media).
then((stream) => {
if (element) {
element.srcObject = stream;
}
if (callback && typeof callback === 'function') {
callback(null, stream);
}
}).
catch((error) => {
callback(error);
});
}
stopMediaStream(stream) {
const tracks = stream.getTracks();
tracks.forEach((track) => {
track.stop();
});
}
previewVideo() {
if (this.mounted) {
if (this.localMedia) {
this.setState({
localMediaLoaded: true,
error: null
});
this.localMedia.enabled = true;
} else {
this.getLocalMedia(
{
audio: true,
video: {
minAspectRatio: MIN_ASPECT,
maxAspectRatio: MAX_ASPECT,
width: VIDEO_WIDTH,
height: VIDEO_HEIGHT
}
},
this.refs['local-video'],
(error, stream) => {
if (error) {
this.setState({
error: (
)
});
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 = (
);
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: (
),
errorType: ' warning'
});
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: (
),
errorType: ' warning'
});
} else if (this.state.isCalling) {
this.handleCancelOffer();
this.close();
} else {
this.close();
}
}
close() {
this.doCleanup();
if (this.session) {
this.session.destroy();
this.session = null;
}
if (this.localMedia) {
this.stopMediaStream(this.localMedia);
this.localMedia = null;
}
WebrtcActions.initWebrtc(null, false);
}
onStatusChange() {
const status = UserStore.getStatus(this.props.userId);
if (status === UserStatuses.OFFLINE) {
const error = (
);
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 = (
);
}
this.stopRinging();
this.setState({
isCalling: false,
isAnswering: false,
callInProgress: false,
error
});
this.doCleanup();
}
onUnsupported() {
if (this.mounted) {
this.stopRinging();
this.setState({
error: (
),
callInProgress: false,
isCalling: false,
isAnswering: false
});
}
this.doCleanup();
}
onNoAnswer() {
let error = null;
if (this.state.isCalling) {
error = (
);
}
this.stopRinging();
this.setState({
isCalling: false,
isAnswering: false,
callInProgress: false,
error
});
this.doCleanup();
}
onBusy() {
let error = null;
if (this.state.isCalling) {
error = (
);
}
this.stopRinging();
this.setState({
isCalling: false,
isAnswering: false,
callInProgress: false,
error
});
this.doCleanup();
}
onDisabled() {
let error = null;
if (this.state.isCalling) {
error = (
);
}
this.stopRinging();
this.setState({
isCalling: false,
isAnswering: false,
callInProgress: false,
error
});
this.doCleanup();
}
onFailed() {
trackEvent('api', 'api_users_webrtc_failed');
this.setState({
isCalling: false,
isAnswering: false,
callInProgress: false,
isPaused: false,
isMuted: false,
isRemotePaused: false,
isRemoteMuted: false,
error: (
)
});
this.stopRinging();
this.doCleanup();
}
onCancelled() {
if (this.mounted && this.state.isAnswering) {
this.stopRinging();
this.setState({
isCalling: false,
isAnswering: false,
callInProgress: false,
error: (
)
});
}
this.doCleanup();
}
onConnectCall() {
WebrtcActions.webrtcToken(
(info) => {
const connectingMsg = (
);
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
});
}
Janus.init({debug: global.mm_config.EnableDeveloper === 'true'});
this.session = new Janus({
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) {
trackEvent('api', 'api_users_webrtc_start');
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) {
trackEvent('api', 'api_users_webrtc_end');
if (this.videocall && this.state.callInProgress) {
this.videocall.send({message: {request: 'hangup'}});
this.videocall.hangup();
this.toggleIcons();
if (this.localMedia) {
this.localMedia.getVideoTracks()[0].enabled = true;
this.localMedia.getAudioTracks()[0].enabled = true;
}
}
if (error) {
this.onSessionError();
return this.doCleanup();
}
WebrtcStore.setVideoCallWith(null);
WebrtcStore.emitRhsChanged(false);
if (manual) {
return this.close();
}
this.setState({
isCalling: false,
isAnswering: false,
callInProgress: false,
isPaused: false,
isMuted: false,
isRemotePaused: false,
isRemoteMuted: false,
error: null,
ended: (
)
});
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 = (
);
} else if (!this.state.callInProgress && this.state.localMediaLoaded) {
buttons = (
);
} 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 = (
);
let videoTitle = (
);
if (this.state.isMuted) {
audioOnClass = onClass;
audioOffClass = offClass;
audioTitle = (
);
}
if (this.state.isPaused) {
videoOnClass = onClass;
videoOffClass = offClass;
videoTitle = (
);
}
buttons = (
);
}
return buttons;
}
render() {
const currentId = UserStore.getCurrentId();
const remoteImage = ();
let localImage;
let localVideoHidden = '';
let remoteVideoHidden = 'hidden';
let error;
let remoteMute;
let localImageHidden = 'webrtc__local-image hidden';
let remoteImageHidden = 'webrtc__remote-image';
if (this.state.error) {
error = (
);
} else if (this.state.ended) {
error = (
);
}
if (this.state.isRemoteMuted) {
remoteMute = (
);
}
let searchForm;
if (currentId != null) {
searchForm = ;
}
const buttons = this.renderButtons();
const calling = this.state.isCalling;
let connecting;
let audio;
if (calling || this.state.isAnswering) {
if (calling) {
audio = (
);
}
connecting = (
{audio}
);
}
if (this.state.callInProgress) {
if (this.state.isPaused) {
localVideoHidden = 'hidden';
localImageHidden = 'webrtc__local-image';
localImage = ();
}
if (this.state.isRemotePaused) {
remoteVideoHidden = 'hidden';
remoteImageHidden = 'webrtc__remote-image';
} else {
remoteVideoHidden = '';
remoteImageHidden = 'webrtc__remote-image hidden';
}
}
return (
{searchForm}
{remoteMute}
{remoteImage}
{localImage}
{error}
{connecting}
{buttons}
);
}
}
WebrtcController.propTypes = {
currentUser: PropTypes.object,
userId: PropTypes.string.isRequired,
isCaller: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired,
toggleSize: PropTypes.func
};