v2
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
|
||||
import Logger from '../Logger';
|
||||
import muiTheme from './muiTheme';
|
||||
import Notifier from './Notifier';
|
||||
import Room from './Room';
|
||||
|
||||
const logger = new Logger('App'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class App extends React.Component
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<div data-component='App'>
|
||||
<Notifier ref='Notifier'/>
|
||||
|
||||
<Room
|
||||
peerId={props.peerId}
|
||||
roomId={props.roomId}
|
||||
onNotify={this.handleNotify.bind(this)}
|
||||
onHideNotification={this.handleHideNotification.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
handleNotify(data)
|
||||
{
|
||||
this.refs.Notifier.notify(data);
|
||||
}
|
||||
|
||||
handleHideNotification(uid)
|
||||
{
|
||||
this.refs.Notifier.hideNotification(uid);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes =
|
||||
{
|
||||
peerId : PropTypes.string.isRequired,
|
||||
roomId : PropTypes.string.isRequired
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { RIEInput } from 'riek';
|
||||
|
||||
export default class EditableInput extends React.Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
value,
|
||||
propName,
|
||||
className,
|
||||
classLoading,
|
||||
classInvalid,
|
||||
editProps,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<RIEInput
|
||||
value={value}
|
||||
propName={propName}
|
||||
className={className}
|
||||
classLoading={classLoading}
|
||||
classInvalid={classInvalid}
|
||||
shouldBlockWhileLoading
|
||||
editProps={editProps}
|
||||
change={(data) => onChange(data)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps)
|
||||
{
|
||||
if (nextProps.value === this.props.value)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
EditableInput.propTypes =
|
||||
{
|
||||
value : PropTypes.string,
|
||||
propName : PropTypes.string.isRequired,
|
||||
className : PropTypes.string,
|
||||
classLoading : PropTypes.string,
|
||||
classInvalid : PropTypes.string,
|
||||
editProps : PropTypes.any,
|
||||
onChange : PropTypes.func.isRequired
|
||||
};
|
||||
@@ -1,156 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import MicOffIcon from 'material-ui/svg-icons/av/mic-off';
|
||||
import VideoCamOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import ChangeVideoCamIcon from 'material-ui/svg-icons/av/repeat';
|
||||
import classnames from 'classnames';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('LocalVideo'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class LocalVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
micMuted : false,
|
||||
webcam : props.stream && !!props.stream.getVideoTracks()[0],
|
||||
togglingWebcam : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='LocalVideo'
|
||||
className={classnames(`state-${props.connectionState}`, {
|
||||
'active-speaker' : props.isActiveSpeaker
|
||||
})}
|
||||
>
|
||||
{props.stream ?
|
||||
<Video
|
||||
stream={props.stream}
|
||||
resolution={props.resolution}
|
||||
muted
|
||||
mirror={props.webcamType === 'front'}
|
||||
onResolutionChange={this.handleResolutionChange.bind(this)}
|
||||
/>
|
||||
:null}
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteMic.bind(this)}
|
||||
>
|
||||
<MicOffIcon
|
||||
color={!state.micMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={state.togglingWebcam}
|
||||
onClick={this.handleClickWebcam.bind(this)}
|
||||
>
|
||||
<VideoCamOffIcon
|
||||
color={state.webcam ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
{props.multipleWebcams ?
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={!state.webcam || state.togglingWebcam}
|
||||
onClick={this.handleClickChangeWebcam.bind(this)}
|
||||
>
|
||||
<ChangeVideoCamIcon
|
||||
color='#fff'
|
||||
/>
|
||||
</IconButton>
|
||||
:null}
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peerId}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
this.setState({ webcam: nextProps.stream && !!nextProps.stream.getVideoTracks()[0] });
|
||||
}
|
||||
|
||||
handleClickMuteMic()
|
||||
{
|
||||
logger.debug('handleClickMuteMic()');
|
||||
|
||||
let value = !this.state.micMuted;
|
||||
|
||||
this.props.onMicMute(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ micMuted: value });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickWebcam()
|
||||
{
|
||||
logger.debug('handleClickWebcam()');
|
||||
|
||||
let value = !this.state.webcam;
|
||||
|
||||
this.setState({ togglingWebcam: true });
|
||||
|
||||
this.props.onWebcamToggle(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ webcam: value, togglingWebcam: false });
|
||||
})
|
||||
.catch(() =>
|
||||
{
|
||||
this.setState({ togglingWebcam: false });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickChangeWebcam()
|
||||
{
|
||||
logger.debug('handleClickChangeWebcam()');
|
||||
|
||||
this.props.onWebcamChange();
|
||||
}
|
||||
|
||||
handleResolutionChange()
|
||||
{
|
||||
logger.debug('handleResolutionChange()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
}
|
||||
|
||||
LocalVideo.propTypes =
|
||||
{
|
||||
peerId : PropTypes.string.isRequired,
|
||||
stream : PropTypes.object,
|
||||
resolution : PropTypes.string,
|
||||
multipleWebcams : PropTypes.bool.isRequired,
|
||||
webcamType : PropTypes.string,
|
||||
connectionState : PropTypes.string,
|
||||
isActiveSpeaker : PropTypes.bool.isRequired,
|
||||
onMicMute : PropTypes.func.isRequired,
|
||||
onWebcamToggle : PropTypes.func.isRequired,
|
||||
onWebcamChange : PropTypes.func.isRequired,
|
||||
onResolutionChange : PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import PeerView from './PeerView';
|
||||
|
||||
class Me extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._mounted = false;
|
||||
this._rootNode = null;
|
||||
this._tooltip = true;
|
||||
|
||||
// TODO: Issue when using react-tooltip in Edge:
|
||||
// https://github.com/wwayne/react-tooltip/issues/328
|
||||
if (getDeviceInfo().flag === 'msedge')
|
||||
this._tooltip = false;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
connected,
|
||||
me,
|
||||
micProducer,
|
||||
webcamProducer,
|
||||
onChangeDisplayName,
|
||||
onMuteMic,
|
||||
onUnmuteMic,
|
||||
onEnableWebcam,
|
||||
onDisableWebcam,
|
||||
onChangeWebcam
|
||||
} = this.props;
|
||||
|
||||
let micState;
|
||||
|
||||
if (!me.canSendMic)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
|
||||
micState = 'on';
|
||||
else
|
||||
micState = 'off';
|
||||
|
||||
let webcamState;
|
||||
|
||||
if (!me.canSendWebcam)
|
||||
webcamState = 'unsupported';
|
||||
else if (webcamProducer)
|
||||
webcamState = 'on';
|
||||
else
|
||||
webcamState = 'off';
|
||||
|
||||
let changeWebcamState;
|
||||
|
||||
if (Boolean(webcamProducer) && me.canChangeWebcam)
|
||||
changeWebcamState = 'on';
|
||||
else
|
||||
changeWebcamState = 'unsupported';
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamProducer) &&
|
||||
!webcamProducer.locallyPaused &&
|
||||
!webcamProducer.remotelyPaused
|
||||
);
|
||||
|
||||
let tip;
|
||||
|
||||
if (!me.displayNameSet)
|
||||
tip = 'Click on your name to change it';
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='Me'
|
||||
ref={(node) => (this._rootNode = node)}
|
||||
data-tip={tip}
|
||||
data-tip-disable={!tip}
|
||||
data-type='dark'
|
||||
>
|
||||
{connected ?
|
||||
<div className='controls'>
|
||||
<div
|
||||
className={classnames('button', 'mic', micState)}
|
||||
onClick={() =>
|
||||
{
|
||||
micState === 'on' ? onMuteMic() : onUnmuteMic();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'webcam', webcamState, {
|
||||
disabled : me.webcamInProgress
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
webcamState === 'on' ? onDisableWebcam() : onEnableWebcam();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'change-webcam', changeWebcamState, {
|
||||
disabled : me.webcamInProgress
|
||||
})}
|
||||
onClick={() => onChangeWebcam()}
|
||||
/>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<PeerView
|
||||
isMe
|
||||
peer={me}
|
||||
audioTrack={micProducer ? micProducer.track : null}
|
||||
videoTrack={webcamProducer ? webcamProducer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
audioCodec={micProducer ? micProducer.codec : null}
|
||||
videoCodec={webcamProducer ? webcamProducer.codec : null}
|
||||
onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)}
|
||||
/>
|
||||
|
||||
{this._tooltip ?
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this._mounted = true;
|
||||
|
||||
if (this._tooltip)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (!this._mounted || this.props.me.displayNameSet)
|
||||
return;
|
||||
|
||||
ReactTooltip.show(this._rootNode);
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
if (this._tooltip)
|
||||
{
|
||||
if (nextProps.me.displayNameSet)
|
||||
ReactTooltip.hide(this._rootNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Me.propTypes =
|
||||
{
|
||||
connected : PropTypes.bool.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
micProducer : appPropTypes.Producer,
|
||||
webcamProducer : appPropTypes.Producer,
|
||||
onChangeDisplayName : PropTypes.func.isRequired,
|
||||
onMuteMic : PropTypes.func.isRequired,
|
||||
onUnmuteMic : PropTypes.func.isRequired,
|
||||
onEnableWebcam : PropTypes.func.isRequired,
|
||||
onDisableWebcam : PropTypes.func.isRequired,
|
||||
onChangeWebcam : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const producersArray = Object.values(state.producers);
|
||||
const micProducer =
|
||||
producersArray.find((producer) => producer.source === 'mic');
|
||||
const webcamProducer =
|
||||
producersArray.find((producer) => producer.source === 'webcam');
|
||||
|
||||
return {
|
||||
connected : state.room.state === 'connected',
|
||||
me : state.me,
|
||||
micProducer : micProducer,
|
||||
webcamProducer : webcamProducer
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onChangeDisplayName : (displayName) =>
|
||||
{
|
||||
dispatch(requestActions.changeDisplayName(displayName));
|
||||
},
|
||||
onMuteMic : () => dispatch(requestActions.muteMic()),
|
||||
onUnmuteMic : () => dispatch(requestActions.unmuteMic()),
|
||||
onEnableWebcam : () => dispatch(requestActions.enableWebcam()),
|
||||
onDisableWebcam : () => dispatch(requestActions.disableWebcam()),
|
||||
onChangeWebcam : () => dispatch(requestActions.changeWebcam())
|
||||
};
|
||||
};
|
||||
|
||||
const MeContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Me);
|
||||
|
||||
export default MeContainer;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as stateActions from '../redux/stateActions';
|
||||
import { Appear } from './transitions';
|
||||
|
||||
const Notifications = ({ notifications, onClick }) =>
|
||||
{
|
||||
return (
|
||||
<div data-component='Notifications'>
|
||||
{
|
||||
notifications.map((notification) =>
|
||||
{
|
||||
return (
|
||||
<Appear key={notification.id} duration={250}>
|
||||
<div
|
||||
className={classnames('notification', notification.type)}
|
||||
onClick={() => onClick(notification.id)}
|
||||
>
|
||||
<div className='icon' />
|
||||
<p className='text'>{notification.text}</p>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Notifications.propTypes =
|
||||
{
|
||||
notifications : PropTypes.arrayOf(appPropTypes.Notification).isRequired,
|
||||
onClick : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const { notifications } = state;
|
||||
|
||||
return { notifications };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onClick : (notificationId) =>
|
||||
{
|
||||
dispatch(stateActions.removeNotification(notificationId));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const NotificationsContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Notifications);
|
||||
|
||||
export default NotificationsContainer;
|
||||
@@ -1,151 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import NotificationSystem from 'react-notification-system';
|
||||
|
||||
const STYLE =
|
||||
{
|
||||
NotificationItem :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '6px 10px',
|
||||
backgroundColor : 'rgba(255,255,255, 0.9)',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 400,
|
||||
fontSize : '1rem',
|
||||
cursor : 'default',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none',
|
||||
userSelect : 'none',
|
||||
transition : '0.15s ease-in-out'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '2px solid rgba(255,0,78, 0.75)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid rgba(73,206,62, 0.75)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid #ff0014'
|
||||
}
|
||||
},
|
||||
Title :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
margin : '0 0 8px 0',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 500,
|
||||
fontSize : '1.1rem',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : 'rgba(255,0,78, 0.85)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : 'rgba(73,206,62, 0.9)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#ff0014'
|
||||
}
|
||||
},
|
||||
Dismiss :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
display : 'none'
|
||||
}
|
||||
},
|
||||
Action :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '8px 24px',
|
||||
fontSize : '1.2rem',
|
||||
cursor : 'pointer',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
backgroundColor : 'rgba(255,0,78, 1)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
backgroundColor : 'rgba(73,206,62, 0.75)'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class Notifier extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<NotificationSystem ref='NotificationSystem' style={STYLE} allowHTML={false}/>
|
||||
);
|
||||
}
|
||||
|
||||
notify(data)
|
||||
{
|
||||
let data2;
|
||||
|
||||
switch (data.level)
|
||||
{
|
||||
case 'info' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'success' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'error' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 3
|
||||
}, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`unknown level "${data.level}"`);
|
||||
}
|
||||
|
||||
this.refs.NotificationSystem.addNotification(data2);
|
||||
}
|
||||
|
||||
hideNotification(uid)
|
||||
{
|
||||
this.refs.NotificationSystem.removeNotification(uid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import PeerView from './PeerView';
|
||||
|
||||
const Peer = (props) =>
|
||||
{
|
||||
const {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer
|
||||
} = props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamConsumer) &&
|
||||
!webcamConsumer.locallyPaused &&
|
||||
!webcamConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
let videoProfile;
|
||||
|
||||
if (webcamConsumer)
|
||||
videoProfile = webcamConsumer.profile;
|
||||
|
||||
return (
|
||||
<div data-component='Peer'>
|
||||
<div className='indicators'>
|
||||
{!micEnabled ?
|
||||
<div className='icon mic-off' />
|
||||
:null
|
||||
}
|
||||
{!videoVisible ?
|
||||
<div className='icon webcam-off' />
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
|
||||
{videoVisible && !webcamConsumer.supported ?
|
||||
<div className='incompatible-video'>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<PeerView
|
||||
peer={peer}
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
videoProfile={videoProfile}
|
||||
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Peer.propTypes =
|
||||
{
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
const webcamConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
|
||||
return {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const PeerContainer = connect(mapStateToProps)(Peer);
|
||||
|
||||
export default PeerContainer;
|
||||
@@ -0,0 +1,260 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Spinner from 'react-spinner';
|
||||
import hark from 'hark';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import EditableInput from './EditableInput';
|
||||
|
||||
export default class PeerView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
volume : 0, // Integer from 0 to 10.,
|
||||
videoWidth : null,
|
||||
videoHeight : null
|
||||
};
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
||||
// Hark instance.
|
||||
// @type {Object}
|
||||
this._hark = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._videoResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
peer,
|
||||
videoVisible,
|
||||
videoProfile,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
onChangeDisplayName
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
volume,
|
||||
videoWidth,
|
||||
videoHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='PeerView'>
|
||||
<div className='info'>
|
||||
<div className={classnames('media', { 'is-me': isMe })}>
|
||||
<div className='box'>
|
||||
{audioCodec ?
|
||||
<p className='codec'>{audioCodec}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{videoCodec ?
|
||||
<p className='codec'>{videoCodec} {videoProfile}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{(videoVisible && videoWidth !== null) ?
|
||||
<p className='resolution'>{videoWidth}x{videoHeight}</p>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classnames('peer', { 'is-me': isMe })}>
|
||||
{isMe ?
|
||||
<EditableInput
|
||||
value={peer.displayName}
|
||||
propName='displayName'
|
||||
className='display-name editable'
|
||||
classLoading='loading'
|
||||
classInvalid='invalid'
|
||||
shouldBlockWhileLoading
|
||||
editProps={{
|
||||
maxLength : 20,
|
||||
autoCorrect : false,
|
||||
spellCheck : false
|
||||
}}
|
||||
onChange={({ displayName }) => onChangeDisplayName(displayName)}
|
||||
/>
|
||||
:
|
||||
<span className='display-name'>
|
||||
{peer.displayName}
|
||||
</span>
|
||||
}
|
||||
|
||||
<div className='row'>
|
||||
<span
|
||||
className={classnames('device-icon', peer.device.flag)}
|
||||
/>
|
||||
<span className='device-version'>
|
||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({
|
||||
hidden : !videoVisible,
|
||||
'is-me' : isMe,
|
||||
loading : videoProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
muted={isMe}
|
||||
/>
|
||||
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${volume}`)} />
|
||||
</div>
|
||||
|
||||
{videoProfile === 'none' ?
|
||||
<div className='spinner-container'>
|
||||
<Spinner />
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack, videoTrack } = this.props;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack, videoTrack } = nextProps;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
}
|
||||
|
||||
_setTracks(audioTrack, videoTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
this._videoTrack = videoTrack;
|
||||
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
this._hideVideoResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (audioTrack || videoTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
if (videoTrack)
|
||||
stream.addTrack(videoTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (audioTrack)
|
||||
this._runHark(stream);
|
||||
|
||||
if (videoTrack)
|
||||
this._showVideoResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_runHark(stream)
|
||||
{
|
||||
if (!stream.getAudioTracks()[0])
|
||||
throw new Error('_runHark() | given stream has no audio track');
|
||||
|
||||
this._hark = hark(stream, { play: false });
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
this._hark.on('volume_change', (dBs, threshold) =>
|
||||
{
|
||||
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
|
||||
// Math.pow(10, dBs / 20)
|
||||
// However it does not produce a visually useful output, so let exagerate
|
||||
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
|
||||
// minimize component renderings.
|
||||
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
|
||||
|
||||
if (volume === 1)
|
||||
volume = 0;
|
||||
|
||||
if (volume !== this.state.volume)
|
||||
this.setState({ volume: volume });
|
||||
});
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { videoWidth, videoHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
videoWidth : video.videoWidth,
|
||||
videoHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideVideoResolution()
|
||||
{
|
||||
this.setState({ videoWidth: null, videoHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
PeerView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
peer : PropTypes.oneOfType(
|
||||
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
||||
audioTrack : PropTypes.any,
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool.isRequired,
|
||||
videoProfile : PropTypes.string,
|
||||
audioCodec : PropTypes.string,
|
||||
videoCodec : PropTypes.string,
|
||||
onChangeDisplayName : PropTypes.func
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import { Appear } from './transitions';
|
||||
import Peer from './Peer';
|
||||
|
||||
const Peers = ({ peers, activeSpeakerName }) =>
|
||||
{
|
||||
return (
|
||||
<div data-component='Peers'>
|
||||
{
|
||||
peers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
<Appear key={peer.name} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'active-speaker' : peer.name === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<Peer name={peer.name} />
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Peers.propTypes =
|
||||
{
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
activeSpeakerName : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
// TODO: This is not OK since it's creating a new array every time, so triggering a
|
||||
// component rendering.
|
||||
const peersArray = Object.values(state.peers);
|
||||
|
||||
return {
|
||||
peers : peersArray,
|
||||
activeSpeakerName : state.room.activeSpeakerName
|
||||
};
|
||||
};
|
||||
|
||||
const PeersContainer = connect(mapStateToProps)(Peers);
|
||||
|
||||
export default PeersContainer;
|
||||
@@ -1,138 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import VolumeOffIcon from 'material-ui/svg-icons/av/volume-off';
|
||||
import VideoOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import classnames from 'classnames';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('RemoteVideo');
|
||||
|
||||
export default class RemoteVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
audioMuted : false
|
||||
};
|
||||
|
||||
let videoTrack = props.stream.getVideoTracks()[0];
|
||||
|
||||
if (videoTrack)
|
||||
{
|
||||
videoTrack.addEventListener('mute', () =>
|
||||
{
|
||||
logger.debug('video track "mute" event');
|
||||
});
|
||||
|
||||
videoTrack.addEventListener('unmute', () =>
|
||||
{
|
||||
logger.debug('video track "unmute" event');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let videoTrack = props.stream.getVideoTracks()[0];
|
||||
let videoEnabled = videoTrack && videoTrack.enabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='RemoteVideo'
|
||||
className={classnames({
|
||||
fullsize : !!props.fullsize,
|
||||
'active-speaker' : props.isActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Video
|
||||
stream={props.stream}
|
||||
muted={state.audioMuted}
|
||||
videoDisabled={!videoEnabled}
|
||||
/>
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteAudio.bind(this)}
|
||||
>
|
||||
<VolumeOffIcon
|
||||
color={!state.audioMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
{videoTrack ?
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickDisableVideo.bind(this)}
|
||||
>
|
||||
<VideoOffIcon
|
||||
color={videoEnabled ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
:null}
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peer.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleClickMuteAudio()
|
||||
{
|
||||
logger.debug('handleClickMuteAudio()');
|
||||
|
||||
let value = !this.state.audioMuted;
|
||||
|
||||
this.setState({ audioMuted: value });
|
||||
}
|
||||
|
||||
handleClickDisableVideo()
|
||||
{
|
||||
logger.debug('handleClickDisableVideo()');
|
||||
|
||||
let videoTrack = this.props.stream.getVideoTracks()[0];
|
||||
let videoEnabled = videoTrack && videoTrack.enabled;
|
||||
let stream = this.props.stream;
|
||||
let msid = stream.jitsiRemoteId || stream.id;
|
||||
|
||||
if (videoEnabled)
|
||||
{
|
||||
this.props.onDisableVideo(msid)
|
||||
.then(() =>
|
||||
{
|
||||
videoTrack.enabled = false;
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.props.onEnableVideo(msid)
|
||||
.then(() =>
|
||||
{
|
||||
videoTrack.enabled = true;
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteVideo.propTypes =
|
||||
{
|
||||
peer : PropTypes.object.isRequired,
|
||||
stream : PropTypes.object.isRequired,
|
||||
fullsize : PropTypes.bool,
|
||||
isActiveSpeaker : PropTypes.bool.isRequired,
|
||||
onDisableVideo : PropTypes.func.isRequired,
|
||||
onEnableVideo : PropTypes.func.isRequired
|
||||
};
|
||||
+135
-513
@@ -1,531 +1,153 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ClipboardButton from 'react-clipboard.js';
|
||||
import browser from 'bowser';
|
||||
import TransitionAppear from './TransitionAppear';
|
||||
import LocalVideo from './LocalVideo';
|
||||
import RemoteVideo from './RemoteVideo';
|
||||
import Stats from './Stats';
|
||||
import Logger from '../Logger';
|
||||
import * as utils from '../utils';
|
||||
import Client from '../Client';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import { Appear } from './transitions';
|
||||
import Me from './Me';
|
||||
import Peers from './Peers';
|
||||
import Notifications from './Notifications';
|
||||
|
||||
const logger = new Logger('Room');
|
||||
const STATS_INTERVAL = 1000;
|
||||
|
||||
export default class Room extends React.Component
|
||||
const Room = (
|
||||
{
|
||||
room,
|
||||
me,
|
||||
amActiveSpeaker,
|
||||
onRoomLinkCopy,
|
||||
onSetAudioMode,
|
||||
onRestartIce
|
||||
}) =>
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
return (
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<Notifications />
|
||||
|
||||
this.state =
|
||||
{
|
||||
peers : {},
|
||||
localStream : null,
|
||||
localVideoResolution : null, // qvga / vga / hd / fullhd.
|
||||
multipleWebcams : false,
|
||||
webcamType : null,
|
||||
connectionState : null,
|
||||
remoteStreams : {},
|
||||
showStats : false,
|
||||
stats : null,
|
||||
activeSpeakerId : null
|
||||
};
|
||||
<div className='state'>
|
||||
<div className={classnames('icon', room.state)} />
|
||||
<p className={classnames('text', room.state)}>{room.state}</p>
|
||||
</div>
|
||||
|
||||
// Mounted flag
|
||||
this._mounted = false;
|
||||
// Client instance
|
||||
this._client = null;
|
||||
// Timer to retrieve RTC stats.
|
||||
this._statsTimer = null;
|
||||
|
||||
// TODO: TMP
|
||||
global.ROOM = this;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let numPeers = Object.keys(state.remoteStreams).length;
|
||||
|
||||
return (
|
||||
<TransitionAppear duration={2000}>
|
||||
<div data-component='Room'>
|
||||
|
||||
<div className='room-link-wrapper'>
|
||||
<div className='room-link'>
|
||||
<ClipboardButton
|
||||
component='a'
|
||||
className='link'
|
||||
button-href={window.location.href}
|
||||
data-clipboard-text={window.location.href}
|
||||
onSuccess={this.handleRoomLinkCopied.bind(this)}
|
||||
onClick={() => {}} // Avoid link action.
|
||||
>
|
||||
invite people to this room
|
||||
</ClipboardButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='remote-videos'>
|
||||
{
|
||||
Object.keys(state.remoteStreams).map((msid) =>
|
||||
<div className='room-link-wrapper'>
|
||||
<div className='room-link'>
|
||||
<ClipboardButton
|
||||
component='a'
|
||||
className='link'
|
||||
button-href={room.url}
|
||||
button-target='_blank'
|
||||
data-clipboard-text={room.url}
|
||||
onSuccess={onRoomLinkCopy}
|
||||
onClick={(event) =>
|
||||
{
|
||||
let stream = state.remoteStreams[msid];
|
||||
let peer;
|
||||
|
||||
for (let peerId of Object.keys(state.peers))
|
||||
// If this is a 'Open in new window/tab' don't prevent
|
||||
// click default action.
|
||||
if (
|
||||
event.ctrlKey || event.shiftKey || event.metaKey ||
|
||||
// Middle click (IE > 9 and everyone else).
|
||||
(event.button && event.button === 1)
|
||||
)
|
||||
{
|
||||
peer = state.peers[peerId];
|
||||
|
||||
if (peer.msids.indexOf(msid) !== -1)
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!peer)
|
||||
return;
|
||||
|
||||
return (
|
||||
<TransitionAppear key={msid} duration={500}>
|
||||
<RemoteVideo
|
||||
peer={peer}
|
||||
stream={stream}
|
||||
fullsize={numPeers === 1}
|
||||
isActiveSpeaker={peer.id === state.activeSpeakerId}
|
||||
onDisableVideo={this.handleDisableRemoteVideo.bind(this)}
|
||||
onEnableVideo={this.handleEnableRemoteVideo.bind(this)}
|
||||
/>
|
||||
</TransitionAppear>
|
||||
);
|
||||
})
|
||||
}
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
invitation link
|
||||
</ClipboardButton>
|
||||
</div>
|
||||
|
||||
<TransitionAppear duration={500}>
|
||||
<div className='local-video'>
|
||||
<LocalVideo
|
||||
peerId={props.peerId}
|
||||
stream={state.localStream}
|
||||
resolution={state.localVideoResolution}
|
||||
multipleWebcams={state.multipleWebcams}
|
||||
webcamType={state.webcamType}
|
||||
connectionState={state.connectionState}
|
||||
isActiveSpeaker={props.peerId === state.activeSpeakerId}
|
||||
onMicMute={this.handleLocalMute.bind(this)}
|
||||
onWebcamToggle={this.handleLocalWebcamToggle.bind(this)}
|
||||
onWebcamChange={this.handleLocalWebcamChange.bind(this)}
|
||||
onResolutionChange={this.handleLocalResolutionChange.bind(this)}
|
||||
/>
|
||||
|
||||
{state.showStats ?
|
||||
<TransitionAppear duration={500}>
|
||||
<Stats
|
||||
stats={state.stats || new Map()}
|
||||
onClose={this.handleStatsClose.bind(this)}
|
||||
/>
|
||||
</TransitionAppear>
|
||||
:
|
||||
<div
|
||||
className='show-stats'
|
||||
onClick={this.handleClickShowStats.bind(this)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
// Set flag
|
||||
this._mounted = true;
|
||||
|
||||
// Run the client
|
||||
this._runClient();
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
// Unset flag
|
||||
this._mounted = false;
|
||||
|
||||
// Close client
|
||||
this._client.removeAllListeners();
|
||||
this._client.close();
|
||||
|
||||
// Close local MediaStream
|
||||
if (state.localStream)
|
||||
utils.closeMediaStream(state.localStream);
|
||||
}
|
||||
|
||||
handleRoomLinkCopied()
|
||||
{
|
||||
logger.debug('handleRoomLinkCopied()');
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
position : 'tr',
|
||||
title : 'Room URL copied to the clipboard',
|
||||
message : 'Share it with others to join this room'
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalMute(value)
|
||||
{
|
||||
logger.debug('handleLocalMute() [value:%s]', value);
|
||||
|
||||
let micTrack = this.state.localStream.getAudioTracks()[0];
|
||||
|
||||
if (!micTrack)
|
||||
return Promise.reject(new Error('no audio track'));
|
||||
|
||||
micTrack.enabled = !value;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
handleLocalWebcamToggle(value)
|
||||
{
|
||||
logger.debug('handleLocalWebcamToggle() [value:%s]', value);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
if (value)
|
||||
return this._client.addVideo();
|
||||
else
|
||||
return this._client.removeVideo();
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let localStream = this.state.localStream;
|
||||
|
||||
this.setState({ localStream });
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalWebcamChange()
|
||||
{
|
||||
logger.debug('handleLocalWebcamChange()');
|
||||
|
||||
this._client.changeWebcam();
|
||||
}
|
||||
|
||||
handleLocalResolutionChange()
|
||||
{
|
||||
logger.debug('handleLocalResolutionChange()');
|
||||
|
||||
if (!utils.canChangeResolution())
|
||||
{
|
||||
logger.warn('changing local resolution not implemented for this browser');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._client.changeVideoResolution();
|
||||
}
|
||||
|
||||
handleStatsClose()
|
||||
{
|
||||
logger.debug('handleStatsClose()');
|
||||
|
||||
this.setState({ showStats: false });
|
||||
this._stopStats();
|
||||
}
|
||||
|
||||
handleClickShowStats()
|
||||
{
|
||||
logger.debug('handleClickShowStats()');
|
||||
|
||||
this.setState({ showStats: true });
|
||||
this._startStats();
|
||||
}
|
||||
|
||||
handleDisableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleDisableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
return this._client.disableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
handleEnableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleEnableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
return this._client.enableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
_runClient()
|
||||
{
|
||||
let peerId = this.props.peerId;
|
||||
let roomId = this.props.roomId;
|
||||
|
||||
logger.debug('_runClient() [peerId:"%s", roomId:"%s"]', peerId, roomId);
|
||||
|
||||
this._client = new Client(peerId, roomId);
|
||||
|
||||
this._client.on('localstream', (stream, resolution) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
localStream : stream,
|
||||
localVideoResolution : resolution
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('join', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
title : 'Yes!',
|
||||
message : 'You are in the room!',
|
||||
image : '/resources/images/room.svg',
|
||||
imageWidth : 80,
|
||||
imageHeight : 80
|
||||
});
|
||||
|
||||
// Start retrieving WebRTC stats (unless mobile or Edge).
|
||||
if (utils.isDesktop() && !browser.msedge)
|
||||
{
|
||||
this.setState({ showStats: true });
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
this._startStats();
|
||||
}, STATS_INTERVAL / 2);
|
||||
}
|
||||
});
|
||||
|
||||
this._client.on('close', (error) =>
|
||||
{
|
||||
// Clear remote streams (for reconnections) and more stuff.
|
||||
this.setState(
|
||||
{
|
||||
remoteStreams : {},
|
||||
activeSpeakerId : null
|
||||
});
|
||||
|
||||
if (error)
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Error',
|
||||
message : error.message
|
||||
});
|
||||
}
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('disconnected', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Warning',
|
||||
message : 'app disconnected'
|
||||
});
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('numwebcams', (num) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
multipleWebcams : (num > 1 ? true : false)
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('webcamtype', (type) =>
|
||||
{
|
||||
this.setState({ webcamType: type });
|
||||
});
|
||||
|
||||
this._client.on('peers', (peers) =>
|
||||
{
|
||||
let peersObject = {};
|
||||
|
||||
for (let peer of peers)
|
||||
{
|
||||
peersObject[peer.id] = peer;
|
||||
}
|
||||
|
||||
this.setState({ peers: peersObject });
|
||||
});
|
||||
|
||||
this._client.on('addpeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
message : `${peer.id} joined the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('updatepeer', (peer) =>
|
||||
{
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('removepeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'info',
|
||||
message : `${peer.id} left the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
peer = peers[peer.id];
|
||||
if (!peer)
|
||||
return;
|
||||
|
||||
delete peers[peer.id];
|
||||
|
||||
// NOTE: This shouldn't be needed but Safari 11 does not fire pc "removestream"
|
||||
// nor stream "removetrack" nor track "ended", so we need to cleanup remote
|
||||
// streams when a peer leaves.
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
for (let msid of peer.msids)
|
||||
{
|
||||
delete remoteStreams[msid];
|
||||
}
|
||||
|
||||
this.setState({ peers, remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('connectionstate', (state) =>
|
||||
{
|
||||
this.setState({ connectionState: state });
|
||||
});
|
||||
|
||||
this._client.on('addstream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
let streamId = stream.jitsiRemoteId || stream.id;
|
||||
|
||||
remoteStreams[streamId] = stream;
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removestream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
let streamId = stream.jitsiRemoteId || stream.id;
|
||||
|
||||
delete remoteStreams[streamId];
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('addtrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removetrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('forcestreamsupdate', () =>
|
||||
{
|
||||
// Just firef for Firefox due to bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
this._client.on('activespeaker', (peer) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
activeSpeakerId : (peer ? peer.id : null)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_startStats()
|
||||
{
|
||||
logger.debug('_startStats()');
|
||||
|
||||
getStats.call(this);
|
||||
|
||||
function getStats()
|
||||
{
|
||||
this._client.getStats()
|
||||
.then((stats) =>
|
||||
{
|
||||
if (!this._mounted)
|
||||
return;
|
||||
|
||||
this.setState({ stats });
|
||||
|
||||
this._statsTimer = setTimeout(() =>
|
||||
{
|
||||
getStats.call(this);
|
||||
}, STATS_INTERVAL);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('getStats() failed: %o', error);
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
// this._statsTimer = setTimeout(() =>
|
||||
// {
|
||||
// getStats.call(this);
|
||||
// }, STATS_INTERVAL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_stopStats()
|
||||
{
|
||||
logger.debug('_stopStats()');
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
clearTimeout(this._statsTimer);
|
||||
}
|
||||
}
|
||||
<Peers />
|
||||
|
||||
<div
|
||||
className={classnames('me-container', {
|
||||
'active-speaker' : amActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Me />
|
||||
</div>
|
||||
|
||||
<div className='sidebar'>
|
||||
<div
|
||||
className={classnames('button', 'audio-only', {
|
||||
on : me.audioOnly,
|
||||
disabled : me.audioOnlyInProgress
|
||||
})}
|
||||
data-tip='Toggle audio only mode'
|
||||
data-type='dark'
|
||||
onClick={() => onSetAudioMode(!me.audioOnly)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'restart-ice', {
|
||||
disabled : me.restartIceInProgress
|
||||
})}
|
||||
data-tip='Restart ICE'
|
||||
data-type='dark'
|
||||
onClick={() => onRestartIce()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
};
|
||||
|
||||
Room.propTypes =
|
||||
{
|
||||
peerId : PropTypes.string.isRequired,
|
||||
roomId : PropTypes.string.isRequired,
|
||||
onNotify : PropTypes.func.isRequired,
|
||||
onHideNotification : PropTypes.func.isRequired
|
||||
room : appPropTypes.Room.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
amActiveSpeaker : PropTypes.bool.isRequired,
|
||||
onRoomLinkCopy : PropTypes.func.isRequired,
|
||||
onSetAudioMode : PropTypes.func.isRequired,
|
||||
onRestartIce : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
room : state.room,
|
||||
me : state.me,
|
||||
amActiveSpeaker : state.me.name === state.room.activeSpeakerName
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onRoomLinkCopy : () =>
|
||||
{
|
||||
dispatch(requestActions.notify(
|
||||
{
|
||||
text : 'Room link copied to the clipboard'
|
||||
}));
|
||||
},
|
||||
onSetAudioMode : (enable) =>
|
||||
{
|
||||
if (enable)
|
||||
dispatch(requestActions.enableAudioOnly());
|
||||
else
|
||||
dispatch(requestActions.disableAudioOnly());
|
||||
},
|
||||
onRestartIce : () =>
|
||||
{
|
||||
dispatch(requestActions.restartIce());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const RoomContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Room);
|
||||
|
||||
export default RoomContainer;
|
||||
|
||||
@@ -1,585 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import browser from 'bowser';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('Stats'); // eslint-disable-line no-unused-vars
|
||||
|
||||
// TODO: TMP
|
||||
global.BROWSER = browser;
|
||||
|
||||
export default class Stats extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport : null,
|
||||
audio : null,
|
||||
video : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount()
|
||||
{
|
||||
let stats = this.props.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stats = nextProps.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Stats'>
|
||||
<div
|
||||
className='close'
|
||||
onClick={this.handleCloseClick.bind(this)}
|
||||
/>
|
||||
{
|
||||
Object.keys(state.stats).map((blockName) =>
|
||||
{
|
||||
let block = state.stats[blockName];
|
||||
|
||||
if (!block)
|
||||
return;
|
||||
|
||||
let items = Object.keys(block).map((itemName) =>
|
||||
{
|
||||
let value = block[itemName];
|
||||
|
||||
if (value === undefined)
|
||||
return;
|
||||
|
||||
return (
|
||||
<div key={itemName} className='item'>
|
||||
<div className='key'>{itemName}</div>
|
||||
<div className='value'>{value}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (!items.length)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div key={blockName} className='block'>
|
||||
<h1>{blockName}</h1>
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleCloseClick()
|
||||
{
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
_processStats(stats)
|
||||
{
|
||||
// global.STATS = stats; // TODO: REMOVE
|
||||
|
||||
if (browser.check({ chrome: '58' }, true))
|
||||
{
|
||||
this._processStatsChrome58(stats);
|
||||
}
|
||||
else if (browser.check({ chrome: '40' }, true))
|
||||
{
|
||||
this._processStatsChromeOld(stats);
|
||||
}
|
||||
else if (browser.check({ firefox: '40' }, true))
|
||||
{
|
||||
this._processStatsFirefox(stats);
|
||||
}
|
||||
else if (browser.check({ safari: '11' }, true))
|
||||
{
|
||||
this._processStatsSafari11(stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn('_processStats() | unsupported browser [name:"%s", version:%s]',
|
||||
browser.name, browser.version);
|
||||
}
|
||||
}
|
||||
|
||||
_processStatsChrome58(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'transport':
|
||||
{
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.writable)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.availableOutgoingBitrate / 1000) + ' kbps';
|
||||
transport['current RTT'] =
|
||||
Math.round(group.currentRoundTripTime * 1000) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'codec':
|
||||
{
|
||||
let mimeType = group.mimeType.split('/');
|
||||
let kind = mimeType[0];
|
||||
let codec = mimeType[1];
|
||||
let block;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
if (codec === 'rtx')
|
||||
break;
|
||||
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = codec;
|
||||
block['payload type'] = group.payloadType;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'track':
|
||||
{
|
||||
if (group.kind !== 'video')
|
||||
break;
|
||||
|
||||
video['frame size'] = group.frameWidth + ' x ' + group.frameHeight;
|
||||
video['frames sent'] = group.framesSent;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.protocol;
|
||||
transport['local IP'] = localCandidate.ip;
|
||||
transport['local port'] = localCandidate.port;
|
||||
transport['remote IP'] = remoteCandidate.ip;
|
||||
transport['remote port'] = remoteCandidate.port;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsChromeOld(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'googCandidatePair':
|
||||
{
|
||||
if (group.googActiveConnection !== 'true')
|
||||
break;
|
||||
|
||||
let localAddress = group.googLocalAddress.split(':');
|
||||
let remoteAddress = group.googRemoteAddress.split(':');
|
||||
let localIP = localAddress[0];
|
||||
let localPort = localAddress[1];
|
||||
let remoteIP = remoteAddress[0];
|
||||
let remotePort = remoteAddress[1];
|
||||
|
||||
transport['protocol'] = group.googTransportType;
|
||||
transport['local IP'] = localIP;
|
||||
transport['local port'] = localPort;
|
||||
transport['remote IP'] = remoteIP;
|
||||
transport['remote port'] = remotePort;
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
transport['RTT'] = Math.round(group.googRtt) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'VideoBwe':
|
||||
{
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.googAvailableSendBandwidth / 1000) + ' kbps';
|
||||
transport['transmit bitrate'] =
|
||||
Math.round(group.googTransmitBitrate / 1000) + ' kbps';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ssrc':
|
||||
{
|
||||
if (group.packetsSent === undefined)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = group.googCodecName;
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
block['packets lost'] = group.packetsLost;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame size'] =
|
||||
group.googFrameWidthSent + ' x ' + group.googFrameHeightSent;
|
||||
video['frame rate'] = group.googFrameRateSent;
|
||||
}
|
||||
|
||||
block['NACK count'] = group.googNacksReceived;
|
||||
block['PLI count'] = group.googPlisReceived;
|
||||
block['FIR count'] = group.googFirsReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsFirefox(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.selected)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['bitrate'] =
|
||||
Math.round(group.bitrateMean / 1000) + ' kbps';
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame rate'] = Math.round(group.framerateMean);
|
||||
}
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.transport;
|
||||
transport['local IP'] = localCandidate.ipAddress;
|
||||
transport['local port'] = localCandidate.portNumber;
|
||||
transport['remote IP'] = remoteCandidate.ipAddress;
|
||||
transport['remote port'] = remoteCandidate.portNumber;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsSafari11(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.writable)
|
||||
break;
|
||||
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.availableOutgoingBitrate / 1000) + ' kbps';
|
||||
transport['current RTT'] =
|
||||
Math.round(group.currentRoundTripTime * 1000) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Stats.propTypes =
|
||||
{
|
||||
stats : PropTypes.object.isRequired,
|
||||
onClose : PropTypes.func.isRequired
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TransitionGroup from 'react-transition-group/TransitionGroup';
|
||||
|
||||
const DEFAULT_DURATION = 1000;
|
||||
|
||||
export default class TransitionAppear extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let duration = props.hasOwnProperty('duration') ? props.duration : DEFAULT_DURATION;
|
||||
|
||||
return (
|
||||
<TransitionGroup
|
||||
component={FakeTransitionWrapper}
|
||||
transitionName='transition'
|
||||
transitionAppear={!!duration}
|
||||
transitionAppearTimeout={duration}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
{this.props.children}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TransitionAppear.propTypes =
|
||||
{
|
||||
children : PropTypes.any,
|
||||
duration : PropTypes.number
|
||||
};
|
||||
|
||||
class FakeTransitionWrapper extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let children = React.Children.toArray(this.props.children);
|
||||
|
||||
return children[0] || null;
|
||||
}
|
||||
}
|
||||
|
||||
FakeTransitionWrapper.propTypes =
|
||||
{
|
||||
children : PropTypes.any
|
||||
};
|
||||
@@ -1,241 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Logger from '../Logger';
|
||||
import classnames from 'classnames';
|
||||
import hark from 'hark';
|
||||
|
||||
const logger = new Logger('Video'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class Video extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
width : 0,
|
||||
height : 0,
|
||||
resolution : null,
|
||||
volume : 0 // Integer from 0 to 10.
|
||||
};
|
||||
|
||||
let stream = props.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// Current MediaStreamTracks info.
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
// Periodic timer to show video dimensions.
|
||||
this._videoResolutionTimer = null;
|
||||
|
||||
// Hark instance.
|
||||
this._hark = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Video'>
|
||||
{state.width ?
|
||||
(
|
||||
<div
|
||||
className={classnames('resolution', { clickable: !!props.resolution })}
|
||||
onClick={this.handleResolutionClick.bind(this)}
|
||||
>
|
||||
<p>{state.width}x{state.height}</p>
|
||||
{props.resolution ?
|
||||
<p>{props.resolution}</p>
|
||||
:null}
|
||||
</div>
|
||||
)
|
||||
:null}
|
||||
<div className='volume'>
|
||||
<div className={classnames('bar', `level${state.volume}`)}/>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames(
|
||||
{
|
||||
mirror : props.mirror,
|
||||
hidden : props.videoDisabled
|
||||
})}
|
||||
autoPlay
|
||||
muted={props.muted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
let stream = this.props.stream;
|
||||
let video = this.refs.video;
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
this._showVideoResolution();
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
this._showVideoResolution();
|
||||
}, 500);
|
||||
|
||||
if (stream.getAudioTracks().length > 0)
|
||||
{
|
||||
this._hark = hark(stream);
|
||||
|
||||
this._hark.on('speaking', () =>
|
||||
{
|
||||
// logger.debug('hark "speaking" event');
|
||||
});
|
||||
|
||||
this._hark.on('stopped_speaking', () =>
|
||||
{
|
||||
// logger.debug('hark "stopped_speaking" event');
|
||||
|
||||
this.setState({ volume: 0 });
|
||||
});
|
||||
|
||||
this._hark.on('volume_change', (volume, threshold) =>
|
||||
{
|
||||
if (volume < threshold)
|
||||
return;
|
||||
|
||||
// logger.debug('hark "volume_change" event [volume:%sdB, threshold:%sdB]', volume, threshold);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
volume : Math.round((volume - threshold) * (-10) / threshold)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stream = nextProps.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// If there is something different in the stream, re-render it.
|
||||
|
||||
let previousTracksHash = this._tracksHash;
|
||||
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
if (this._tracksHash !== previousTracksHash)
|
||||
this.refs.video.srcObject = stream;
|
||||
}
|
||||
|
||||
handleResolutionClick()
|
||||
{
|
||||
if (!this.props.resolution)
|
||||
return;
|
||||
|
||||
logger.debug('handleResolutionClick()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
|
||||
_getTracksHash(stream)
|
||||
{
|
||||
return stream.getTracks()
|
||||
.map((track) =>
|
||||
{
|
||||
let trackId = track.jitsiRemoteId || track.id;
|
||||
|
||||
return trackId;
|
||||
})
|
||||
.join('|');
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
let video = this.refs.video;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
width : video.videoWidth,
|
||||
height : video.videoHeight
|
||||
});
|
||||
}
|
||||
|
||||
_cleanStream(stream)
|
||||
{
|
||||
// Hack for Firefox bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
let previousNumTracks = tracks.length;
|
||||
|
||||
// Remove ended tracks.
|
||||
for (let track of tracks)
|
||||
{
|
||||
if (track.readyState === 'ended')
|
||||
{
|
||||
logger.warn('_cleanStream() | removing ended track [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are multiple live audio tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getAudioTracks().length > 1)
|
||||
{
|
||||
let track = stream.getAudioTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live audio track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
// If there are multiple live video tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getVideoTracks().length > 1)
|
||||
{
|
||||
let track = stream.getVideoTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live video track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
let numTracks = stream.getTracks().length;
|
||||
|
||||
if (numTracks !== previousNumTracks)
|
||||
logger.warn('_cleanStream() | num tracks changed from %s to %s', previousNumTracks, numTracks);
|
||||
}
|
||||
}
|
||||
|
||||
Video.propTypes =
|
||||
{
|
||||
stream : PropTypes.object.isRequired,
|
||||
resolution : PropTypes.string,
|
||||
muted : PropTypes.bool,
|
||||
videoDisabled : PropTypes.bool,
|
||||
mirror : PropTypes.bool,
|
||||
onResolutionChange : PropTypes.func
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Room = PropTypes.shape(
|
||||
{
|
||||
url : PropTypes.string.isRequired,
|
||||
state : PropTypes.oneOf(
|
||||
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
|
||||
activeSpeakerName : PropTypes.string
|
||||
});
|
||||
|
||||
export const Device = PropTypes.shape(
|
||||
{
|
||||
flag : PropTypes.string.isRequired,
|
||||
name : PropTypes.string.isRequired,
|
||||
version : PropTypes.string
|
||||
});
|
||||
|
||||
export const Me = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
displayNameSet : PropTypes.bool.isRequired,
|
||||
device : Device.isRequired,
|
||||
canSendMic : PropTypes.bool.isRequired,
|
||||
canSendWebcam : PropTypes.bool.isRequired,
|
||||
canChangeWebcam : PropTypes.bool.isRequired,
|
||||
webcamInProgress : PropTypes.bool.isRequired,
|
||||
audioOnly : PropTypes.bool.isRequired,
|
||||
audioOnlyInProgress : PropTypes.bool.isRequired,
|
||||
restartIceInProgress : PropTypes.bool.isRequired
|
||||
});
|
||||
|
||||
export const Producer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam' ]).isRequired,
|
||||
deviceLabel : PropTypes.string,
|
||||
type : PropTypes.oneOf([ 'front', 'back' ]),
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
export const Peer = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
device : Device.isRequired,
|
||||
consumers : PropTypes.arrayOf(PropTypes.number).isRequired
|
||||
});
|
||||
|
||||
export const Consumer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
peerName : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam' ]).isRequired,
|
||||
supported : PropTypes.bool.isRequired,
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
profile : PropTypes.oneOf([ 'none', 'low', 'medium', 'high' ]),
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string
|
||||
});
|
||||
|
||||
export const Notification = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.string.isRequired,
|
||||
type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
|
||||
timeout : PropTypes.number
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import getMuiTheme from 'material-ui/styles/getMuiTheme';
|
||||
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
|
||||
import { grey500 } from 'material-ui/styles/colors';
|
||||
|
||||
// NOTE: I should clone it
|
||||
let theme = lightBaseTheme;
|
||||
|
||||
theme.palette.borderColor = grey500;
|
||||
|
||||
let muiTheme = getMuiTheme(lightBaseTheme);
|
||||
|
||||
export default muiTheme;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
const Appear = ({ duration, children }) => (
|
||||
<CSSTransition
|
||||
in
|
||||
classNames='Appear'
|
||||
timeout={duration || 1000}
|
||||
appear
|
||||
>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
Appear.propTypes =
|
||||
{
|
||||
duration : PropTypes.number,
|
||||
children : PropTypes.any
|
||||
};
|
||||
|
||||
export { Appear };
|
||||
Reference in New Issue
Block a user