This commit is contained in:
Iñaki Baz Castillo
2017-11-02 16:38:52 +01:00
parent 625f20f547
commit 52acf81eff
100 changed files with 26907 additions and 10234 deletions
-57
View File
@@ -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
};
+51
View File
@@ -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
};
-156
View File
@@ -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
};
+222
View File
@@ -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;
+61
View File
@@ -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;
-151
View File
@@ -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);
}
}
+90
View File
@@ -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;
+260
View File
@@ -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
};
+53
View File
@@ -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;
-138
View File
@@ -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
View File
@@ -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;
-585
View File
@@ -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
};
-60
View File
@@ -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
};
-241
View File
@@ -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
};
+71
View File
@@ -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
});
-16
View File
@@ -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;
+22
View File
@@ -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 };