Merge branch 'feat-cleanup' into develop

master
Håvar Aambø Fosstveit 2018-12-18 12:15:48 +01:00
commit 94b1db5789
70 changed files with 1409 additions and 17977 deletions

View File

@ -2,6 +2,7 @@
"plugins": "plugins":
[ [
"@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-object-rest-spread",
"jsx-control-statements",
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime" "@babel/plugin-transform-runtime"
], ],

View File

@ -15,7 +15,8 @@ module.exports =
extends: extends:
[ [
'eslint:recommended', 'eslint:recommended',
'plugin:react/recommended' 'plugin:react/recommended',
'plugin:jsx-control-statements/recommended'
], ],
settings: settings:
{ {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
import React from 'react';
const RoomContext = React.createContext();
export default RoomContext;
export function withRoomContext(Component)
{
return (props) => ( // eslint-disable-line react/display-name
<RoomContext.Consumer>
{(roomClient) => <Component {...props} roomClient={roomClient} />}
</RoomContext.Consumer>
);
}

View File

@ -1,92 +0,0 @@
import React, { Component } from 'react';
import { compose } from 'redux';
import PropTypes from 'prop-types';
import marked from 'marked';
import { connect } from 'react-redux';
import scrollToBottom from './scrollToBottom';
const linkRenderer = new marked.Renderer();
linkRenderer.link = (href, title, text) =>
{
title = title ? title : href;
text = text ? text : href;
return (`<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`);
};
class MessageList extends Component
{
getTimeString(time)
{
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
}
render()
{
const {
chatmessages
} = this.props;
return (
<div data-component='MessageList' id='messages'>
{ chatmessages.length > 0 ?
chatmessages.map((message, i) =>
{
const messageTime = new Date(message.time);
const picture = (message.sender === 'response' ?
message.picture : this.props.myPicture) || 'resources/images/avatar-empty.jpeg';
return (
<div className='message' key={i}>
<div className={message.sender}>
<img className='message-avatar' src={picture} />
<div className='message-content'>
<div
className='message-text'
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html : marked.parse(
message.text,
{ sanitize: true, renderer: linkRenderer }
) }}
/>
<span className='message-time'>
{message.name} - {this.getTimeString(messageTime)}
</span>
</div>
</div>
</div>
);
})
:<div className='empty'>
<p>No one has said anything yet...</p>
</div>
}
</div>
);
}
}
MessageList.propTypes =
{
chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired,
myPicture : PropTypes.string
};
const mapStateToProps = (state) =>
{
return {
chatmessages : state.chatmessages,
myPicture : state.me.picture
};
};
const MessageListContainer = compose(
connect(mapStateToProps),
scrollToBottom()
)(MessageList);
export default MessageListContainer;

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../../redux/stateActions';
class HiddenPeers extends Component class HiddenPeers extends Component
{ {
@ -29,7 +29,7 @@ class HiddenPeers extends Component
this.timeout = setTimeout(() => this.timeout = setTimeout(() =>
{ {
this.setState({ className: '' }); this.setState({ className: '' });
}, 2000); }, 500);
}); });
} }
} }
@ -42,20 +42,13 @@ class HiddenPeers extends Component
} = this.props; } = this.props;
return ( return (
<div <div data-component='HiddenPeers'>
data-component='HiddenPeers'
className={this.state.className}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
>
<div data-component='HiddenPeersView'>
<div className={classnames('view-container', this.state.className)} onClick={() => openUsersTab()}> <div className={classnames('view-container', this.state.className)} onClick={() => openUsersTab()}>
<p>+{hiddenPeersCount} <br /> participant <p>+{hiddenPeersCount} <br /> participant
{(hiddenPeersCount === 1) ? null : 's'} {(hiddenPeersCount === 1) ? null : 's'}
</p> </p>
</div> </div>
</div> </div>
</div>
); );
} }
} }

View File

@ -4,10 +4,10 @@ import ReactTooltip from 'react-tooltip';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { getDeviceInfo } from 'mediasoup-client'; import { getDeviceInfo } from 'mediasoup-client';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from '../appPropTypes';
import * as requestActions from '../redux/requestActions'; import { withRoomContext } from '../../RoomContext';
import PeerView from './PeerView'; import PeerView from '../VideoContainers/PeerView';
import ScreenView from './ScreenView'; import ScreenView from '../VideoContainers/ScreenView';
class Me extends React.Component class Me extends React.Component
{ {
@ -46,17 +46,13 @@ class Me extends React.Component
render() render()
{ {
const { const {
roomClient,
connected, connected,
me, me,
advancedMode, advancedMode,
micProducer, micProducer,
webcamProducer, webcamProducer,
screenProducer, screenProducer
onChangeDisplayName,
onMuteMic,
onUnmuteMic,
onEnableWebcam,
onDisableWebcam
} = this.props; } = this.props;
let micState; let micState;
@ -107,7 +103,7 @@ class Me extends React.Component
onMouseOut={this.handleMouseOut} onMouseOut={this.handleMouseOut}
> >
<div className={classnames('view-container', 'webcam')}> <div className={classnames('view-container', 'webcam')}>
{connected ? <If condition={connected}>
<div className={classnames('controls', 'visible')}> <div className={classnames('controls', 'visible')}>
<div <div
data-tip='keyboard shortcut: &lsquo;m&lsquo;' data-tip='keyboard shortcut: &lsquo;m&lsquo;'
@ -120,7 +116,9 @@ class Me extends React.Component
})} })}
onClick={() => onClick={() =>
{ {
micState === 'on' ? onMuteMic() : onUnmuteMic(); micState === 'on' ?
roomClient.muteMic() :
roomClient.unmuteMic();
}} }}
/> />
<ReactTooltip <ReactTooltip
@ -134,12 +132,13 @@ class Me extends React.Component
})} })}
onClick={() => onClick={() =>
{ {
webcamState === 'on' ? onDisableWebcam() : onEnableWebcam(); webcamState === 'on' ?
roomClient.disableWebcam() :
roomClient.enableWebcam();
}} }}
/> />
</div> </div>
:null </If>
}
<PeerView <PeerView
isMe isMe
@ -151,11 +150,14 @@ class Me extends React.Component
videoVisible={videoVisible} videoVisible={videoVisible}
audioCodec={micProducer ? micProducer.codec : null} audioCodec={micProducer ? micProducer.codec : null}
videoCodec={webcamProducer ? webcamProducer.codec : null} videoCodec={webcamProducer ? webcamProducer.codec : null}
onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)} onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
}}
/> />
</div> </div>
{screenProducer ? <If condition={screenProducer}>
<div className={classnames('view-container', 'screen')}> <div className={classnames('view-container', 'screen')}>
<ScreenView <ScreenView
isMe isMe
@ -165,8 +167,7 @@ class Me extends React.Component
screenCodec={screenProducer ? screenProducer.codec : null} screenCodec={screenProducer ? screenProducer.codec : null}
/> />
</div> </div>
:null </If>
}
</div> </div>
); );
} }
@ -204,17 +205,13 @@ class Me extends React.Component
Me.propTypes = Me.propTypes =
{ {
roomClient : PropTypes.any.isRequired,
connected : PropTypes.bool.isRequired, connected : PropTypes.bool.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
micProducer : appPropTypes.Producer, micProducer : appPropTypes.Producer,
webcamProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer,
screenProducer : appPropTypes.Producer, screenProducer : appPropTypes.Producer
onChangeDisplayName : PropTypes.func.isRequired,
onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -236,23 +233,8 @@ const mapStateToProps = (state) =>
}; };
}; };
const mapDispatchToProps = (dispatch) => const MeContainer = withRoomContext(connect(
{ mapStateToProps
return { )(Me));
onChangeDisplayName : (displayName) =>
{
dispatch(requestActions.changeDisplayName(displayName));
},
onMuteMic : () => dispatch(requestActions.muteMic()),
onUnmuteMic : () => dispatch(requestActions.unmuteMic()),
onEnableWebcam : () => dispatch(requestActions.enableWebcam()),
onDisableWebcam : () => dispatch(requestActions.disableWebcam())
};
};
const MeContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Me);
export default MeContainer; export default MeContainer;

View File

@ -2,11 +2,11 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from '../appPropTypes';
import * as requestActions from '../redux/requestActions'; import { withRoomContext } from '../../RoomContext';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../../redux/stateActions';
import PeerView from './PeerView'; import PeerView from '../VideoContainers/PeerView';
import ScreenView from './ScreenView'; import ScreenView from '../VideoContainers/ScreenView';
class Peer extends Component class Peer extends Component
{ {
@ -31,13 +31,12 @@ class Peer extends Component
render() render()
{ {
const { const {
roomClient,
advancedMode, advancedMode,
peer, peer,
micConsumer, micConsumer,
webcamConsumer, webcamConsumer,
screenConsumer, screenConsumer,
onMuteMic,
onUnmuteMic,
toggleConsumerFullscreen, toggleConsumerFullscreen,
toggleConsumerWindow, toggleConsumerWindow,
style, style,
@ -81,23 +80,21 @@ class Peer extends Component
onMouseOver={this.handleMouseOver} onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut} onMouseOut={this.handleMouseOut}
> >
{videoVisible && !webcamConsumer.supported ? <If condition={videoVisible && !webcamConsumer.supported}>
<div className='incompatible-video'> <div className='incompatible-video'>
<p>incompatible video</p> <p>incompatible video</p>
</div> </div>
:null </If>
}
{!videoVisible ? <If condition={!videoVisible}>
<div className='paused-video'> <div className='paused-video'>
<p>this video is paused</p> <p>this video is paused</p>
</div> </div>
:null </If>
}
<div className={classnames('view-container', 'webcam')} style={style}> <div className={classnames('view-container', 'webcam')} style={style}>
<div className='indicators'> <div className='indicators'>
{peer.raiseHandState ? <If condition={peer.raiseHandState}>
<div className={ <div className={
classnames( classnames(
'icon', 'raise-hand', { 'icon', 'raise-hand', {
@ -107,8 +104,7 @@ class Peer extends Component
) )
} }
/> />
:null </If>
}
</div> </div>
<div <div
className={classnames('controls', { className={classnames('controls', {
@ -124,7 +120,9 @@ class Peer extends Component
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name); micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
}} }}
/> />
@ -141,7 +139,9 @@ class Peer extends Component
/> />
<div <div
className={classnames('button', 'fullscreen')} className={classnames('button', 'fullscreen', {
disabled : !videoVisible
})}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -162,7 +162,7 @@ class Peer extends Component
/> />
</div> </div>
{screenConsumer ? <If condition={screenConsumer}>
<div className={classnames('view-container', 'screen')} style={style}> <div className={classnames('view-container', 'screen')} style={style}>
<div <div
className={classnames('controls', { className={classnames('controls', {
@ -170,7 +170,10 @@ class Peer extends Component
})} })}
> >
<div <div
className={classnames('button', 'newwindow')} className={classnames('button', 'newwindow', {
disabled : !screenVisible ||
(windowConsumer === screenConsumer.id)
})}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -179,7 +182,9 @@ class Peer extends Component
/> />
<div <div
className={classnames('button', 'fullscreen')} className={classnames('button', 'fullscreen', {
disabled : !screenVisible
})}
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
@ -195,8 +200,7 @@ class Peer extends Component
screenCodec={screenConsumer ? screenConsumer.codec : null} screenCodec={screenConsumer ? screenConsumer.codec : null}
/> />
</div> </div>
:null </If>
}
</div> </div>
); );
} }
@ -204,14 +208,13 @@ class Peer extends Component
Peer.propTypes = Peer.propTypes =
{ {
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peer : appPropTypes.Peer.isRequired, peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer,
windowConsumer : PropTypes.number, windowConsumer : PropTypes.number,
onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired,
streamDimensions : PropTypes.object, streamDimensions : PropTypes.object,
style : PropTypes.object, style : PropTypes.object,
toggleConsumerFullscreen : PropTypes.func.isRequired, toggleConsumerFullscreen : PropTypes.func.isRequired,
@ -242,14 +245,6 @@ const mapStateToProps = (state, { name }) =>
const mapDispatchToProps = (dispatch) => const mapDispatchToProps = (dispatch) =>
{ {
return { return {
onMuteMic : (peerName) =>
{
dispatch(requestActions.mutePeerAudio(peerName));
},
onUnmuteMic : (peerName) =>
{
dispatch(requestActions.unmutePeerAudio(peerName));
},
toggleConsumerFullscreen : (consumer) => toggleConsumerFullscreen : (consumer) =>
{ {
if (consumer) if (consumer)
@ -263,9 +258,9 @@ const mapDispatchToProps = (dispatch) =>
}; };
}; };
const PeerContainer = connect( const PeerContainer = withRoomContext(connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(Peer); )(Peer));
export default PeerContainer; export default PeerContainer;

View File

@ -2,9 +2,9 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from '../appPropTypes';
import * as requestActions from '../redux/requestActions'; import { withRoomContext } from '../../RoomContext';
import FullScreen from './FullScreen'; import FullScreen from '../FullScreen';
class Sidebar extends Component class Sidebar extends Component
{ {
@ -56,8 +56,10 @@ class Sidebar extends Component
render() render()
{ {
const { const {
toolbarsVisible, me, screenProducer, onLogin, onShareScreen, roomClient,
onUnShareScreen, onNeedExtension, onLeaveMeeting, onLogout, onToggleHand toolbarsVisible,
me,
screenProducer
} = this.props; } = this.props;
let screenState; let screenState;
@ -91,7 +93,7 @@ class Sidebar extends Component
})} })}
data-component='Sidebar' data-component='Sidebar'
> >
{this.fullscreen.fullscreenEnabled && ( <If condition={this.fullscreen.fullscreenEnabled}>
<div <div
className={classnames('button', 'fullscreen', { className={classnames('button', 'fullscreen', {
on : this.state.fullscreen on : this.state.fullscreen
@ -101,7 +103,7 @@ class Sidebar extends Component
data-place='right' data-place='right'
data-type='dark' data-type='dark'
/> />
)} </If>
<div <div
className={classnames('button', 'screen', screenState)} className={classnames('button', 'screen', screenState)}
@ -114,17 +116,17 @@ class Sidebar extends Component
{ {
case 'on': case 'on':
{ {
onUnShareScreen(); roomClient.disableScreenSharing();
break; break;
} }
case 'off': case 'off':
{ {
onShareScreen(); roomClient.enableScreenSharing();
break; break;
} }
case 'need-extension': case 'need-extension':
{ {
onNeedExtension(); roomClient.installExtension();
break; break;
} }
default: default:
@ -135,25 +137,30 @@ class Sidebar extends Component
}} }}
/> />
{me.loginEnabled && (me.loggedIn ? ( <If condition={me.loginEnabled}>
<Choose>
<When condition={me.loggedIn}>
<div <div
className='button logout' className='button logout'
data-tip='Logout' data-tip='Logout'
data-place='right' data-place='right'
data-type='dark' data-type='dark'
onClick={onLogout} onClick={() => roomClient.logout()}
> >
<img src={me.picture || 'resources/images/avatar-empty.jpeg'} /> <img src={me.picture || 'resources/images/avatar-empty.jpeg'} />
</div> </div>
) : ( </When>
<Otherwise>
<div <div
className='button login off' className='button login off'
data-tip='Login' data-tip='Login'
data-place='right' data-place='right'
data-type='dark' data-type='dark'
onClick={onLogin} onClick={() => roomClient.login()}
/> />
))} </Otherwise>
</Choose>
</If>
<div <div
className={classnames('button', 'raise-hand', { className={classnames('button', 'raise-hand', {
on : me.raiseHand, on : me.raiseHand,
@ -162,7 +169,7 @@ class Sidebar extends Component
data-tip='Raise hand' data-tip='Raise hand'
data-place='right' data-place='right'
data-type='dark' data-type='dark'
onClick={() => onToggleHand(!me.raiseHand)} onClick={() => roomClient.sendRaiseHandState(!me.raiseHand)}
/> />
<div <div
@ -170,7 +177,7 @@ class Sidebar extends Component
data-tip='Leave meeting' data-tip='Leave meeting'
data-place='right' data-place='right'
data-type='dark' data-type='dark'
onClick={() => onLeaveMeeting()} onClick={() => roomClient.close()}
/> />
</div> </div>
); );
@ -178,15 +185,9 @@ class Sidebar extends Component
} }
Sidebar.propTypes = { Sidebar.propTypes = {
roomClient : PropTypes.any.isRequired,
toolbarsVisible : PropTypes.bool.isRequired, toolbarsVisible : PropTypes.bool.isRequired,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
onShareScreen : PropTypes.func.isRequired,
onUnShareScreen : PropTypes.func.isRequired,
onNeedExtension : PropTypes.func.isRequired,
onToggleHand : PropTypes.func.isRequired,
onLeaveMeeting : PropTypes.func.isRequired,
onLogin : PropTypes.func.isRequired,
onLogout : PropTypes.func.isRequired,
screenProducer : appPropTypes.Producer screenProducer : appPropTypes.Producer
}; };
@ -198,17 +199,6 @@ const mapStateToProps = (state) =>
me : state.me me : state.me
}); });
const mapDispatchToProps = { export default withRoomContext(connect(
onLeaveMeeting : requestActions.leaveRoom, mapStateToProps
onShareScreen : requestActions.enableScreenSharing, )(Sidebar));
onUnShareScreen : requestActions.disableScreenSharing,
onNeedExtension : requestActions.installExtension,
onToggleHand : requestActions.toggleHand,
onLogin : requestActions.userLogin,
onLogout : requestActions.userLogout
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar);

View File

@ -1,196 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import magnet from 'magnet-uri';
import WebTorrent from 'webtorrent';
import * as requestActions from '../../redux/requestActions';
import { saveAs } from 'file-saver/FileSaver';
import { client } from './index';
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
class FileEntry extends Component
{
state = {
active : false,
numPeers : 0,
progress : 0,
files : null
};
saveFile = (file) =>
{
file.getBlob((err, blob) =>
{
if (err)
{
return this.props.notify({
text : 'An error occurred while saving a file'
});
}
saveAs(blob, file.name);
});
};
handleTorrent = (torrent) =>
{
// Torrent already done, this can happen if the
// same file was sent multiple times.
if (torrent.progress === 1)
{
this.setState({
files : torrent.files,
numPeers : torrent.numPeers,
progress : 1,
active : false,
timeout : false
});
return;
}
const onProgress = () =>
{
this.setState({
numPeers : torrent.numPeers,
progress : torrent.progress
});
};
onProgress();
setInterval(onProgress, 500);
torrent.on('done', () =>
{
onProgress();
clearInterval(onProgress);
this.setState({
files : torrent.files,
active : false
});
});
};
handleDownload = () =>
{
this.setState({
active : true
});
const magnetURI = this.props.data.file.magnet;
const existingTorrent = client.get(magnetURI);
if (existingTorrent)
{
// Never add duplicate torrents, use the existing one instead.
return this.handleTorrent(existingTorrent);
}
client.add(magnetURI, this.handleTorrent);
setTimeout(() =>
{
if (this.state.active && this.state.numPeers === 0)
{
this.setState({
timeout : true
});
}
}, 10 * 1000);
}
render()
{
return (
<div className='file-entry'>
<img className='file-avatar' src={this.props.data.picture || DEFAULT_PICTURE} />
<div className='file-content'>
{this.props.data.me ? (
<p>You shared a file.</p>
) : (
<p>{this.props.data.name} shared a file.</p>
)}
{!this.state.active && !this.state.files && (
<div className='file-info'>
{WebTorrent.WEBRTC_SUPPORT ? (
<span className='button' onClick={this.handleDownload}>
<img src='resources/images/download-icon.svg' />
</span>
) : (
<p>
Your browser does not support downloading files using WebTorrent.
</p>
)}
<p>{magnet.decode(this.props.data.file.magnet).dn}</p>
</div>
)}
{this.state.active && this.state.numPeers === 0 && (
<Fragment>
<p>
Locating peers
</p>
{this.state.timeout && (
<p>
If this process takes a long time, there might not be anyone seeding
this torrent. Try asking someone to reupload the file that you want.
</p>
)}
</Fragment>
)}
{this.state.active && this.state.numPeers > 0 && (
<progress value={this.state.progress} />
)}
{this.state.files && (
<Fragment>
<p>Torrent finished downloading.</p>
{this.state.files.map((file, i) => (
<div className='file-info' key={i}>
<span className='button' onClick={() => this.saveFile(file)}>
<img src='resources/images/save-icon.svg' />
</span>
<p>{file.name}</p>
</div>
))}
</Fragment>
)}
</div>
</div>
);
}
}
export const FileEntryProps = {
data : PropTypes.shape({
name : PropTypes.string.isRequired,
picture : PropTypes.string,
file : PropTypes.shape({
magnet : PropTypes.string.isRequired
}).isRequired,
me : PropTypes.bool
}).isRequired,
notify : PropTypes.func.isRequired
};
FileEntry.propTypes = FileEntryProps;
const mapDispatchToProps = {
notify : requestActions.notify
};
export default connect(
undefined,
mapDispatchToProps
)(FileEntry);

View File

@ -1,54 +0,0 @@
import React, { Component } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import FileEntry, { FileEntryProps } from './FileEntry';
import scrollToBottom from '../Chat/scrollToBottom';
/**
* This component cannot be pure, as we need to use
* refs to scroll to the bottom when new files arrive.
*/
class SharedFilesList extends Component
{
render()
{
const { sharing } = this.props;
return (
<div className='shared-files'>
{ sharing.length > 0 ?
sharing.map((entry, i) => (
<FileEntry
data={entry}
key={i}
/>
))
:<div className='empty'>
<p>No one has shared files yet...</p>
</div>
}
</div>
);
}
}
SharedFilesList.propTypes = {
sharing : PropTypes.arrayOf(FileEntryProps.data).isRequired
};
const mapStateToProps = (state) =>
({
sharing : state.sharing,
// Included to scroll to the bottom when the user
// actually opens the tab. When the component first
// mounts, the component is not visible and so the
// component has no height which can be used for scrolling.
tabOpen : state.toolarea.currentToolTab === 'files'
});
export default compose(
connect(mapStateToProps),
scrollToBottom()
)(SharedFilesList);

View File

@ -1,131 +0,0 @@
import React, { Component } from 'react';
import WebTorrent from 'webtorrent';
import createTorrent from 'create-torrent';
import randomString from 'random-string';
import classNames from 'classnames';
import * as stateActions from '../../redux/stateActions';
import * as requestActions from '../../redux/requestActions';
import { store } from '../../store';
import config from '../../../config';
import SharedFilesList from './SharedFilesList';
export const client = WebTorrent.WEBRTC_SUPPORT && new WebTorrent({
tracker : {
rtcConfig : {
iceServers : config.turnServers
}
}
});
const notifyPeers = (file) =>
{
const { displayName, picture } = store.getState().me;
store.dispatch(requestActions.sendFile(file, displayName, picture));
};
export const shareFiles = async (files) =>
{
const notification =
{
id : randomString({ length: 6 }).toLowerCase(),
text : 'Creating torrent',
type : 'info'
};
store.dispatch(stateActions.addNotification(notification));
createTorrent(files, (err, torrent) =>
{
if (err)
{
return store.dispatch(requestActions.notify({
text : 'An error occured while uploading a file'
}));
}
const existingTorrent = client.get(torrent);
if (existingTorrent)
{
return notifyPeers({
magnet : existingTorrent.magnetURI
});
}
client.seed(files, (newTorrent) =>
{
store.dispatch(stateActions.removeNotification(notification.id));
store.dispatch(requestActions.notify({
text : 'Torrent successfully created'
}));
notifyPeers({
magnet : newTorrent.magnetURI
});
});
});
};
class FileSharing extends Component
{
constructor(props)
{
super(props);
this.fileInput = React.createRef();
}
handleFileChange = async (event) =>
{
if (event.target.files.length > 0)
{
await shareFiles(event.target.files);
}
};
handleClick = () =>
{
if (WebTorrent.WEBRTC_SUPPORT)
{
// We want to open the file dialog when we click a button
// instead of actually rendering the input element itself.
this.fileInput.current.click();
}
};
render()
{
const buttonDescription = WebTorrent.WEBRTC_SUPPORT ?
'Share file' : 'File sharing not supported';
return (
<div data-component='FileSharing'>
<div className='sharing-toolbar'>
<input
style={{ display: 'none' }}
ref={this.fileInput}
type='file'
onChange={this.handleFileChange}
multiple
/>
<div
type='button'
onClick={this.handleClick}
className={classNames('share-file', {
disabled : !WebTorrent.WEBRTC_SUPPORT
})}
>
<span>{buttonDescription}</span>
</div>
</div>
<SharedFilesList />
</div>
);
}
}
export default FileSharing;

View File

@ -4,9 +4,9 @@ import ResizeObserver from 'resize-observer-polyfill';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import classnames from 'classnames'; import classnames from 'classnames';
import * as requestActions from '../redux/requestActions'; import { withRoomContext } from '../../RoomContext';
import Peer from './Peer'; import Peer from '../Containers/Peer';
import HiddenPeers from './HiddenPeers'; import HiddenPeers from '../Containers/HiddenPeers';
class Filmstrip extends Component class Filmstrip extends Component
{ {
@ -114,14 +114,20 @@ class Filmstrip extends Component
render() render()
{ {
const { peers, advancedMode, spotlights, spotlightsLength } = this.props; const {
roomClient,
peers,
advancedMode,
spotlights,
spotlightsLength
} = this.props;
const activePeerName = this.getActivePeerName(); const activePeerName = this.getActivePeerName();
return ( return (
<div data-component='Filmstrip'> <div data-component='Filmstrip'>
<div className='active-peer-container' ref={this.activePeerContainer}> <div className='active-peer-container' ref={this.activePeerContainer}>
{peers[activePeerName] && ( <If condition={peers[activePeerName]}>
<div <div
className='active-peer' className='active-peer'
style={{ style={{
@ -134,19 +140,19 @@ class Filmstrip extends Component
name={activePeerName} name={activePeerName}
/> />
</div> </div>
)} </If>
</div> </div>
<div className='filmstrip'> <div className='filmstrip'>
<div className='filmstrip-content'> <div className='filmstrip-content'>
{ Object.keys(peers).map((peerName) =>
{ {
Object.keys(peers).map((peerName) => if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
{ {
return ( return (
spotlights.find((spotlightsElement) => spotlightsElement === peerName)?
<div <div
key={peerName} key={peerName}
onClick={() => this.props.setSelectedPeer(peerName)} onClick={() => roomClient.setSelectedPeer(peerName)}
className={classnames('film', { className={classnames('film', {
selected : this.props.selectedPeerName === peerName, selected : this.props.selectedPeerName === peerName,
active : this.state.lastSpeaker === peerName active : this.state.lastSpeaker === peerName
@ -159,18 +165,17 @@ class Filmstrip extends Component
/> />
</div> </div>
</div> </div>
:null
); );
})
} }
})}
</div> </div>
</div> </div>
<div className='hidden-peer-container'> <div className='hidden-peer-container'>
{ (spotlightsLength<Object.keys(peers).length)? <If condition={(spotlightsLength<Object.keys(peers).length)}>
<HiddenPeers <HiddenPeers
hiddenPeersCount={Object.keys(peers).length-spotlightsLength} hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
/>:null />
} </If>
</div> </div>
</div> </div>
@ -179,13 +184,13 @@ class Filmstrip extends Component
} }
Filmstrip.propTypes = { Filmstrip.propTypes = {
roomClient : PropTypes.any.isRequired,
activeSpeakerName : PropTypes.string, activeSpeakerName : PropTypes.string,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.object.isRequired, peers : PropTypes.object.isRequired,
consumers : PropTypes.object.isRequired, consumers : PropTypes.object.isRequired,
myName : PropTypes.string.isRequired, myName : PropTypes.string.isRequired,
selectedPeerName : PropTypes.string, selectedPeerName : PropTypes.string,
setSelectedPeer : PropTypes.func.isRequired,
spotlightsLength : PropTypes.number, spotlightsLength : PropTypes.number,
spotlights : PropTypes.array.isRequired spotlights : PropTypes.array.isRequired
}; };
@ -205,11 +210,7 @@ const mapStateToProps = (state) =>
}; };
}; };
const mapDispatchToProps = { export default withRoomContext(connect(
setSelectedPeer : requestActions.setSelectedPeer
};
export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps undefined
)(Filmstrip); )(Filmstrip));

View File

@ -3,10 +3,9 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import * as appPropTypes from './appPropTypes'; import { Appear } from '../transitions';
import { Appear } from './transitions'; import Peer from '../Containers/Peer';
import Peer from './Peer'; import HiddenPeers from '../Containers/HiddenPeers';
import HiddenPeers from './HiddenPeers';
import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
const RATIO = 1.334; const RATIO = 1.334;
@ -105,38 +104,36 @@ class Peers extends React.Component
return ( return (
<div data-component='Peers' ref={this.peersRef}> <div data-component='Peers' ref={this.peersRef}>
{ Object.keys(peers).map((peerName) =>
{ {
peers.map((peer) => if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
{ {
return ( return (
(spotlights.find(function(spotlightsElement) <Appear key={peerName} duration={1000}>
{ return spotlightsElement == peer.name; }))?
<Appear key={peer.name} duration={1000}>
<div <div
className={classnames('peer-container', { className={classnames('peer-container', {
'selected' : this.props.selectedPeerName === peer.name, 'selected' : this.props.selectedPeerName === peerName,
'active-speaker' : peer.name === activeSpeakerName 'active-speaker' : peerName === activeSpeakerName
})} })}
> >
<div className='peer-content'> <div className='peer-content'>
<Peer <Peer
advancedMode={advancedMode} advancedMode={advancedMode}
name={peer.name} name={peerName}
style={style} style={style}
/> />
</div> </div>
</div> </div>
</Appear> </Appear>
:null
); );
})
} }
})}
<div className='hidden-peer-container'> <div className='hidden-peer-container'>
{ (spotlightsLength<peers.length)? <If condition={spotlightsLength < Object.keys(peers).length}>
<HiddenPeers <HiddenPeers
hiddenPeersCount={peers.length-spotlightsLength} hiddenPeersCount={Object.keys(peers).length - spotlightsLength}
/>:null />
} </If>
</div> </div>
</div> </div>
); );
@ -146,7 +143,7 @@ class Peers extends React.Component
Peers.propTypes = Peers.propTypes =
{ {
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, peers : PropTypes.object.isRequired,
boxes : PropTypes.number, boxes : PropTypes.number,
activeSpeakerName : PropTypes.string, activeSpeakerName : PropTypes.string,
selectedPeerName : PropTypes.string, selectedPeerName : PropTypes.string,
@ -156,14 +153,13 @@ Peers.propTypes =
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
const peers = Object.values(state.peers);
const spotlights = state.room.spotlights; const spotlights = state.room.spotlights;
const spotlightsLength = spotlights ? state.room.spotlights.length : 0; const spotlightsLength = spotlights ? state.room.spotlights.length : 0;
const boxes = spotlightsLength + Object.values(state.consumers) const boxes = spotlightsLength + Object.values(state.consumers)
.filter((consumer) => consumer.source === 'screen').length; .filter((consumer) => consumer.source === 'screen').length;
return { return {
peers, peers : state.peers,
boxes, boxes,
activeSpeakerName : state.room.activeSpeakerName, activeSpeakerName : state.room.activeSpeakerName,
selectedPeerName : state.room.selectedPeerName, selectedPeerName : state.room.selectedPeerName,

View File

@ -9,21 +9,17 @@ import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../redux/stateActions';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Me from './Me'; import Me from './Containers/Me';
import Peers from './Peers'; import Peers from './Layouts/Peers';
import AudioPeers from './PeerAudio/AudioPeers'; import AudioPeers from './PeerAudio/AudioPeers';
import Notifications from './Notifications'; import Notifications from './Notifications';
// import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea'; import ToolArea from './ToolArea/ToolArea';
import FullScreenView from './FullScreenView'; import FullScreenView from './VideoContainers/FullScreenView';
import VideoWindow from './VideoWindow/VideoWindow'; import VideoWindow from './VideoWindow/VideoWindow';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import { idle } from '../utils'; import { idle } from '../utils';
import Sidebar from './Sidebar'; import Sidebar from './Controls/Sidebar';
import Filmstrip from './Filmstrip'; import Filmstrip from './Layouts/Filmstrip';
import { configureDragDrop, HoldingOverlay } from './FileSharing/DragDropSharing';
configureDragDrop();
// Hide toolbars after 10 seconds of inactivity. // Hide toolbars after 10 seconds of inactivity.
const TIMEOUT = 10 * 1000; const TIMEOUT = 10 * 1000;
@ -79,8 +75,6 @@ class Room extends React.Component
return ( return (
<Fragment> <Fragment>
<HoldingOverlay />
<Appear duration={300}> <Appear duration={300}>
<div data-component='Room'> <div data-component='Room'>
<CookieConsent> <CookieConsent>
@ -97,13 +91,12 @@ class Room extends React.Component
<Notifications /> <Notifications />
{room.advancedMode ? <If condition={room.advancedMode}>
<div className='state' data-tip='Server status'> <div className='state' data-tip='Server status'>
<div className={classnames('icon', room.state)} /> <div className={classnames('icon', room.state)} />
<p className={classnames('text', room.state)}>{room.state}</p> <p className={classnames('text', room.state)}>{room.state}</p>
</div> </div>
:null </If>
}
<div <div
className={classnames('room-link-wrapper room-controls', { className={classnames('room-link-wrapper room-controls', {

View File

@ -1,125 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions';
import PropTypes from 'prop-types';
import Dropdown from 'react-dropdown';
import ReactTooltip from 'react-tooltip';
const modes = [ {
value : 'democratic',
label : 'Democratic view'
}, {
value : 'filmstrip',
label : 'Filmstrip view'
} ];
const findOption = (options, value) => options.find((option) => option.value === value);
const Settings = ({
room, me, onToggleAdvancedMode, handleChangeWebcam,
handleChangeAudioDevice, handleChangeMode
}) =>
{
let webcams;
if (me.webcamDevices)
webcams = Array.from(me.webcamDevices.values());
else
webcams = [];
let audioDevices;
let audioDevicesText;
if (me.canChangeAudioDevice)
audioDevicesText = 'Select audio input device';
else
audioDevicesText = 'Unable to select audio input device';
if (me.audioDevices)
audioDevices = Array.from(me.audioDevices.values());
else
audioDevices = [];
return (
<div data-component='Settings'>
<div className='settings'>
<Dropdown
options={webcams}
value={findOption(webcams, me.selectedWebcam)}
onChange={(webcam) => handleChangeWebcam(webcam.value)}
placeholder={'Select camera'}
/>
<Dropdown
disabled={!me.canChangeAudioDevice}
options={audioDevices}
value={findOption(audioDevices, me.selectedAudioDevice)}
onChange={(device) => handleChangeAudioDevice(device.value)}
placeholder={audioDevicesText}
/>
<ReactTooltip
effect='solid'
/>
<div
data-tip='keyboard shortcut: &lsquo;a&lsquo;'
data-type='dark'
data-place='left'
>
<input
id='room-mode'
type='checkbox'
checked={room.advancedMode}
onChange={onToggleAdvancedMode}
/>
<label htmlFor='room-mode'>Advanced mode</label>
</div>
<div
data-tip='keyboard shortcut: type a digit'
data-type='dark'
data-place='left'
>
<Dropdown
options={modes}
value={findOption(modes, room.mode)}
onChange={(mode) => handleChangeMode(mode.value)}
/>
</div>
</div>
</div>
);
};
Settings.propTypes =
{
me : appPropTypes.Me.isRequired,
room : appPropTypes.Room.isRequired,
handleChangeWebcam : PropTypes.func.isRequired,
handleChangeAudioDevice : PropTypes.func.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
me : state.me,
room : state.room
};
};
const mapDispatchToProps = {
handleChangeWebcam : requestActions.changeWebcam,
handleChangeAudioDevice : requestActions.changeAudioDevice,
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
handleChangeMode : stateActions.setDisplayMode
};
const SettingsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Settings);
export default SettingsContainer;

View File

@ -1,17 +1,28 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as stateActions from '../../redux/stateActions'; import { withRoomContext } from '../../../RoomContext';
import * as requestActions from '../../redux/requestActions';
import MessageList from './MessageList'; import MessageList from './MessageList';
class Chat extends Component class Chat extends Component
{ {
createNewMessage(text, sender, name, picture)
{
return {
type : 'message',
text,
time : Date.now(),
name,
sender,
picture
};
}
render() render()
{ {
const { const {
roomClient,
senderPlaceHolder, senderPlaceHolder,
onSendMessage,
disabledInput, disabledInput,
autofocus, autofocus,
displayName, displayName,
@ -23,7 +34,19 @@ class Chat extends Component
<MessageList /> <MessageList />
<form <form
data-component='Sender' data-component='Sender'
onSubmit={(e) => { onSendMessage(e, displayName, picture); }} onSubmit={(e) =>
{
e.preventDefault();
const userInput = e.target.message.value;
if (userInput)
{
const message = this.createNewMessage(userInput, 'response', displayName, picture);
roomClient.sendChatMessage(message);
}
e.target.message.value = '';
}}
> >
<input <input
type='text' type='text'
@ -47,8 +70,8 @@ class Chat extends Component
Chat.propTypes = Chat.propTypes =
{ {
roomClient : PropTypes.any.isRequired,
senderPlaceHolder : PropTypes.string, senderPlaceHolder : PropTypes.string,
onSendMessage : PropTypes.func,
disabledInput : PropTypes.bool, disabledInput : PropTypes.bool,
autofocus : PropTypes.bool, autofocus : PropTypes.bool,
displayName : PropTypes.string, displayName : PropTypes.string,
@ -71,27 +94,8 @@ const mapStateToProps = (state) =>
}; };
}; };
const mapDispatchToProps = (dispatch) => const ChatContainer = withRoomContext(connect(
{ mapStateToProps
return { )(Chat));
onSendMessage : (event, displayName, picture) =>
{
event.preventDefault();
const userInput = event.target.message.value;
if (userInput)
{
dispatch(stateActions.addUserMessage(userInput));
dispatch(requestActions.sendChatMessage(userInput, displayName, picture));
}
event.target.message.value = '';
}
};
};
const ChatContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Chat);
export default ChatContainer; export default ChatContainer;

View File

@ -0,0 +1,98 @@
import React, { Component } from 'react';
import { compose } from 'redux';
import PropTypes from 'prop-types';
import marked from 'marked';
import { connect } from 'react-redux';
import scrollToBottom from '../scrollToBottom';
const linkRenderer = new marked.Renderer();
linkRenderer.link = (href, title, text) =>
{
title = title ? title : href;
text = text ? text : href;
return (`<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`);
};
class MessageList extends Component
{
getTimeString(time)
{
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
}
render()
{
const {
chatmessages
} = this.props;
return (
<div data-component='MessageList' id='messages'>
<Choose>
<When condition={chatmessages.length > 0}>
{
chatmessages.map((message, i) =>
{
const messageTime = new Date(message.time);
const picture = (message.sender === 'response' ?
message.picture : this.props.myPicture) || 'resources/images/avatar-empty.jpeg';
return (
<div className='message' key={i}>
<div className={message.sender}>
<img className='message-avatar' src={picture} />
<div className='message-content'>
<div
className='message-text'
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html : marked.parse(
message.text,
{ sanitize: true, renderer: linkRenderer }
) }}
/>
<span className='message-time'>
{message.name} - {this.getTimeString(messageTime)}
</span>
</div>
</div>
</div>
);
})
}
</When>
<Otherwise>
<div className='empty'>
<p>No one has said anything yet...</p>
</div>
</Otherwise>
</Choose>
</div>
);
}
}
MessageList.propTypes =
{
chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired,
myPicture : PropTypes.string
};
const mapStateToProps = (state) =>
{
return {
chatmessages : state.chatmessages,
myPicture : state.me.picture
};
};
const MessageListContainer = compose(
connect(mapStateToProps),
scrollToBottom()
)(MessageList);
export default MessageListContainer;

View File

@ -0,0 +1,113 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRoomContext } from '../../../RoomContext';
import magnet from 'magnet-uri';
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
class File extends Component
{
render()
{
const {
roomClient,
torrentSupport,
file
} = this.props;
return (
<div className='file-entry'>
<img className='file-avatar' src={file.picture || DEFAULT_PICTURE} />
<div className='file-content'>
<Choose>
<When condition={file.me}>
<p>You shared a file.</p>
</When>
<Otherwise>
<p>{file.displayName} shared a file.</p>
</Otherwise>
</Choose>
<If condition={!file.active && !file.files}>
<div className='file-info'>
<Choose>
<When condition={torrentSupport}>
<span
className='button'
onClick={() =>
{
roomClient.handleDownload(file.magnetUri);
}}
>
<img src='resources/images/download-icon.svg' />
</span>
</When>
<Otherwise>
<p>
Your browser does not support downloading files using WebTorrent.
</p>
</Otherwise>
</Choose>
<p>{magnet.decode(file.magnetUri).dn}</p>
</div>
</If>
<If condition={file.timeout}>
<Fragment>
<p>
If this process takes a long time, there might not be anyone seeding
this torrent. Try asking someone to reupload the file that you want.
</p>
</Fragment>
</If>
<If condition={file.active}>
<progress value={file.progress} />
</If>
<If condition={file.files}>
<Fragment>
<p>File finished downloading.</p>
{file.files.map((sharedFile, i) => (
<div className='file-info' key={i}>
<span
className='button'
onClick={() =>
{
roomClient.saveFile(sharedFile);
}}
>
<img src='resources/images/save-icon.svg' />
</span>
<p>{sharedFile.name}</p>
</div>
))}
</Fragment>
</If>
</div>
</div>
);
}
}
File.propTypes = {
roomClient : PropTypes.object.isRequired,
torrentSupport : PropTypes.bool.isRequired,
file : PropTypes.object.isRequired
};
const mapStateToProps = (state, { magnetUri }) =>
{
return {
file : state.files[magnetUri],
torrentSupport : state.room.torrentSupport
};
};
export default withRoomContext(connect(
mapStateToProps
)(File));

View File

@ -0,0 +1,40 @@
import React, { Component } from 'react';
import { compose } from 'redux';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import scrollToBottom from '../scrollToBottom';
import File from './File';
class FileList extends Component
{
render()
{
const {
files
} = this.props;
return (
<div className='shared-files'>
{ Object.keys(files).map((magnetUri) =>
<File key={magnetUri} magnetUri={magnetUri} />
)}
</div>
);
}
}
FileList.propTypes = {
files : PropTypes.object.isRequired
};
const mapStateToProps = (state) =>
{
return {
files : state.files
};
};
export default compose(
connect(mapStateToProps),
scrollToBottom()
)(FileList);

View File

@ -0,0 +1,88 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { withRoomContext } from '../../../RoomContext';
import FileList from './FileList';
class FileSharing extends Component
{
constructor(props)
{
super(props);
this._fileInput = React.createRef();
}
handleFileChange = async (event) =>
{
if (event.target.files.length > 0)
{
this.props.roomClient.shareFiles(event.target.files);
}
};
handleClick = () =>
{
if (this.props.torrentSupport)
{
// We want to open the file dialog when we click a button
// instead of actually rendering the input element itself.
this._fileInput.current.click();
}
};
render()
{
const {
torrentSupport
} = this.props;
const buttonDescription = torrentSupport ?
'Share file' : 'File sharing not supported';
return (
<div data-component='FileSharing'>
<div className='sharing-toolbar'>
<input
style={{ display: 'none' }}
ref={this._fileInput}
type='file'
onChange={this.handleFileChange}
multiple
/>
<div
type='button'
onClick={this.handleClick}
className={classNames('share-file', {
disabled : !torrentSupport
})}
>
<span>{buttonDescription}</span>
</div>
</div>
<FileList />
</div>
);
}
}
FileSharing.propTypes = {
roomClient : PropTypes.any.isRequired,
torrentSupport : PropTypes.bool.isRequired,
tabOpen : PropTypes.bool.isRequired
};
const mapStateToProps = (state) =>
{
return {
torrentSupport : state.room.torrentSupport,
tabOpen : state.toolarea.currentToolTab === 'files'
};
};
export default withRoomContext(connect(
mapStateToProps
)(FileSharing));

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Me } from '../appPropTypes'; import { Me } from '../../appPropTypes';
const ListMe = ({ me }) => const ListMe = ({ me }) =>
{ {

View File

@ -2,19 +2,16 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import * as requestActions from '../../redux/requestActions'; import { withRoomContext } from '../../../RoomContext';
const ListPeer = (props) => const ListPeer = (props) =>
{ {
const { const {
roomClient,
peer, peer,
micConsumer, micConsumer,
screenConsumer, screenConsumer
onMuteMic,
onUnmuteMic,
onDisableScreen,
onEnableScreen
} = props; } = props;
const micEnabled = ( const micEnabled = (
@ -39,7 +36,7 @@ const ListPeer = (props) =>
{peer.displayName} {peer.displayName}
</div> </div>
<div className='indicators'> <div className='indicators'>
{peer.raiseHandState ? <If condition={peer.raiseHandState}>
<div className={ <div className={
classnames( classnames(
'icon', 'raise-hand', { 'icon', 'raise-hand', {
@ -49,14 +46,13 @@ const ListPeer = (props) =>
) )
} }
/> />
:null </If>
}
</div> </div>
<div className='volume-container'> <div className='volume-container'>
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} /> <div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
</div> </div>
<div className='controls'> <div className='controls'>
{ screenConsumer ? <If condition={screenConsumer}>
<div <div
className={classnames('button', 'screen', { className={classnames('button', 'screen', {
on : screenVisible, on : screenVisible,
@ -67,11 +63,11 @@ const ListPeer = (props) =>
{ {
e.stopPropagation(); e.stopPropagation();
screenVisible ? screenVisible ?
onDisableScreen(peer.name) : onEnableScreen(peer.name); roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
roomClient.modifyPeerConsumer(peer.name, 'screen', false);
}} }}
/> />
:null </If>
}
<div <div
className={classnames('button', 'mic', { className={classnames('button', 'mic', {
on : micEnabled, on : micEnabled,
@ -81,7 +77,9 @@ const ListPeer = (props) =>
onClick={(e) => onClick={(e) =>
{ {
e.stopPropagation(); e.stopPropagation();
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name); micEnabled ?
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
}} }}
/> />
</div> </div>
@ -91,17 +89,12 @@ const ListPeer = (props) =>
ListPeer.propTypes = ListPeer.propTypes =
{ {
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peer : appPropTypes.Peer.isRequired, peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer, micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer
onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired,
onEnableScreen : PropTypes.func.isRequired,
onDisableScreen : PropTypes.func.isRequired
}; };
const mapStateToProps = (state, { name }) => const mapStateToProps = (state, { name }) =>
@ -124,40 +117,8 @@ const mapStateToProps = (state, { name }) =>
}; };
}; };
const mapDispatchToProps = (dispatch) => const ListPeerContainer = withRoomContext(connect(
{ mapStateToProps
return { )(ListPeer));
onMuteMic : (peerName) =>
{
dispatch(requestActions.mutePeerAudio(peerName));
},
onUnmuteMic : (peerName) =>
{
dispatch(requestActions.unmutePeerAudio(peerName));
},
onEnableWebcam : (peerName) =>
{
dispatch(requestActions.resumePeerVideo(peerName));
},
onDisableWebcam : (peerName) =>
{
dispatch(requestActions.pausePeerVideo(peerName));
},
onEnableScreen : (peerName) =>
{
dispatch(requestActions.resumePeerScreen(peerName));
},
onDisableScreen : (peerName) =>
{
dispatch(requestActions.pausePeerScreen(peerName));
}
};
};
const ListPeerContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ListPeer);
export default ListPeerContainer; export default ListPeerContainer;

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../../appPropTypes';
import * as requestActions from '../../redux/requestActions'; import { withRoomContext } from '../../../RoomContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ListPeer from './ListPeer'; import ListPeer from './ListPeer';
import ListMe from './ListMe'; import ListMe from './ListMe';
const ParticipantList = const ParticipantList =
({ ({
roomClient,
advancedMode, advancedMode,
peers, peers,
setSelectedPeer,
selectedPeerName, selectedPeerName,
spotlights spotlights
}) => ( }) => (
@ -33,7 +33,7 @@ const ParticipantList =
className={classNames('list-item', { className={classNames('list-item', {
selected : peer.name === selectedPeerName selected : peer.name === selectedPeerName
})} })}
onClick={() => setSelectedPeer(peer.name)} onClick={() => roomClient.setSelectedPeer(peer.name)}
> >
<ListPeer name={peer.name} advancedMode={advancedMode} /> <ListPeer name={peer.name} advancedMode={advancedMode} />
</li> </li>
@ -52,7 +52,7 @@ const ParticipantList =
className={classNames('list-item', { className={classNames('list-item', {
selected : peer.name === selectedPeerName selected : peer.name === selectedPeerName
})} })}
onClick={() => setSelectedPeer(peer.name)} onClick={() => roomClient.setSelectedPeer(peer.name)}
> >
<ListPeer name={peer.name} advancedMode={advancedMode} /> <ListPeer name={peer.name} advancedMode={advancedMode} />
</li> </li>
@ -64,9 +64,9 @@ const ParticipantList =
ParticipantList.propTypes = ParticipantList.propTypes =
{ {
roomClient : PropTypes.any.isRequired,
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired, peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
setSelectedPeer : PropTypes.func.isRequired,
selectedPeerName : PropTypes.string, selectedPeerName : PropTypes.string,
spotlights : PropTypes.array.isRequired spotlights : PropTypes.array.isRequired
}; };
@ -82,13 +82,8 @@ const mapStateToProps = (state) =>
}; };
}; };
const mapDispatchToProps = { const ParticipantListContainer = withRoomContext(connect(
setSelectedPeer : requestActions.setSelectedPeer mapStateToProps
}; )(ParticipantList));
const ParticipantListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ParticipantList);
export default ParticipantListContainer; export default ParticipantListContainer;

View File

@ -0,0 +1,123 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from '../../appPropTypes';
import { withRoomContext } from '../../../RoomContext';
import * as stateActions from '../../../redux/stateActions';
import PropTypes from 'prop-types';
import Dropdown from 'react-dropdown';
import ReactTooltip from 'react-tooltip';
const modes = [ {
value : 'democratic',
label : 'Democratic view'
}, {
value : 'filmstrip',
label : 'Filmstrip view'
} ];
const findOption = (options, value) => options.find((option) => option.value === value);
const Settings = ({
roomClient,
room,
me,
onToggleAdvancedMode,
handleChangeMode
}) =>
{
let webcams;
if (me.webcamDevices)
webcams = Array.from(me.webcamDevices.values());
else
webcams = [];
let audioDevices;
let audioDevicesText;
if (me.canChangeAudioDevice)
audioDevicesText = 'Select audio input device';
else
audioDevicesText = 'Unable to select audio input device';
if (me.audioDevices)
audioDevices = Array.from(me.audioDevices.values());
else
audioDevices = [];
return (
<div className='settings'>
<Dropdown
options={webcams}
value={findOption(webcams, me.selectedWebcam)}
onChange={(webcam) => roomClient.changeWebcam(webcam.value)}
placeholder={'Select camera'}
/>
<Dropdown
disabled={!me.canChangeAudioDevice}
options={audioDevices}
value={findOption(audioDevices, me.selectedAudioDevice)}
onChange={(device) => roomClient.changeAudioDevice(device.value)}
placeholder={audioDevicesText}
/>
<ReactTooltip
effect='solid'
/>
<div
data-tip='keyboard shortcut: &lsquo;a&lsquo;'
data-type='dark'
data-place='left'
>
<input
id='room-mode'
type='checkbox'
checked={room.advancedMode}
onChange={onToggleAdvancedMode}
/>
<label htmlFor='room-mode'>Advanced mode</label>
</div>
<div
data-tip='keyboard shortcut: type a digit'
data-type='dark'
data-place='left'
>
<Dropdown
options={modes}
value={findOption(modes, room.mode)}
onChange={(mode) => handleChangeMode(mode.value)}
/>
</div>
</div>
);
};
Settings.propTypes =
{
roomClient : PropTypes.any.isRequired,
me : appPropTypes.Me.isRequired,
room : appPropTypes.Room.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired,
handleChangeMode : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
me : state.me,
room : state.room
};
};
const mapDispatchToProps = {
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
handleChangeMode : stateActions.setDisplayMode
};
const SettingsContainer = withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(Settings));
export default SettingsContainer;

View File

@ -3,10 +3,10 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import * as stateActions from '../../redux/stateActions'; import * as stateActions from '../../redux/stateActions';
import ParticipantList from '../ParticipantList/ParticipantList'; import ParticipantList from './ParticipantList/ParticipantList';
import Chat from '../Chat/Chat'; import Chat from './Chat/Chat';
import Settings from '../Settings'; import Settings from './Settings/Settings';
import FileSharing from '../FileSharing'; import FileSharing from './FileSharing/FileSharing';
import TabHeader from './TabHeader'; import TabHeader from './TabHeader';
class ToolArea extends React.Component class ToolArea extends React.Component

View File

@ -1,77 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as stateActions from '../../redux/stateActions';
class ToolAreaButton extends React.Component
{
render()
{
const {
toolAreaOpen,
toggleToolArea,
unread,
visible
} = this.props;
return (
<div
data-component='ToolAreaButton'
className={classnames('room-controls', {
on : toolAreaOpen,
visible
})}
>
<div
className={classnames('button toolarea-button', {
on : toolAreaOpen
})}
data-tip='Open tools'
data-type='dark'
onClick={() => toggleToolArea()}
/>
{!toolAreaOpen && unread > 0 && (
<span className={classnames('badge', { long: unread >= 10 })}>
{unread}
</span>
)}
</div>
);
}
}
ToolAreaButton.propTypes =
{
toolAreaOpen : PropTypes.bool.isRequired,
toggleToolArea : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired,
visible : PropTypes.bool.isRequired
};
const mapStateToProps = (state) =>
{
return {
toolAreaOpen : state.toolarea.toolAreaOpen,
visible : state.room.toolbarsVisible,
unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleToolArea : () =>
{
dispatch(stateActions.toggleToolArea());
}
};
};
const ToolAreaButtonContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ToolAreaButton);
export default ToolAreaButtonContainer;

View File

@ -2,8 +2,8 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../../redux/stateActions';
import FullView from './FullView'; import FullView from './FullView';
const FullScreenView = (props) => const FullScreenView = (props) =>
@ -31,12 +31,11 @@ const FullScreenView = (props) =>
return ( return (
<div data-component='FullScreenView'> <div data-component='FullScreenView'>
{consumerVisible && !consumer.supported ? <If condition={consumerVisible && !consumer.supported}>
<div className='incompatible-video'> <div className='incompatible-video'>
<p>incompatible video</p> <p>incompatible video</p>
</div> </div>
:null </If>
}
<div className='controls'> <div className='controls'>
<div <div

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import Spinner from 'react-spinner';
export default class FullView extends React.Component export default class FullView extends React.Component
{ {
@ -34,13 +33,6 @@ export default class FullView extends React.Component
autoPlay autoPlay
muted={Boolean(true)} muted={Boolean(true)}
/> />
{videoProfile === 'none' ?
<div className='spinner-container'>
<Spinner />
</div>
:null
}
</div> </div>
); );
} }

View File

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import Spinner from 'react-spinner'; import * as appPropTypes from '../appPropTypes';
import * as appPropTypes from './appPropTypes'; import EditableInput from '../Controls/EditableInput';
import EditableInput from './EditableInput';
export default class PeerView extends React.Component export default class PeerView extends React.Component
{ {
@ -52,30 +51,27 @@ export default class PeerView extends React.Component
return ( return (
<div data-component='PeerView'> <div data-component='PeerView'>
<div className='info'> <div className='info'>
{advancedMode ? <If condition={advancedMode}>
<div className={classnames('media', { 'is-me': isMe })}> <div className={classnames('media', { 'is-me': isMe })}>
<div className='box'> <div className='box'>
{audioCodec ? <If condition={audioCodec}>
<p className='codec'>{audioCodec}</p> <p className='codec'>{audioCodec}</p>
:null </If>
}
{videoCodec ? <If condition={videoCodec}>
<p className='codec'>{videoCodec} {videoProfile}</p> <p className='codec'>{videoCodec} {videoProfile}</p>
:null </If>
}
{(videoVisible && videoWidth !== null) ? <If condition={(videoVisible && videoWidth !== null)}>
<p className='resolution'>{videoWidth}x{videoHeight}</p> <p className='resolution'>{videoWidth}x{videoHeight}</p>
:null </If>
}
</div> </div>
</div> </div>
:null </If>
}
<div className={classnames('peer', { 'is-me': isMe })}> <div className={classnames('peer', { 'is-me': isMe })}>
{isMe ? <Choose>
<When condition={isMe}>
<EditableInput <EditableInput
value={peer.displayName} value={peer.displayName}
propName='displayName' propName='displayName'
@ -90,13 +86,15 @@ export default class PeerView extends React.Component
}} }}
onChange={({ displayName }) => onChangeDisplayName(displayName)} onChange={({ displayName }) => onChangeDisplayName(displayName)}
/> />
: </When>
<Otherwise>
<span className='display-name'> <span className='display-name'>
{peer.displayName} {peer.displayName}
</span> </span>
} </Otherwise>
</Choose>
{advancedMode ? <If condition={advancedMode}>
<div className='row'> <div className='row'>
<span <span
className={classnames('device-icon', peer.device.flag)} className={classnames('device-icon', peer.device.flag)}
@ -105,8 +103,7 @@ export default class PeerView extends React.Component
{peer.device.name} {Math.floor(peer.device.version) || null} {peer.device.name} {Math.floor(peer.device.version) || null}
</span> </span>
</div> </div>
:null </If>
}
</div> </div>
</div> </div>
@ -124,13 +121,6 @@ export default class PeerView extends React.Component
<div className='volume-container'> <div className='volume-container'>
<div className={classnames('bar', `level${volume}`)} /> <div className={classnames('bar', `level${volume}`)} />
</div> </div>
{videoProfile === 'none' ?
<div className='spinner-container'>
<Spinner />
</div>
:null
}
</div> </div>
); );
} }

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import Spinner from 'react-spinner';
export default class ScreenView extends React.Component export default class ScreenView extends React.Component
{ {
@ -41,25 +40,21 @@ export default class ScreenView extends React.Component
return ( return (
<div data-component='ScreenView'> <div data-component='ScreenView'>
<div className='info'> <div className='info'>
{advancedMode ? <If condition={advancedMode}>
<div className={classnames('media', { 'is-me': isMe })}> <div className={classnames('media', { 'is-me': isMe })}>
{screenVisible ? <If condition={screenVisible}>
<div className='box'> <div className='box'>
{screenCodec ? <If condition={screenCodec}>
<p className='codec'>{screenCodec} {screenProfile}</p> <p className='codec'>{screenCodec} {screenProfile}</p>
:null </If>
}
{(screenVisible && screenWidth !== null) ? <If condition={(screenVisible && screenWidth !== null)}>
<p className='resolution'>{screenWidth}x{screenHeight}</p> <p className='resolution'>{screenWidth}x{screenHeight}</p>
:null </If>
}
</div> </div>
:null </If>
}
</div> </div>
:null </If>
}
</div> </div>
<video <video
@ -72,13 +67,6 @@ export default class ScreenView extends React.Component
autoPlay autoPlay
muted={Boolean(true)} muted={Boolean(true)}
/> />
{screenProfile === 'none' ?
<div className='spinner-container'>
<Spinner />
</div>
:null
}
</div> </div>
); );
} }

View File

@ -4,7 +4,7 @@ import NewWindow from './NewWindow';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../redux/stateActions'; import * as stateActions from '../../redux/stateActions';
import FullView from '../FullView'; import FullView from '../VideoContainers/FullView';
const VideoWindow = (props) => const VideoWindow = (props) =>
{ {

View File

@ -76,3 +76,16 @@ export const Message = PropTypes.shape(
text : PropTypes.string, text : PropTypes.string,
sender : PropTypes.string sender : PropTypes.string
}); });
export const FileEntryProps = PropTypes.shape(
{
data : PropTypes.shape({
name : PropTypes.string.isRequired,
picture : PropTypes.string,
file : PropTypes.shape({
magnet : PropTypes.string.isRequired
}).isRequired,
me : PropTypes.bool
}).isRequired,
notify : PropTypes.func.isRequired
});

View File

@ -7,8 +7,9 @@ import { getDeviceInfo } from 'mediasoup-client';
import randomString from 'random-string'; import randomString from 'random-string';
import Logger from './Logger'; import Logger from './Logger';
import * as utils from './utils'; import * as utils from './utils';
import RoomClient from './RoomClient';
import RoomContext from './RoomContext';
import * as cookiesManager from './cookiesManager'; import * as cookiesManager from './cookiesManager';
import * as requestActions from './redux/requestActions';
import * as stateActions from './redux/stateActions'; import * as stateActions from './redux/stateActions';
import Room from './components/Room'; import Room from './components/Room';
import { loginEnabled } from '../config'; import { loginEnabled } from '../config';
@ -16,6 +17,10 @@ import { store } from './store';
const logger = new Logger(); const logger = new Logger();
let roomClient;
RoomClient.init({ store });
domready(() => domready(() =>
{ {
logger.debug('DOM ready'); logger.debug('DOM ready');
@ -93,39 +98,38 @@ function run()
device.version = undefined; device.version = undefined;
} }
// NOTE: I don't like this.
store.dispatch( store.dispatch(
stateActions.setRoomUrl(roomUrl)); stateActions.setRoomUrl(roomUrl));
// NOTE: I don't like this.
store.dispatch( store.dispatch(
stateActions.setMe({ peerName, displayName, displayNameSet, device, loginEnabled })); stateActions.setMe({ peerName, displayName, displayNameSet, device, loginEnabled }));
// NOTE: I don't like this. roomClient = new RoomClient(
store.dispatch( { roomId, peerName, displayName, device, useSimulcast, produce });
requestActions.joinRoom(
{ roomId, peerName, displayName, device, useSimulcast, produce }));
render( render(
<Provider store={store}> <Provider store={store}>
<RoomContext.Provider value={roomClient}>
<Room /> <Room />
</RoomContext.Provider>
</Provider>, </Provider>,
document.getElementById('multiparty-meeting') document.getElementById('multiparty-meeting')
); );
} }
// TODO: Debugging stuff. // TODO: Debugging stuff.
global.CLIENT = roomClient;
setInterval(() => setInterval(() =>
{ {
if (!global.CLIENT._room.peers[0]) if (!roomClient._room.peers[0])
{ {
delete global.CONSUMER; delete global.CONSUMER;
return; return;
} }
const peer = global.CLIENT._room.peers[0]; const peer = roomClient._room.peers[0];
global.CONSUMER = peer.consumers[peer.consumers.length - 1]; global.CONSUMER = peer.consumers[peer.consumers.length - 1];
}, 2000); }, 2000);
@ -134,20 +138,20 @@ global.sendSdp = function()
{ {
logger.debug('---------- SEND_TRANSPORT LOCAL SDP OFFER:'); logger.debug('---------- SEND_TRANSPORT LOCAL SDP OFFER:');
logger.debug( logger.debug(
global.CLIENT._sendTransport._handler._pc.localDescription.sdp); roomClient._sendTransport._handler._pc.localDescription.sdp);
logger.debug('---------- SEND_TRANSPORT REMOTE SDP ANSWER:'); logger.debug('---------- SEND_TRANSPORT REMOTE SDP ANSWER:');
logger.debug( logger.debug(
global.CLIENT._sendTransport._handler._pc.remoteDescription.sdp); roomClient._sendTransport._handler._pc.remoteDescription.sdp);
}; };
global.recvSdp = function() global.recvSdp = function()
{ {
logger.debug('---------- RECV_TRANSPORT REMOTE SDP OFFER:'); logger.debug('---------- RECV_TRANSPORT REMOTE SDP OFFER:');
logger.debug( logger.debug(
global.CLIENT._recvTransport._handler._pc.remoteDescription.sdp); roomClient._recvTransport._handler._pc.remoteDescription.sdp);
logger.debug('---------- RECV_TRANSPORT LOCAL SDP ANSWER:'); logger.debug('---------- RECV_TRANSPORT LOCAL SDP ANSWER:');
logger.debug( logger.debug(
global.CLIENT._recvTransport._handler._pc.localDescription.sdp); roomClient._recvTransport._handler._pc.localDescription.sdp);
}; };

View File

@ -0,0 +1,99 @@
const files = (state = {}, action) =>
{
switch (action.type)
{
case 'ADD_FILE':
{
const { file } = action.payload;
const newFile = {
active : false,
progress : 0,
files : null,
me : false,
...file
};
return { ...state, [file.magnetUri]: newFile };
}
case 'ADD_FILE_HISTORY':
{
const { fileHistory } = action.payload;
const newFileHistory = {};
fileHistory.map((file) =>
{
const newFile = {
active : false,
progress : 0,
files : null,
me : false,
...file
};
newFileHistory[file.magnetUri] = newFile;
});
return { ...state, ...newFileHistory };
}
case 'SET_FILE_ACTIVE':
{
const { magnetUri } = action.payload;
const file = state[magnetUri];
const newFile = { ...file, active: true };
return { ...state, [magnetUri]: newFile };
}
case 'SET_FILE_INACTIVE':
{
const { magnetUri } = action.payload;
const file = state[magnetUri];
const newFile = { ...file, active: false };
return { ...state, [magnetUri]: newFile };
}
case 'SET_FILE_PROGRESS':
{
const { magnetUri, progress } = action.payload;
const file = state[magnetUri];
const newFile = { ...file, progress: progress };
return { ...state, [magnetUri]: newFile };
}
case 'SET_FILE_DONE':
{
const { magnetUri, sharedFiles } = action.payload;
const file = state[magnetUri];
const newFile = {
...file,
files : sharedFiles,
progress : 1,
active : false,
timeout : false
};
return { ...state, [magnetUri]: newFile };
}
case 'REMOVE_FILE':
{
const { magnetUri } = action.payload;
return state.filter((file) => file.magnetUri !== magnetUri);
}
default:
return state;
}
};
export default files;

View File

@ -8,7 +8,7 @@ import notifications from './notifications';
import chatmessages from './chatmessages'; import chatmessages from './chatmessages';
import chatbehavior from './chatbehavior'; import chatbehavior from './chatbehavior';
import toolarea from './toolarea'; import toolarea from './toolarea';
import sharing from './sharing'; import files from './files';
const reducers = combineReducers( const reducers = combineReducers(
{ {
@ -21,7 +21,7 @@ const reducers = combineReducers(
chatmessages, chatmessages,
chatbehavior, chatbehavior,
toolarea, toolarea,
sharing files
}); });
export default reducers; export default reducers;

View File

@ -1,6 +1,4 @@
const initialState = []; const notifications = (state = [], action) =>
const notifications = (state = initialState, action) =>
{ {
switch (action.type) switch (action.type)
{ {

View File

@ -3,6 +3,7 @@ const initialState =
url : null, url : null,
state : 'new', // new/connecting/connected/disconnected/closed, state : 'new', // new/connecting/connected/disconnected/closed,
activeSpeakerName : null, activeSpeakerName : null,
torrentSupport : false,
showSettings : false, showSettings : false,
advancedMode : false, advancedMode : false,
fullScreenConsumer : null, // ConsumerID fullScreenConsumer : null, // ConsumerID
@ -41,6 +42,13 @@ const room = (state = initialState, action) =>
return { ...state, activeSpeakerName: peerName }; return { ...state, activeSpeakerName: peerName };
} }
case 'FILE_SHARING_SUPPORTED':
{
const { supported } = action.payload;
return { ...state, torrentSupport: supported };
}
case 'TOGGLE_SETTINGS': case 'TOGGLE_SETTINGS':
{ {
const showSettings = !state.showSettings; const showSettings = !state.showSettings;

View File

@ -1,233 +1,5 @@
import randomString from 'random-string'; import randomString from 'random-string';
import * as stateActions from './stateActions'; import * as stateActions from './stateActions';
import
{
createNewMessage
} from './reducers/helper';
export const joinRoom = (
{ roomId, peerName, displayName, device, useSimulcast, produce }) =>
{
return {
type : 'JOIN_ROOM',
payload : { roomId, peerName, displayName, device, useSimulcast, produce }
};
};
export const leaveRoom = () =>
{
return {
type : 'LEAVE_ROOM'
};
};
export const changeDisplayName = (displayName) =>
{
return {
type : 'CHANGE_DISPLAY_NAME',
payload : { displayName }
};
};
export const muteMic = () =>
{
return {
type : 'MUTE_MIC'
};
};
export const unmuteMic = () =>
{
return {
type : 'UNMUTE_MIC'
};
};
export const enableWebcam = () =>
{
return {
type : 'ENABLE_WEBCAM'
};
};
export const disableWebcam = () =>
{
return {
type : 'DISABLE_WEBCAM'
};
};
export const changeWebcam = (deviceId) =>
{
return {
type : 'CHANGE_WEBCAM',
payload : { deviceId }
};
};
export const changeAudioDevice = (deviceId) =>
{
return {
type : 'CHANGE_AUDIO_DEVICE',
payload : { deviceId }
};
};
export const enableAudioOnly = () =>
{
return {
type : 'ENABLE_AUDIO_ONLY'
};
};
export const disableAudioOnly = () =>
{
return {
type : 'DISABLE_AUDIO_ONLY'
};
};
export const mutePeerAudio = (peerName) =>
{
return {
type : 'MUTE_PEER_AUDIO',
payload : { peerName }
};
};
export const unmutePeerAudio = (peerName) =>
{
return {
type : 'UNMUTE_PEER_AUDIO',
payload : { peerName }
};
};
export const pausePeerVideo = (peerName) =>
{
return {
type : 'PAUSE_PEER_VIDEO',
payload : { peerName }
};
};
export const resumePeerVideo = (peerName) =>
{
return {
type : 'RESUME_PEER_VIDEO',
payload : { peerName }
};
};
export const pausePeerScreen = (peerName) =>
{
return {
type : 'PAUSE_PEER_SCREEN',
payload : { peerName }
};
};
export const resumePeerScreen = (peerName) =>
{
return {
type : 'RESUME_PEER_SCREEN',
payload : { peerName }
};
};
export const userLogin = () =>
{
return {
type : 'USER_LOGIN'
};
};
export const userLogout = () =>
{
return {
type : 'USER_LOGOUT'
};
};
export const raiseHand = () =>
{
return {
type : 'RAISE_HAND'
};
};
export const lowerHand = () =>
{
return {
type : 'LOWER_HAND'
};
};
export const restartIce = () =>
{
return {
type : 'RESTART_ICE'
};
};
export const enableScreenSharing = () =>
{
return {
type : 'ENABLE_SCREEN_SHARING'
};
};
export const disableScreenSharing = () =>
{
return {
type : 'DISABLE_SCREEN_SHARING'
};
};
export const installExtension = () =>
{
return {
type : 'INSTALL_EXTENSION'
};
};
export const toggleHand = (enable) =>
{
if (enable)
return {
type : 'RAISE_HAND'
};
else
return {
type : 'LOWER_HAND'
};
};
export const sendChatMessage = (text, name, picture) =>
{
const message = createNewMessage(text, 'response', name, picture);
return {
type : 'SEND_CHAT_MESSAGE',
payload : { message }
};
};
export const sendFile = (file, name, picture) =>
{
return {
type : 'SEND_FILE',
payload : { file, name, picture }
};
};
export const setSelectedPeer = (selectedPeerName) =>
{
return {
type : 'REQUEST_SELECTED_PEER',
payload : { selectedPeerName }
};
};
// This returns a redux-thunk action (a function). // This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, timeout }) => export const notify = ({ type = 'info', text, timeout }) =>

View File

@ -1,253 +0,0 @@
import RoomClient from '../RoomClient';
export default ({ dispatch, getState }) => (next) =>
{
let client;
return (action) =>
{
switch (action.type)
{
case 'JOIN_ROOM':
{
const {
roomId,
peerName,
displayName,
device,
useSimulcast,
produce
} = action.payload;
client = new RoomClient(
{
roomId,
peerName,
displayName,
device,
useSimulcast,
produce,
dispatch,
getState
});
// TODO: TMP
global.CLIENT = client;
break;
}
case 'LEAVE_ROOM':
{
client.close();
break;
}
case 'CHANGE_DISPLAY_NAME':
{
const { displayName } = action.payload;
client.changeDisplayName(displayName);
break;
}
case 'MUTE_MIC':
{
client.muteMic();
break;
}
case 'UNMUTE_MIC':
{
client.unmuteMic();
break;
}
case 'ENABLE_WEBCAM':
{
client.enableWebcam();
break;
}
case 'DISABLE_WEBCAM':
{
client.disableWebcam();
break;
}
case 'CHANGE_WEBCAM':
{
const { deviceId } = action.payload;
client.changeWebcam(deviceId);
break;
}
case 'CHANGE_AUDIO_DEVICE':
{
const { deviceId } = action.payload;
client.changeAudioDevice(deviceId);
break;
}
case 'ENABLE_AUDIO_ONLY':
{
client.enableAudioOnly();
break;
}
case 'DISABLE_AUDIO_ONLY':
{
client.disableAudioOnly();
break;
}
case 'MUTE_PEER_AUDIO':
{
const { peerName } = action.payload;
client.mutePeerAudio(peerName);
break;
}
case 'UNMUTE_PEER_AUDIO':
{
const { peerName } = action.payload;
client.unmutePeerAudio(peerName);
break;
}
case 'PAUSE_PEER_VIDEO':
{
const { peerName } = action.payload;
client.pausePeerVideo(peerName);
break;
}
case 'RESUME_PEER_VIDEO':
{
const { peerName } = action.payload;
client.resumePeerVideo(peerName);
break;
}
case 'PAUSE_PEER_SCREEN':
{
const { peerName } = action.payload;
client.pausePeerScreen(peerName);
break;
}
case 'RESUME_PEER_SCREEN':
{
const { peerName } = action.payload;
client.resumePeerScreen(peerName);
break;
}
case 'RAISE_HAND':
{
client.sendRaiseHandState(true);
break;
}
case 'USER_LOGIN':
{
client.login();
break;
}
case 'USER_LOGOUT':
{
client.logout();
break;
}
case 'LOWER_HAND':
{
client.sendRaiseHandState(false);
break;
}
case 'RESTART_ICE':
{
client.restartIce();
break;
}
case 'ENABLE_SCREEN_SHARING':
{
client.enableScreenSharing();
break;
}
case 'DISABLE_SCREEN_SHARING':
{
client.disableScreenSharing();
break;
}
case 'INSTALL_EXTENSION':
{
client.installExtension();
break;
}
case 'SEND_CHAT_MESSAGE':
{
const { message } = action.payload;
client.sendChatMessage(message);
break;
}
case 'SEND_FILE':
{
client.sendFile(action.payload);
break;
}
case 'REQUEST_SELECTED_PEER':
{
const { selectedPeerName } = action.payload;
client.setSelectedPeer(selectedPeerName);
break;
}
}
return next(action);
};
};

View File

@ -70,6 +70,14 @@ export const setWebcamDevices = (devices) =>
}; };
}; };
export const setFileSharingSupported = (supported) =>
{
return {
type : 'FILE_SHARING_SUPPORTED',
payload : { supported }
};
};
export const setDisplayName = (displayName) => export const setDisplayName = (displayName) =>
{ {
return { return {
@ -455,11 +463,11 @@ export const dropMessages = () =>
}; };
}; };
export const addFile = (payload) => export const addFile = (file) =>
{ {
return { return {
type : 'ADD_FILE', type : 'ADD_FILE',
payload payload : { file }
}; };
}; };
@ -471,6 +479,38 @@ export const addFileHistory = (fileHistory) =>
}; };
}; };
export const setFileActive = (magnetUri) =>
{
return {
type : 'SET_FILE_ACTIVE',
payload : { magnetUri }
};
};
export const setFileInActive = (magnetUri) =>
{
return {
type : 'SET_FILE_INACTIVE',
payload : { magnetUri }
};
};
export const setFileProgress = (magnetUri, progress) =>
{
return {
type : 'SET_FILE_PROGRESS',
payload : { magnetUri, progress }
};
};
export const setFileDone = (magnetUri, sharedFiles) =>
{
return {
type : 'SET_FILE_DONE',
payload : { magnetUri, sharedFiles }
};
};
export const setPicture = (picture) => export const setPicture = (picture) =>
({ ({
type : 'SET_PICTURE', type : 'SET_PICTURE',

View File

@ -6,12 +6,10 @@ import {
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger'; import { createLogger } from 'redux-logger';
import reducers from './redux/reducers'; import reducers from './redux/reducers';
import roomClientMiddleware from './redux/roomClientMiddleware';
const reduxMiddlewares = const reduxMiddlewares =
[ [
thunk, thunk
roomClientMiddleware
]; ];
if (process.env.NODE_ENV === 'development') if (process.env.NODE_ENV === 'development')

View File

@ -47,6 +47,7 @@
"@babel/plugin-transform-runtime": "^7.1.0", "@babel/plugin-transform-runtime": "^7.1.0",
"@babel/preset-env": "^7.1.0", "@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"babel-plugin-jsx-control-statements": "^3.2.8",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babelify": "^10.0.0", "babelify": "^10.0.0",
"browser-sync": "^2.26.3", "browser-sync": "^2.26.3",

View File

@ -23,7 +23,7 @@
} }
> .client, > .response { > .client, > .response {
background-color: rgba(#000, 0.1); background-color: var(--chat-message-color);
border-radius: 5px; border-radius: 5px;
max-width: 85%; max-width: 85%;
display: flex; display: flex;
@ -70,8 +70,8 @@
[data-component='Sender'] { [data-component='Sender'] {
display: flex; display: flex;
background-color: #fff; background-color: var(--chat-input-bg-color);
color: #000; color: var(--chat-input-text-color);
flex-shrink: 0; flex-shrink: 0;
margin-top: 0.5rem; margin-top: 0.5rem;
height: 3rem; height: 3rem;
@ -84,7 +84,6 @@
margin-right: 1vmin; margin-right: 1vmin;
border-radius: 0.5vmin; border-radius: 0.5vmin;
padding-left: 1vmin; padding-left: 1vmin;
color: #000;
&.focus { &.focus {
outline: none; outline: none;
@ -95,8 +94,7 @@
width: 20%; width: 20%;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5); box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
border: 0; border: 0;
background-color: #aef; background-color: var(--chat-send-bg-color);
color: #000;
font-size: 1rem; font-size: 1rem;
border-radius: 0.5vmin; border-radius: 0.5vmin;
} }

View File

@ -7,11 +7,15 @@
> .share-file { > .share-file {
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
background: #aef; background: var(--filesharing-bg-color);
padding: 1rem; padding: 1rem;
border-radius: 1vmin; border-radius: 1vmin;
box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5); box-shadow: 0vmin 0vmin 1vmin 0vmin rgba(17,17,17,0.5);
&:hover {
opacity: 0.85;
}
&.disabled { &.disabled {
cursor: not-allowed; cursor: not-allowed;
} }

View File

@ -63,13 +63,13 @@
&.active { &.active {
> .film-content { > .film-content {
border-color: #FFF; border-color: var(--active-speaker-border-color);
} }
} }
&.selected { &.selected {
> .film-content { > .film-content {
border-color: #377EFF; border-color: var(--selected-peer-border-color);
} }
} }
} }

View File

@ -69,37 +69,4 @@
filter: blur(5px); filter: blur(5px);
} }
} }
> .spinner-container {
position: absolute;
top: 0
bottom: 0;
left: 0;
right: 0;
background-color: rgba(#000, 0.75);
.react-spinner {
position: relative;
width: 48px;
height: 48px;
top: 50%;
left: 50%;
.react-spinner_bar {
position: absolute;
width: 20%;
height: 7.8%;
top: -3.9%;
left: -10%;
animation: PeerView-spinner 1.2s linear infinite;
border-radius: 5px;
background-color: rgba(#fff, 0.5);
}
}
}
}
@keyframes FullView-spinner {
0% { opacity: 1; }
100% { opacity: 0.15; }
} }

View File

@ -1,4 +1,4 @@
[data-component='HiddenPeersView'] { [data-component='HiddenPeers'] {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
@ -31,6 +31,8 @@
background-position: bottom; background-position: bottom;
background-size: auto 85%; background-size: auto 85%;
background-repeat: no-repeat; background-repeat: no-repeat;
border: var(--peer-border);
box-shadow: var(--peer-shadow);
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
line-height: 1.8vmin; line-height: 1.8vmin;
@ -39,55 +41,28 @@
animation: none; animation: none;
&.pulse { &.pulse {
animation: pulse 2s; animation: pulse 0.5s;
} }
} }
.view-container>p{ .view-container>p{
transform: translate(0%,50%); transform: translate(0%,50%);
} }
.view-container,
.view-container::before,
.view-container::after {
/* Add shadow to distinguish sheets from one another */
box-shadow: 2px 1px 1px rgba(0,0,0,0.15);
}
.view-container::before,
.view-container::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-color: #2a4b58;
}
/* Second sheet of paper */
.view-container::before {
left: .7vmin;
top: .7vmin;
z-index: -1;
}
/* Third sheet of paper */
.view-container::after {
left: 1.4vmin;
top: 1.4vmin;
z-index: -2;
}
} }
@keyframes pulse { @keyframes pulse {
0% { 0%
box-shadow: 0 0 0 0 rgba(255, 255, 255, 1.0); {
transform: scale3d(1, 1, 1);
} }
70% { 50%
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0); {
transform: scale3d(1.2, 1.2, 1.2);
} }
100% { 100%
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); {
transform: scale3d(1, 1, 1);
} }
} }

View File

@ -1,19 +1,18 @@
[data-component='Logo'] { [data-component='Logo'] {
position: absolute; position: absolute;
height: 4%; width: var(--logo-width);
width: 8%; height: var(--logo-height);
top: 1%; top: 1%;
left: 1%; left: 1%;
z-index: 20; z-index: 20;
background-position: left; background-position: left;
background-size: 100%; background-size: 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
+desktop() { +desktop() {
opacity: 1; opacity: 1;
} }
+mobile() { background-image: var(--logo);
}
background-image: url('/resources/images/logo.svg');
background-size: contain; background-size: contain;
} }

View File

@ -6,8 +6,8 @@
> .view-container { > .view-container {
position: relative; position: relative;
width: 20vmin; width: var(--me-width);
height: 15vmin; height: var(--me-height);
&.webcam { &.webcam {
order: 2; order: 2;
@ -23,9 +23,7 @@
right: 0; right: 0;
top: 0; top: 0;
display: flex; display: flex;
flex-direction:; row; flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 0.4vmin; padding: 0.4vmin;
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
@ -41,28 +39,20 @@
background-position: center; background-position: center;
background-size: 75%; background-size: 75%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: rgba(#000, 0.5); background-color: var(--media-control-button-color);
cursor: pointer; cursor: pointer;
opacity: 0; opacity: 0;
transition-property: opacity, background-color; transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
width: var(--media-control-button-size);
&.visible { height: var(--media-control-button-size);
opacity: 0.85;
}
+desktop() {
width: 24px;
height: 24px;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
}
+mobile() { &.visible {
width: 22px; opacity: 0.85;
height: 22px;
} }
&.unsupported { &.unsupported {
@ -71,11 +61,15 @@
&.disabled { &.disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; background-color: var(--media-control-botton-disabled);
} }
&.on { &.on {
background-color: rgba(#fff, 0.7); background-color: var(--media-control-botton-on);
}
&.off {
background-color: var(--media-control-botton-off);
} }
&.mic { &.mic {
@ -85,7 +79,6 @@
&.off { &.off {
background-image: url('/resources/images/icon_remote_mic_white_off.svg'); background-image: url('/resources/images/icon_remote_mic_white_off.svg');
background-color: rgba(#d42241, 0.7);
} }
&.unsupported { &.unsupported {
@ -100,33 +93,12 @@
&.off { &.off {
background-image: url('/resources/images/icon_remote_webcam_white_off.svg'); background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
background-color: rgba(#d42241, 0.7);
} }
&.unsupported { &.unsupported {
background-image: url('/resources/images/icon_webcam_white_unsupported.svg'); background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
} }
} }
&.screen {
&.on {
background-image: url('/resources/images/share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
}
}
&.fullscreen {
background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7);
}
} }
} }
} }

View File

@ -84,8 +84,8 @@
} }
&.info { &.info {
background-color: rgba(#0a1d26, 0.75); background-color: var(--notification-info-bg-color);
color: rgba(#fff, 0.65); color: var(--notification-info-text-color);
>.icon { >.icon {
opacity: 0.65; opacity: 0.65;
@ -94,8 +94,8 @@
} }
&.error { &.error {
background-color: rgba(#ff1914, 0.65); background-color: var(--notification-error-bg-color);
color: rgba(#fff, 0.85); color: var(--notification-error-text-color);
>.icon { >.icon {
opacity: 0.85; opacity: 0.85;

View File

@ -51,21 +51,13 @@
background-color: rgba(#000, 0.5); background-color: rgba(#000, 0.5);
transition-property: opacity, background-color; transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
width: var(--media-control-button-size);
+desktop() { height: var(--media-control-button-size);
width: 24px;
height: 24px;
opacity: 0.85; opacity: 0.85;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
}
+mobile() {
width: 22px;
height: 22px;
}
&.on { &.on {
opacity: 1; opacity: 1;
@ -102,19 +94,20 @@
position: absolute; position: absolute;
bottom: 0px; bottom: 0px;
&.level0 { height: 0; background-color: rgba(#000, 0.8); } &.level0 { height: 0; }
&.level1 { height: 0.2vh; background-color: rgba(#000, 0.8); } &.level1 { height: 0.2vh; }
&.level2 { height: 0.4vh; background-color: rgba(#000, 0.8); } &.level2 { height: 0.4vh; }
&.level3 { height: 0.6vh; background-color: rgba(#000, 0.8); } &.level3 { height: 0.6vh; }
&.level4 { height: 0.8vh; background-color: rgba(#000, 0.8); } &.level4 { height: 0.8vh; }
&.level5 { height: 1.0vh; background-color: rgba(#000, 0.8); } &.level5 { height: 1.0vh; }
&.level6 { height: 1.2vh; background-color: rgba(#000, 0.8); } &.level6 { height: 1.2vh; }
&.level7 { height: 1.4vh; background-color: rgba(#000, 0.8); } &.level7 { height: 1.4vh; }
&.level8 { height: 1.6vh; background-color: rgba(#000, 0.8); } &.level8 { height: 1.6vh; }
&.level9 { height: 1.8vh; background-color: rgba(#000, 0.8); } &.level9 { height: 1.8vh; }
&.level10 { height: 2.0vh; background-color: rgba(#000, 0.8); } &.level10 { height: 2.0vh; }
} }
} }
> .controls { > .controls {
float: right; float: right;
display: flex; display: flex;
@ -133,21 +126,13 @@
cursor: pointer; cursor: pointer;
transition-property: opacity, background-color; transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
width: var(--media-control-button-size);
+desktop() { height: var(--media-control-button-size);
width: 24px;
height: 24px;
opacity: 0.85; opacity: 0.85;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
}
+mobile() {
width: 22px;
height: 22px;
}
&.unsupported { &.unsupported {
pointer-events: none; pointer-events: none;
@ -155,11 +140,15 @@
&.disabled { &.disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; background-color: var(--media-control-botton-disabled);
} }
&.on { &.on {
background-color: rgba(#fff, 0.7); background-color: var(--media-control-botton-on);
}
&.off {
background-color: var(--media-control-botton-off);
} }
&.mic { &.mic {
@ -169,7 +158,6 @@
&.off { &.off {
background-image: url('/resources/images/icon_remote_mic_white_off.svg'); background-image: url('/resources/images/icon_remote_mic_white_off.svg');
background-color: rgba(#d42241, 0.7);
} }
&.unsupported { &.unsupported {
@ -184,7 +172,6 @@
&.off { &.off {
background-image: url('/resources/images/icon_remote_webcam_white_off.svg'); background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
background-color: rgba(#d42241, 0.7);
} }
&.unsupported { &.unsupported {
@ -199,7 +186,6 @@
&.off { &.off {
background-image: url('/resources/images/no-share-screen-white.svg'); background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
} }
&.unsupported { &.unsupported {

View File

@ -54,21 +54,13 @@
background-color: rgba(#000, 0.5); background-color: rgba(#000, 0.5);
transition-property: opacity, background-color; transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
width: var(--media-control-button-size);
+desktop() { height: var(--media-control-button-size);
width: 24px;
height: 24px;
opacity: 0.85; opacity: 0.85;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
}
+mobile() {
width: 22px;
height: 22px;
}
&.on { &.on {
opacity: 1; opacity: 1;
@ -107,25 +99,17 @@
background-position: center; background-position: center;
background-size: 75%; background-size: 75%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: rgba(#000, 0.5); background-color: var(--media-control-button-color);
cursor: pointer; cursor: pointer;
transition-property: opacity, background-color; transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
width: var(--media-control-button-size);
+desktop() { height: var(--media-control-button-size);
width: 24px;
height: 24px;
opacity: 0.85; opacity: 0.85;
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
}
+mobile() {
width: 22px;
height: 22px;
}
&.unsupported { &.unsupported {
pointer-events: none; pointer-events: none;
@ -133,11 +117,15 @@
&.disabled { &.disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; background-color: var(--media-control-botton-disabled);
} }
&.on { &.on {
background-color: rgba(#fff, 0.7); background-color: var(--media-control-botton-on);
}
&.off {
background-color: var(--media-control-botton-off);
} }
&.mic { &.mic {
@ -147,7 +135,6 @@
&.off { &.off {
background-image: url('/resources/images/icon_remote_mic_white_off.svg'); background-image: url('/resources/images/icon_remote_mic_white_off.svg');
background-color: rgba(#d42241, 0.7);
} }
&.unsupported { &.unsupported {
@ -155,21 +142,6 @@
} }
} }
&.webcam {
&.on {
background-image: url('/resources/images/icon_webcam_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
}
}
&.screen { &.screen {
&.on { &.on {
background-image: url('/resources/images/share-screen-black.svg'); background-image: url('/resources/images/share-screen-black.svg');
@ -177,7 +149,6 @@
&.off { &.off {
background-image: url('/resources/images/no-share-screen-white.svg'); background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
} }
&.unsupported { &.unsupported {
@ -187,12 +158,10 @@
&.fullscreen { &.fullscreen {
background-image: url('/resources/images/icon_fullscreen_black.svg'); background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7);
} }
&.newwindow { &.newwindow {
background-image: url('/resources/images/icon_new_window_black.svg'); background-image: url('/resources/images/icon_new_window_black.svg');
background-color: rgba(#fff, 0.7);
} }
} }
} }

View File

@ -6,15 +6,13 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
background-color: rgba(#2a4b58, 0.9); background-color: var(--peer-bg-color);
background-image: url('/resources/images/buddy.svg'); background-image: var(--peer-empty-avatar);
background-position: bottom; background-position: bottom;
background-size: auto 85%; background-size: auto 85%;
background-repeat: no-repeat; background-repeat: no-repeat;
> .info { > .info {
$backgroundTint = #000;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
top: 0.6vmin; top: 0.6vmin;
@ -178,7 +176,7 @@
user-select: none; user-select: none;
transition-property: opacity; transition-property: opacity;
transition-duration: .15s; transition-duration: .15s;
background-color: rgba(#000, 0.75); background-color: var(--peer-video-bg-color);
&.is-me { &.is-me {
transform: scaleX(-1); transform: scaleX(-1);
@ -225,37 +223,4 @@
&.level10 { height: 100%; background-color: rgba(#000, 0.65); } &.level10 { height: 100%; background-color: rgba(#000, 0.65); }
} }
} }
> .spinner-container {
position: absolute;
top: 0
bottom: 0;
left: 0;
right: 0;
background-color: rgba(#000, 0.75);
.react-spinner {
position: relative;
width: 48px;
height: 48px;
top: 50%;
left: 50%;
.react-spinner_bar {
position: absolute;
width: 20%;
height: 7.8%;
top: -3.9%;
left: -10%;
animation: PeerView-spinner 1.2s linear infinite;
border-radius: 5px;
background-color: rgba(#fff, 0.5);
}
}
}
}
@keyframes PeerView-spinner {
0% { opacity: 1; }
100% { opacity: 0.15; }
} }

View File

@ -31,18 +31,18 @@
+desktop() { +desktop() {
flex: 0 0 auto; flex: 0 0 auto;
margin: 6px; margin: 6px;
border: 1px solid rgba(#fff, 0.15); border: var(--peer-border);
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5); box-shadow: var(--peer-shadow);
transition-property: border-color; transition-property: border-color;
transition-duration: 0.15s; transition-duration: 0.15s;
&.active-speaker { &.active-speaker {
border-color: #fff; border-color: var(--active-speaker-border-color);
} }
&.selected { &.selected {
> .peer-content { > .peer-content {
border: 1px solid #377eff; border-color: var(--selected-peer-border-color);
} }
} }
} }

View File

@ -143,24 +143,15 @@
position: fixed; position: fixed;
z-index: 110; z-index: 110;
overflow: hidden; overflow: hidden;
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5); box-shadow: var(--me-shadow);
transition-property: border-color; transition-property: border-color;
transition-duration: 0.15s; transition-duration: 0.15s;
&.active-speaker {
border-color: #fff;
}
+desktop() {
top: 6%; top: 6%;
left:1%; left:1%;
border: 1px solid rgba(#fff, 0.15); border: var(--me-border);
}
+mobile() { &.active-speaker {
top: 6%; border-color: var(--active-speaker-border-color);
left: 1%;
border: 1px solid rgba(#fff, 0.25);
} }
} }
} }

View File

@ -6,15 +6,13 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
background-color: rgba(#2a4b58, 0.9); background-color: var(--peer-bg-color);
background-image: url('/resources/images/buddy.svg'); background-image: var(--peer-empty-avatar);
background-position: bottom; background-position: bottom;
background-size: auto 85%; background-size: auto 85%;
background-repeat: no-repeat; background-repeat: no-repeat;
> .info { > .info {
$backgroundTint = #000;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
top: 0.6vmin; top: 0.6vmin;
@ -58,7 +56,7 @@
user-select: none; user-select: none;
transition-property: opacity; transition-property: opacity;
transition-duration: .15s; transition-duration: .15s;
background-color: rgba(#000, 0.75); background-color: var(--peer-video-bg-color);
&.is-me { &.is-me {
transform: scaleX(-1); transform: scaleX(-1);
@ -73,37 +71,4 @@
filter: blur(5px); filter: blur(5px);
} }
} }
> .spinner-container {
position: absolute;
top: 0
bottom: 0;
left: 0;
right: 0;
background-color: rgba(#000, 0.75);
.react-spinner {
position: relative;
width: 48px;
height: 48px;
top: 50%;
left: 50%;
.react-spinner_bar {
position: absolute;
width: 20%;
height: 7.8%;
top: -3.9%;
left: -10%;
animation: PeerView-spinner 1.2s linear infinite;
border-radius: 5px;
background-color: rgba(#fff, 0.5);
}
}
}
}
@keyframes ScreenView-spinner {
0% { opacity: 1; }
100% { opacity: 0.15; }
} }

View File

@ -1,3 +0,0 @@
[data-component='Settings'] {
}

View File

@ -1,8 +1,8 @@
[data-component='Sidebar'] { [data-component='Sidebar'] {
position: fixed; position: fixed;
z-index: 500; z-index: 500;
top: calc(50% - 60px); top: 50%;
height: 120px; transform: translate(0%, -50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -24,7 +24,7 @@
background-position: center; background-position: center;
background-size: 75%; background-size: 75%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: rgba(#fff, 0.3); background-color: var(--circle-button-color);
cursor: pointer; cursor: pointer;
transition-property: opacity, background-color; transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
@ -32,24 +32,20 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: var(--circle-button-size);
+desktop() { width: var(--circle-button-size);
height: 2.5em;
width: 2.5em;
}
+mobile() {
height: 2.5em;
width: 2.5em;
}
&.on { &.on {
background-color: rgba(#fff, 0.7); background-color: var(--circle-button-toggled-color);
}
&.unsupported {
background-color: var(--circle-button-unsupported-color);
} }
&.disabled { &.disabled {
pointer-events: none; pointer-events: none;
opacity: 0.5; background-color: var(--circle-button-diabled-color);
} }
&.login { &.login {
@ -93,7 +89,6 @@
&.unsupported { &.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg'); background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
} }
&.need-extension { &.need-extension {

View File

@ -19,12 +19,6 @@
} }
} }
[data-component='ToolAreaButton'] {
&.on {
right: 80%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 80%; width: 80%;
@ -33,35 +27,6 @@
.toolarea-shade.open { .toolarea-shade.open {
display: block; display: block;
} }
> .button {
background-position: center;
background-size: 100%;
background-repeat: no-repeat;
background-color: rgba(#aef);
cursor: pointer;
border-radius: 15%;
padding: 1px;
+desktop() {
height: 36px;
width: 18px;
}
+mobile() {
height: 32px;
width: 16px;
}
&.toolarea-close-button {
background-image: url('/resources/images/arrow_right.svg');
position: absolute;
top: 50%;
left: -22px;
display: none;
&.on {
display: block;
}
}
}
> .toolarea-button { > .toolarea-button {
text-align: center; text-align: center;
@ -153,12 +118,6 @@
} }
@media (min-width: 600px) { @media (min-width: 600px) {
[data-component='ToolAreaButton'] {
&.on {
right: 60%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 60%; width: 60%;
@ -167,12 +126,6 @@
} }
@media (min-width: 900px) { @media (min-width: 900px) {
[data-component='ToolAreaButton'] {
&.on {
right: 40%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 40%; width: 40%;
@ -181,12 +134,6 @@
} }
@media (min-width: 1500px) { @media (min-width: 1500px) {
[data-component='ToolAreaButton'] {
&.on {
right: 25%;
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
&.open { &.open {
width: 25%; width: 25%;
@ -194,79 +141,6 @@
} }
} }
[data-component='ToolAreaButton'] {
position: absolute;
z-index: 1020;
right: 0;
height: 36px;
width: 36px;
margin: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: right 0.3s;
> .button {
flex: 0 0 auto;
margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
border-radius: 100%;
+desktop() {
height: 36px;
width: 36px;
}
+mobile() {
height: 32px;
width: 32px;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.toolarea-button {
background-image: url('/resources/images/icon_tool_area_white.svg');
&.on {
background-image: url('/resources/images/icon_tool_area_black.svg');
}
}
}
> .badge {
border-radius: 50%;
font-size: 1rem;
background: #b12525;
color: #fff;
text-align: center;
margin-top: -8px;
line-height: 1rem;
margin-right: -8px;
position: absolute;
padding: 0.2rem 0.4rem;
top: 0;
right: 0;
&.long {
border-radius: 25% / 50%;
}
}
}
[data-component='ToolArea'] { [data-component='ToolArea'] {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -7,6 +7,53 @@ global-reset();
@import './reset'; @import './reset';
@import './keyframes'; @import './keyframes';
:root {
--logo: url('/resources/images/logo.svg');
--logo-width: 8%;
--logo-height: 4%;
--background: url('/resources/images/background.svg');
--background-color: rgba(51, 51, 51, 1.0);
--circle-button-color: rgba(255, 255, 255, 0.3);
--circle-button-toggled-color: rgba(255, 255, 255, 0.7);
--circle-button-unsupported-color: rgba(212, 34, 65, 0.7);
--circle-button-diabled-color: rgba(255, 255, 255, 0.5);
--circle-button-size: 2.5em;
--media-control-button-color: rgba(255, 255, 255, 0.85);
--media-control-botton-on: rgba(255, 255, 255, 0.7);
--media-control-botton-off: rgba(212, 34, 65, 0.7);
--media-control-botton-disabled: rgba(255, 255, 255, 0.5)
--media-control-button-size: 1.5em;
--me-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5);
--me-border: 1px solid rgba(255, 255, 255, 0.15);
--me-width: 20vmin;
--me-height: 15vmin;
--peer-shadow: 0px 5px 12px 2px rgba(17, 17, 17, 0.5);
--peer-border: 1px solid rgba(255, 255, 255, 0.15);
--peer-empty-avatar: url('/resources/images/buddy.svg');
--peer-bg-color: rgba(42, 75, 88, 0.9);
--peer-video-bg-color: rgba(0, 0, 0, 0.75);
--chat-message-color: rgba(0, 0, 0, 0.1);
--chat-input-bg-color: rgba(255, 255, 255, 1.0);
--chat-input-text-color: rgba(0, 0, 0, 1.0);
--chat-send-bg-color: rgba(170, 238, 255, 1.0);
--filesharing-bg-color: rgba(170, 238, 255, 1.0);
--notification-info-bg-color: rgba(10, 29, 38, 0.75);
--notification-info-text-color: rgba(255, 255, 255, 0.65);
--notification-error-bg-color: rgba(255, 25, 20, 0.65);
--notification-error-text-color: rgba(255, 255, 255, 0.85);
--active-speaker-border-color: rgba(255, 255, 255, 1.0);
--selected-peer-border-color: rgba(55, 126, 255, 1.0);
}
html { html {
height: 100%; height: 100%;
font-family: 'Roboto'; font-family: 'Roboto';
@ -23,11 +70,11 @@ body {
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
overflow-y: hidden; overflow-y: hidden;
background-color: #333; background-color: var(--background-color);
+desktop() { +desktop() {
font-size: 16px; font-size: 16px;
background-image: url('/resources/images/background.svg'); background-image: var(--background);
background-attachment: fixed; background-attachment: fixed;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
@ -48,11 +95,10 @@ body {
@import './components/Peers'; @import './components/Peers';
@import './components/Peer'; @import './components/Peer';
@import './components/PeerView'; @import './components/PeerView';
@import './components/HiddenPeersView'; @import './components/HiddenPeers';
@import './components/ScreenView'; @import './components/ScreenView';
@import './components/Notifications'; @import './components/Notifications';
@import './components/Chat'; @import './components/Chat';
@import './components/Settings';
@import './components/ToolArea'; @import './components/ToolArea';
@import './components/ParticipantList'; @import './components/ParticipantList';
@import './components/FullScreenView'; @import './components/FullScreenView';

View File

@ -1,401 +0,0 @@
/* eslint-disable key-spacing */
exports.ROOM_OPTIONS =
{
requestTimeout: 10000,
transportOptions:
{
tcp: false
},
__turnServers:
[
{
urls: [ 'turn:worker2.versatica.com:3478?transport=udp' ],
username: 'testuser1',
credential: 'testpasswd1'
}
],
hidden: false
};
exports.ROOM_RTP_CAPABILITIES =
{
codecs:
[
{
name: 'PCMA',
mimeType: 'audio/PCMA',
kind: 'audio',
clockRate: 8000,
preferredPayloadType: 8,
rtcpFeedback: [],
parameters: {}
},
{
name: 'opus',
mimeType: 'audio/opus',
kind: 'audio',
clockRate: 48000,
channels: 2,
preferredPayloadType: 96,
rtcpFeedback: [],
parameters: {}
},
{
name: 'SILK',
mimeType: 'audio/SILK',
kind: 'audio',
clockRate: 16000,
preferredPayloadType: 97,
rtcpFeedback: [],
parameters: {}
},
{
name: 'VP9',
mimeType: 'video/VP9',
kind: 'video',
clockRate: 90000,
preferredPayloadType: 102,
rtcpFeedback:
[
{
parameter: '',
type: 'nack'
},
{
parameter: 'pli',
type: 'nack'
},
{
parameter: '',
type: 'goog-remb'
},
{
parameter: 'bar',
type: 'foo'
}
],
parameters: {}
},
{
name: 'rtx',
mimeType: 'video/rtx',
kind: 'video',
clockRate: 90000,
preferredPayloadType: 103,
rtcpFeedback: [],
parameters: {
apt: 102
}
},
{
name: 'VP8',
mimeType: 'video/VP8',
kind: 'video',
clockRate: 90000,
preferredPayloadType: 100,
rtcpFeedback:
[
{
parameter: '',
type: 'nack'
},
{
parameter: 'pli',
type: 'nack'
},
{
parameter: '',
type: 'goog-remb'
},
{
parameter: 'bar',
type: 'foo'
}
],
parameters: {}
},
{
name: 'rtx',
mimeType: 'video/rtx',
kind: 'video',
clockRate: 90000,
preferredPayloadType: 101,
rtcpFeedback: [],
parameters: {
apt: 100
}
}
],
headerExtensions: [
{
kind: 'audio',
uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level',
preferredId: 10
},
{
kind: 'video',
uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time',
preferredId: 11
},
{
kind: 'video',
uri: 'http://foo.bar',
preferredId: 12
}
],
fecMechanisms: []
};
exports.QUERY_ROOM_RESPONSE =
{
rtpCapabilities: exports.ROOM_RTP_CAPABILITIES
};
exports.JOIN_ROOM_RESPONSE =
{
peers:
[
{
name: 'alice',
appData: 'Alice iPad Pro',
consumers:
[
{
id: 3333,
kind: 'audio',
paused: false,
appData: 'ALICE_MIC',
rtpParameters:
{
muxId: null,
codecs:
[
{
name: 'PCMA',
mimeType: 'audio/PCMA',
clockRate: 8000,
payloadType: 8,
rtcpFeedback: [],
parameters: {}
}
],
headerExtensions:
[
{
uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level',
id: 1
}
],
encodings:
[
{
ssrc: 33333333
}
],
rtcp:
{
cname: 'ALICECNAME',
reducedSize: true,
mux: true
}
}
}
]
},
{
name: 'bob',
appData: 'Bob HP Laptop',
consumers:
[
{
id: 6666,
kind: 'audio',
paused: false,
appData: 'BOB_MIC',
rtpParameters:
{
muxId: null,
codecs:
[
{
name: 'opus',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2,
payloadType: 96,
rtcpFeedback: [],
parameters: {}
}
],
headerExtensions:
[
{
uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level',
id: 1
}
],
encodings:
[
{
ssrc: 66666666
}
],
rtcp:
{
cname: 'BOBCNAME',
reducedSize: true,
mux: true
}
}
}
]
}
]
};
exports.CREATE_TRANSPORT_1_RESPONSE =
{
iceParameters:
{
usernameFragment: 'server-usernamefragment-12345678',
password: 'server-password-xxxxxxxx',
iceLite: true
},
iceCandidates:
[
{
foundation: 'F1',
priority: 1234,
ip: '1.2.3.4',
protocol: 'udp',
port: 9999,
type: 'host'
}
],
dtlsParameters:
{
fingerprints:
[
{
algorithm: 'sha-256',
value: 'FF:FF:39:66:A4:E2:66:60:30:18:A7:59:B3:AF:A5:33:58:5E:7F:69:A4:62:A6:D4:EB:9F:B7:42:05:35:FF:FF'
}
],
role: 'client'
}
};
exports.CREATE_TRANSPORT_2_RESPONSE =
{
iceParameters:
{
usernameFragment: 'server-usernamefragment-12345678',
password: 'server-password-xxxxxxxx',
iceLite: true
},
iceCandidates:
[
{
foundation: 'F1',
priority: 1234,
ip: '1.2.3.4',
protocol: 'udp',
port: 9999,
type: 'host'
}
],
dtlsParameters:
{
fingerprints:
[
{
algorithm: 'sha-256',
value: 'FF:FF:39:66:A4:E2:66:60:30:18:A7:59:B3:AF:A5:33:58:5E:7F:69:A4:62:A6:D4:EB:9F:B7:42:05:35:FF:FF'
}
],
role: 'auto'
}
};
exports.ALICE_WEBCAM_NEW_CONSUMER_NOTIFICATION =
{
method: 'newConsumer',
notification: true,
id: 4444,
peerName: 'alice',
kind: 'video',
paused: true,
appData: 'ALICE_WEBCAM',
rtpParameters:
{
muxId: null,
codecs:
[
{
name: 'VP8',
mimeType: 'video/VP8',
clockRate: 90000,
payloadType: 100,
rtcpFeedback:
[
{
parameter: '',
type: 'nack'
},
{
parameter: 'pli',
type: 'nack'
},
{
parameter: '',
type: 'goog-remb'
},
{
parameter: 'bar',
type: 'foo'
}
],
parameters: {}
},
{
name: 'rtx',
mimeType: 'video/rtx',
clockRate: 90000,
payloadType: 101,
rtcpFeedback: [],
parameters: {
apt: 100
}
}
],
headerExtensions:
[
{
kind: 'video',
uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time',
id: 11
},
{
kind: 'video',
uri: 'http://foo.bar',
id: 12
}
],
encodings:
[
{
ssrc: 444444441,
rtx: {
ssrc: 444444442
}
}
],
rtcp:
{
cname: 'ALICECNAME',
reducedSize: true,
mux: true
}
}
};

View File

@ -1,145 +0,0 @@
const path = require('path');
const gulp = require('gulp');
const gutil = require('gulp-util');
const plumber = require('gulp-plumber');
const rename = require('gulp-rename');
const browserify = require('browserify');
const watchify = require('watchify');
const envify = require('envify/custom');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const eslint = require('gulp-eslint');
const browserSync = require('browser-sync');
const OUTPUT_DIR = 'output';
const APP_NAME = 'mediasoup-client-test';
// Node environment.
process.env.NODE_ENV = 'development';
function logError(error)
{
gutil.log(gutil.colors.red(error.stack));
}
gulp.task('lint', () =>
{
const src =
[
'gulpfile.js',
'**/*.js',
'**/*.jsx'
];
return gulp.src(src)
.pipe(plumber())
.pipe(eslint())
.pipe(eslint.format());
});
gulp.task('html', () =>
{
return gulp.src('index.html')
.pipe(gulp.dest(OUTPUT_DIR));
});
gulp.task('bundle', () =>
{
const watch = true;
let bundler = browserify(
{
entries : 'index.jsx',
extensions : [ '.js', '.jsx' ],
// required for sourcemaps (must be false otherwise).
debug : process.env.NODE_ENV === 'development',
// required for watchify.
cache : {},
// required for watchify.
packageCache : {},
// required to be true only for watchify.
fullPaths : watch
})
.transform('babelify',
{
presets : [ 'es2015', 'es2017', 'react' ],
plugins :
[
'transform-runtime',
'transform-object-assign',
'transform-object-rest-spread'
]
})
.transform(envify(
{
NODE_ENV : process.env.NODE_ENV,
_ : 'purge'
}));
if (watch)
{
bundler = watchify(bundler);
bundler.on('update', () =>
{
const start = Date.now();
gutil.log('bundling...');
rebundle();
gutil.log('bundle took %sms', (Date.now() - start));
});
}
function rebundle()
{
return bundler.bundle()
.on('error', logError)
.pipe(plumber())
.pipe(source(`${APP_NAME}.js`))
.pipe(buffer())
.pipe(rename(`${APP_NAME}.js`))
.pipe(gulp.dest(OUTPUT_DIR));
}
return rebundle();
});
gulp.task('livebrowser', (done) =>
{
browserSync(
{
server :
{
baseDir : OUTPUT_DIR
},
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
});
done();
});
gulp.task('watch', (done) =>
{
// Watch changes in HTML.
gulp.watch([ 'index.html' ], gulp.series(
'html'
));
// Watch changes in JS files.
gulp.watch([ 'gulpfile.js', '**/*.js', '**/*.jsx' ], gulp.series(
'lint'
));
done();
});
gulp.task('live', gulp.series(
'lint',
'html',
'bundle',
'watch',
'livebrowser'
));
gulp.task('default', gulp.series('live'));

View File

@ -1,16 +0,0 @@
<!doctype html>
<html>
<head>
<title>mediasoup-client test</title>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
<meta name='description' content='mediasoup-client test'>
<script async src='/mediasoup-client-test.js'></script>
</head>
<body>
<h1>mediasoup-client test</h1>
</body>
</html>

View File

@ -1,692 +0,0 @@
import * as mediasoupClient from 'mediasoup-client';
import domready from 'domready';
import Logger from '../lib/Logger';
const DATA = require('./DATA');
window.mediasoupClient = mediasoupClient;
const logger = new Logger();
const SEND = true;
const SEND_AUDIO = true;
const SEND_VIDEO = false;
const RECV = true;
domready(() =>
{
logger.debug('DOM ready');
run();
});
function run()
{
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
let transport1;
let transport2;
let audioTrack;
let videoTrack;
let audioProducer1;
let audioProducer2;
let videoProducer;
logger.debug('calling room = new mediasoupClient.Room()');
// const room = new mediasoupClient.Room();
const room = new mediasoupClient.Room(DATA.ROOM_OPTIONS);
window.room = room;
room.on('closed', (originator, appData) =>
{
logger.warn(
'room "closed" event [originator:%s, appData:%o]', originator, appData);
});
room.on('request', (request, callback, errback) =>
{
logger.warn('sending request [method:%s]:%o', request.method, request);
switch (request.method)
{
case 'queryRoom':
{
setTimeout(() =>
{
callback(DATA.QUERY_ROOM_RESPONSE);
errback('upppps');
}, 200);
break;
}
case 'joinRoom':
{
setTimeout(() =>
{
callback(DATA.JOIN_ROOM_RESPONSE);
// errback('upppps');
}, 200);
break;
}
case 'createTransport':
{
setTimeout(() =>
{
switch (request.appData)
{
case 'TRANSPORT_1':
callback(DATA.CREATE_TRANSPORT_1_RESPONSE);
break;
case 'TRANSPORT_2':
callback(DATA.CREATE_TRANSPORT_2_RESPONSE);
break;
default:
errback('upppps');
}
}, 250);
break;
}
case 'createProducer':
{
setTimeout(() =>
{
callback();
}, 250);
break;
}
case 'enableConsumer':
{
setTimeout(() =>
{
callback();
}, 500);
break;
}
default:
errback(`NO IDEA ABOUT REQUEST METHOD "${request.method}"`);
}
});
room.on('notify', (notification) =>
{
logger.warn(
'sending notification [method:%s]:%o', notification.method, notification);
switch (notification.method)
{
case 'leaveRoom':
case 'updateTransport':
case 'closeTransport':
case 'closeProducer':
case 'pauseProducer':
case 'resumeProducer':
case 'pauseConsumer':
case 'resumeConsumer':
break;
default:
logger.error(`NO IDEA ABOUT NOTIFICATION METHOD "${notification.method}"`);
}
});
room.on('newpeer', (peer) =>
{
logger.warn('room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
handlePeer(peer);
});
Promise.resolve()
.then(() =>
{
logger.debug('calling room.join()');
const deviceInfo = mediasoupClient.getDeviceInfo();
const appData =
{
device : `${deviceInfo.name} ${deviceInfo.version}`
};
return room.join(null, appData);
// return room.join(DATA.ROOM_RTP_CAPABILITIES, appData);
})
.then((peers) =>
{
if (!RECV)
return;
logger.debug('room.join() succeeded');
logger.debug('calling transport2 = room.createTransport("recv")');
transport2 = room.createTransport('recv', 'TRANSPORT_2');
window.transport2 = transport2;
window.pc2 = transport2._handler._pc;
handleTransport(transport2);
for (const peer of peers)
{
handlePeer(peer);
}
})
.then(() =>
{
if (!SEND)
return;
if (room.canSend('audio'))
logger.debug('can send audio');
else
logger.warn('cannot send audio');
if (room.canSend('video'))
logger.debug('can send video');
else
logger.warn('cannot send video');
logger.debug('calling transport1 = room.createTransport("send")');
transport1 = room.createTransport('send', 'TRANSPORT_1');
window.transport1 = transport1;
window.pc1 = transport1._handler._pc;
handleTransport(transport1);
logger.debug('calling getUserMedia()');
return navigator.mediaDevices
.getUserMedia({ audio: SEND_AUDIO, video: SEND_VIDEO });
})
.then((stream) =>
{
if (!SEND)
return;
audioTrack = stream.getAudioTracks()[0];
videoTrack = stream.getVideoTracks()[0];
window.audioTrack = audioTrack;
window.videoTrack = videoTrack;
})
// Add Producers.
.then(() =>
{
if (audioTrack)
{
const deviceId = audioTrack.getSettings().deviceId;
logger.debug('calling audioProducer1 = room.createProducer(audioTrack)');
try
{
audioProducer1 = room.createProducer(audioTrack, `${deviceId}-1`);
window.audioProducer1 = audioProducer1;
handleProducer(audioProducer1);
}
catch (error)
{
logger.error(error);
}
logger.debug('calling audioProducer2 = room.createProducer(audioTrack)');
try
{
audioProducer2 = room.createProducer(audioTrack, `${deviceId}-2`);
window.audioProducer2 = audioProducer2;
handleProducer(audioProducer2);
}
catch (error)
{
logger.error(error);
}
}
if (videoTrack)
{
const deviceId = videoTrack.getSettings().deviceId;
logger.debug('calling videoProducer = room.createProducer(videoTrack)');
try
{
videoProducer = room.createProducer(videoTrack, `${deviceId}-1`);
window.videoProducer = videoProducer;
handleProducer(videoProducer);
}
catch (error)
{
logger.error(error);
}
}
})
// Receive notifications.
.then(() =>
{
if (!RECV)
return;
setTimeout(() =>
{
room.receiveNotification(DATA.ALICE_WEBCAM_NEW_CONSUMER_NOTIFICATION);
}, 2000);
});
}
function handleTransport(transport)
{
logger.warn(
'handleTransport() [direction:%s, appData:"%s", transport:%o]',
transport.direction, transport.appData, transport);
transport.on('closed', (originator, appData) =>
{
logger.warn(
'transport "closed" event [originator:%s, appData:%o, transport:%o]',
originator, appData, transport);
});
transport.on('connectionstatechange', (state) =>
{
logger.warn(
'transport "connectionstatechange" event [direction:%s, state:%s, transport:%o]',
transport.direction, state, transport);
});
setInterval(() =>
{
const queue = transport._commandQueue._queue;
if (queue.length !== 0)
logger.error('queue not empty [transport:%o, queue:%o]', transport, queue);
}, 15000);
}
function handlePeer(peer)
{
logger.warn('handlePeer() [name:"%s", peer:%o]', peer.name, peer);
switch (peer.name)
{
case 'alice':
window.alice = peer;
break;
case 'bob':
window.bob = peer;
break;
}
for (const consumer of peer.consumers)
{
handleConsumer(consumer);
}
peer.on('closed', (originator, appData) =>
{
logger.warn(
'peer "closed" event [name:"%s", originator:%s, appData:%o]',
peer.name, originator, appData);
});
peer.on('newconsumer', (consumer) =>
{
logger.warn(
'peer "newconsumer" event [name:"%s", id:%s, consumer:%o]',
peer.name, consumer.id, consumer);
handleConsumer(consumer);
});
}
function handleProducer(producer)
{
const transport1 = window.transport1;
logger.debug(
'handleProducer() [id:"%s", appData:%o, producer:%o]',
producer.id, producer.appData, producer);
logger.debug('handleProducer() | calling transport1.send(producer)');
transport1.send(producer)
.then(() =>
{
logger.debug('transport1.send(producer) succeeded');
})
.catch((error) =>
{
logger.error('transport1.send(producer) failed: %o', error);
});
producer.on('closed', (originator, appData) =>
{
logger.warn(
'producer "closed" event [id:%s, originator:%s, appData:%o, producer:%o]',
producer.id, originator, appData, producer);
});
producer.on('paused', (originator, appData) =>
{
logger.warn(
'producer "paused" event [id:%s, originator:%s, appData:%o, producer:%o]',
producer.id, originator, appData, producer);
});
producer.on('resumed', (originator, appData) =>
{
logger.warn(
'producer "resumed" event [id:%s, originator:%s, appData:%o, producer:%o]',
producer.id, originator, appData, producer);
});
producer.on('unhandled', () =>
{
logger.warn(
'producer "unhandled" event [id:%s, producer:%o]', producer.id, producer);
});
}
function handleConsumer(consumer)
{
const transport2 = window.transport2;
logger.debug(
'handleConsumer() [id:"%s", appData:%o, consumer:%o]',
consumer.id, consumer.appData, consumer);
switch (consumer.appData)
{
case 'ALICE_MIC':
window.aliceAudioConsumer = consumer;
break;
case 'ALICE_WEBCAM':
window.aliceVideoConsumer = consumer;
break;
case 'BOB_MIC':
window.bobAudioConsumer = consumer;
break;
}
logger.debug('handleConsumer() calling transport2.receive(consumer)');
transport2.receive(consumer)
.then((track) =>
{
logger.warn(
'transport2.receive(consumer) succeeded [track:%o]', track);
})
.catch((error) =>
{
logger.error('transport2.receive() failed:%o', error);
});
consumer.on('closed', (originator, appData) =>
{
logger.warn(
'consumer "closed" event [id:%s, originator:%s, appData:%o, consumer:%o]',
consumer.id, originator, appData, consumer);
});
consumer.on('paused', (originator, appData) =>
{
logger.warn(
'consumer "paused" event [id:%s, originator:%s, appData:%o, consumer:%o]',
consumer.id, originator, appData, consumer);
});
consumer.on('resumed', (originator, appData) =>
{
logger.warn(
'consumer "resumed" event [id:%s, originator:%s, appData:%o, consumer:%o]',
consumer.id, originator, appData, consumer);
});
consumer.on('unhandled', () =>
{
logger.warn(
'consumer "unhandled" event [id:%s, consumer:%o]', consumer.id, consumer);
});
}
// NOTE: Trigger server notifications.
window.notifyRoomClosed = function()
{
const room = window.room;
const notification =
{
method : 'roomClosed',
notification : true,
appData : 'ha cascao la room remota!!!'
};
room.receiveNotification(notification);
};
window.notifyTransportClosed = function()
{
const room = window.room;
const notification =
{
method : 'transportClosed',
notification : true,
id : room.transports[0].id,
appData : 'admin closed your transport'
};
room.receiveNotification(notification);
};
window.notifyAudioProducer1Closed = function()
{
const room = window.room;
const notification =
{
method : 'producerClosed',
notification : true,
id : window.audioProducer1.id,
appData : 'te paro el micro por la fuerza'
};
room.receiveNotification(notification);
};
window.notifyAudioProducer1Paused = function()
{
const room = window.room;
const notification =
{
method : 'producerPaused',
notification : true,
id : window.audioProducer1.id,
appData : 'te pause el micro por la fuerza'
};
room.receiveNotification(notification);
};
window.notifyAudioProducer1Resumed = function()
{
const room = window.room;
const notification =
{
method : 'producerResumed',
notification : true,
id : window.audioProducer1.id,
appData : 'te resumo el micro'
};
room.receiveNotification(notification);
};
window.notifyAlicePeerClosed = function()
{
const room = window.room;
const notification =
{
method : 'peerClosed',
notification : true,
name : 'alice',
appData : 'peer left'
};
room.receiveNotification(notification);
};
window.notifyAliceAudioConsumerClosed = function()
{
const room = window.room;
const notification =
{
method : 'consumerClosed',
notification : true,
peerName : 'alice',
id : 3333,
appData : 'mic broken'
};
room.receiveNotification(notification);
};
window.notifyAliceVideoConsumerClosed = function()
{
const room = window.room;
const notification =
{
method : 'consumerClosed',
notification : true,
peerName : 'alice',
id : 4444,
appData : 'webcam broken'
};
room.receiveNotification(notification);
};
window.notifyAliceVideoConsumerPaused = function()
{
const room = window.room;
const notification =
{
method : 'consumerPaused',
notification : true,
peerName : 'alice',
id : 4444,
appData : 'webcam paused'
};
room.receiveNotification(notification);
};
window.notifyAliceVideoConsumerResumed = function()
{
const room = window.room;
const notification =
{
method : 'consumerResumed',
notification : true,
peerName : 'alice',
id : 4444,
appData : 'webcam resumed'
};
room.receiveNotification(notification);
};
// NOTE: Test pause/resume.
window.testPauseResume = function()
{
logger.debug('testPauseResume() with audioProducer1');
const producer = window.audioProducer1;
// producer.once('paused', () =>
// {
// producer.resume('I RESUME TO FUACK!!!');
// });
logger.debug('testPauseResume() | (1) calling producer.pause()');
if (producer.pause('I PAUSE (1)'))
{
logger.warn(
'testPauseResume() | (1) producer.pause() succeeded [locallyPaused:%s]',
producer.locallyPaused);
}
else
{
logger.error(
'testPauseResume() | (1) producer.pause() failed [locallyPaused:%s]',
producer.locallyPaused);
}
logger.debug('testPauseResume() | (2) calling producer.pause()');
if (producer.pause('I PAUSE (2)'))
{
logger.warn(
'testPauseResume() | (2) producer.pause() succeeded [locallyPaused:%s]',
producer.locallyPaused);
}
else
{
logger.error(
'testPauseResume() | (2) producer.pause() failed [locallyPaused:%s]',
producer.locallyPaused);
}
logger.debug('testPauseResume() | (3) calling producer.resume()');
if (producer.resume('I RESUME (3)'))
{
logger.warn(
'testPauseResume() | (3) producer.resume() succeeded [locallyPaused:%s]',
producer.locallyPaused);
}
else
{
logger.error(
'testPauseResume() | (3) producer.resume() failed [locallyPaused:%s]',
producer.locallyPaused);
}
};
// NOTE: For debugging.
window.dump1 = function()
{
const transport1 = window.transport1;
const pc1 = transport1._handler._pc;
if (pc1 && pc1.localDescription)
logger.warn('PC1 SEND LOCAL OFFER:\n%s', pc1.localDescription.sdp);
if (pc1 && pc1.remoteDescription)
logger.warn('PC1 SEND REMOTE ANSWER:\n%s', pc1.remoteDescription.sdp);
};
window.dump2 = function()
{
const transport2 = window.transport2;
const pc2 = transport2._handler._pc;
if (pc2 && pc2.remoteDescription)
logger.warn('PC2 RECV REMOTE OFFER:\n%s', pc2.remoteDescription.sdp);
if (pc2 && pc2.localDescription)
logger.warn('PC2 RECV LOCAL ANSWER:\n%s', pc2.localDescription.sdp);
};

View File

@ -1,16 +0,0 @@
<!doctype html>
<html>
<head>
<title>mediasoup-client test</title>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
<meta name='description' content='mediasoup-client test'>
<script async src='/mediasoup-client-test.js'></script>
</head>
<body>
<h1>mediasoup-client test</h1>
</body>
</html>

File diff suppressed because one or more lines are too long