Merge pull request #50 from havfo/feat/file-sharing

[WIP] Feat/file sharing
master
Håvar Aambø Fosstveit 2018-08-02 10:01:52 +02:00 committed by GitHub
commit 48ac6c8391
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 5331 additions and 1400 deletions

View File

@ -21,6 +21,19 @@ $ cd server
$ npm install
```
In addition, the server requires a screen to be installed for the server
to be able to seed shared torrent files. This is because the headless
Electron instance used by WebTorrent expects one. This means that in order
to run the project on a server, you need to install a virtual screen
such as `xvfb` by running
```bash
sudo apt install xvfb
```
See [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid) for
more information about this.
* Copy `config.example.js` as `config.js` and customize it for your scenario:
```bash

View File

@ -172,7 +172,7 @@ module.exports =
'semi': [ 2, 'always' ],
'semi-spacing': 2,
'space-before-blocks': 2,
'space-before-function-paren': [ 2, 'never' ],
'space-before-function-paren': [ 2, { anonymous: 'never', named: 'never', 'asyncArrow': 'always'}],
'space-in-parens': [ 2, 'never' ],
'spaced-comment': [ 2, 'always' ],
'strict': 2,

View File

@ -205,6 +205,22 @@ export default class RoomClient
});
}
sendFile(file)
{
logger.debug('sendFile() [file: %o]', file);
return this._protoo.send('send-file', { file })
.catch((error) =>
{
logger.error('sendFile() | failed: %o', error);
this._dispatch(requestActions.notify({
typ : 'error',
text : 'An error occurred while sharing a file'
}));
});
}
getChatHistory()
{
logger.debug('getChatHistory()');
@ -222,6 +238,22 @@ export default class RoomClient
});
}
getFileHistory()
{
logger.debug('getFileHistory()');
return this._protoo.send('file-history', {})
.catch((error) =>
{
logger.error('getFileHistory() | failed: %o', error);
this._dispatch(requestActions.notify({
type : 'error',
text : 'Could not get file history'
}));
});
}
muteMic()
{
logger.debug('muteMic()');
@ -1136,6 +1168,37 @@ export default class RoomClient
break;
}
case 'file-receive':
{
accept();
const payload = request.data.file;
this._dispatch(stateActions.addFile(payload));
this._dispatch(requestActions.notify({
text : `${payload.name} shared a file`
}));
break;
}
case 'file-history-receive':
{
accept();
const files = request.data.fileHistory;
if (files.length > 0)
{
logger.debug('Got files history');
this._dispatch(stateActions.addFileHistory(files));
}
break;
}
default:
{
logger.error('unknown protoo method "%s"', request.method);
@ -1273,6 +1336,7 @@ export default class RoomClient
this._dispatch(stateActions.removeAllNotifications());
this.getChatHistory();
this.getFileHistory();
this._dispatch(requestActions.notify(
{

View File

@ -0,0 +1,18 @@
import React from 'react';
import WebTorrent from 'webtorrent';
import dragDrop from 'drag-drop';
import { shareFiles } from './index';
export const configureDragDrop = () =>
{
if (WebTorrent.WEBRTC_SUPPORT)
{
dragDrop('body', async (files) => await shareFiles(files));
}
};
export const HoldingOverlay = () => (
<div id='holding-overlay'>
Drop files here to share them
</div>
);

View File

@ -0,0 +1,196 @@
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

@ -0,0 +1,47 @@
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()
{
return (
<div className='shared-files'>
{this.props.sharing.map((entry, i) => (
<FileEntry
data={entry}
key={i}
/>
))}
</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

@ -0,0 +1,131 @@
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

@ -1,4 +1,4 @@
import React from 'react';
import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import ReactTooltip from 'react-tooltip';
import PropTypes from 'prop-types';
@ -18,6 +18,9 @@ import Draggable from 'react-draggable';
import { idle } from '../utils';
import Sidebar from './Sidebar';
import Filmstrip from './Filmstrip';
import { configureDragDrop, HoldingOverlay } from './FileSharing/DragDropSharing';
configureDragDrop();
// Hide toolbars after 10 seconds of inactivity.
const TIMEOUT = 10 * 1000;
@ -73,13 +76,16 @@ class Room extends React.Component
}[room.mode];
return (
<Fragment>
<HoldingOverlay />
<Appear duration={300}>
<div data-component='Room'>
<FullScreenView advancedMode={room.advancedMode} />
<div
className='room-wrapper'
style={{
width : toolAreaOpen ? '80%' : '100%'
width : toolAreaOpen ? '75%' : '100%'
}}
>
<Notifications />
@ -157,7 +163,7 @@ class Room extends React.Component
<div
className='toolarea-wrapper'
style={{
width : toolAreaOpen ? '20%' : '0%'
width : toolAreaOpen ? '25%' : '0%'
}}
>
{toolAreaOpen ?
@ -169,6 +175,7 @@ class Room extends React.Component
</div>
</div>
</Appear>
</Fragment>
);
}
}

View File

@ -5,6 +5,7 @@ import * as toolTabActions from '../../redux/stateActions';
import ParticipantList from '../ParticipantList/ParticipantList';
import Chat from '../Chat/Chat';
import Settings from '../Settings';
import FileSharing from '../FileSharing';
class ToolArea extends React.Component
{
@ -17,7 +18,8 @@ class ToolArea extends React.Component
{
const {
currentToolTab,
unread,
unreadMessages,
unreadFiles,
setToolTab
} = this.props;
@ -37,8 +39,8 @@ class ToolArea extends React.Component
<label htmlFor='tab-chat'>
Chat
{unread > 0 && (
<span className='badge'>{unread}</span>
{unreadMessages > 0 && (
<span className='badge'>{unreadMessages}</span>
)}
</label>
@ -46,6 +48,25 @@ class ToolArea extends React.Component
<Chat />
</div>
<input
type='radio'
name='tabs'
id='tab-files'
onChange={() => setToolTab('files')}
checked={currentToolTab === 'files'}
/>
<label htmlFor='tab-files'>
Files
{unreadFiles > 0 && (
<span className='badge'>{unreadFiles}</span>
)}
</label>
<div className='tab'>
<FileSharing />
</div>
<input
type='radio'
name='tabs'
@ -88,12 +109,14 @@ ToolArea.propTypes =
advancedMode : PropTypes.bool,
currentToolTab : PropTypes.string.isRequired,
setToolTab : PropTypes.func.isRequired,
unread : PropTypes.number.isRequired
unreadMessages : PropTypes.number.isRequired,
unreadFiles : PropTypes.number.isRequired
};
const mapStateToProps = (state) => ({
currentToolTab : state.toolarea.currentToolTab,
unread : state.toolarea.unread
unreadMessages : state.toolarea.unreadMessages,
unreadFiles : state.toolarea.unreadFiles
});
const mapDispatchToProps = {

View File

@ -27,7 +27,7 @@ class ToolAreaButton extends React.Component
onClick={() => toggleToolArea()}
/>
{unread > 0 && (
{!toolAreaOpen && unread > 0 && (
<span className={classnames('badge', { long: unread >= 10 })}>
{unread}
</span>
@ -50,7 +50,7 @@ const mapStateToProps = (state) =>
return {
toolAreaOpen : state.toolarea.toolAreaOpen,
visible : state.room.toolbarsVisible,
unread : state.toolarea.unread
unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles
};
};

View File

@ -3,13 +3,6 @@ import UrlParse from 'url-parse';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import {
applyMiddleware as applyReduxMiddleware,
createStore as createReduxStore,
compose as composeRedux
} from 'redux';
import thunk from 'redux-thunk';
import { createLogger as createReduxLogger } from 'redux-logger';
import { getDeviceInfo } from 'mediasoup-client';
import randomString from 'random-string';
import Logger from './Logger';
@ -17,48 +10,11 @@ import * as utils from './utils';
import * as cookiesManager from './cookiesManager';
import * as requestActions from './redux/requestActions';
import * as stateActions from './redux/stateActions';
import reducers from './redux/reducers';
import roomClientMiddleware from './redux/roomClientMiddleware';
import Room from './components/Room';
import { loginEnabled } from '../config';
import { store } from './store';
const logger = new Logger();
const reduxMiddlewares =
[
thunk,
roomClientMiddleware
];
if (process.env.NODE_ENV === 'development')
{
const reduxLogger = createReduxLogger(
{
duration : true,
timestamp : false,
level : 'log',
logErrors : true
});
reduxMiddlewares.push(reduxLogger);
}
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extensions options like name, actionsBlacklist, actionsCreators, serialize...
}) : composeRedux;
const enhancer = composeEnhancers(
applyReduxMiddleware(...reduxMiddlewares)
// other store enhancers if any
);
const store = createReduxStore(
reducers,
undefined,
enhancer
);
domready(() =>
{

View File

@ -11,7 +11,7 @@ const chatmessages = (state = [], action) =>
{
const { text } = action.payload;
const message = createNewMessage(text, 'client', 'Me');
const message = createNewMessage(text, 'client', 'Me', undefined);
return [ ...state, message ];
}

View File

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

View File

@ -0,0 +1,19 @@
const sharing = (state = [], action) =>
{
switch (action.type)
{
case 'SEND_FILE':
return [ ...state, { ...action.payload, me: true } ];
case 'ADD_FILE':
return [ ...state, action.payload ];
case 'ADD_FILE_HISTORY':
return [ ...action.payload.fileHistory, ...state ];
default:
return state;
}
};
export default sharing;

View File

@ -2,7 +2,8 @@ const initialState =
{
toolAreaOpen : false,
currentToolTab : 'chat', // chat, settings, users
unread : 0
unreadMessages : 0,
unreadFiles : 0
};
const toolarea = (state = initialState, action) =>
@ -12,17 +13,19 @@ const toolarea = (state = initialState, action) =>
case 'TOGGLE_TOOL_AREA':
{
const toolAreaOpen = !state.toolAreaOpen;
const unread = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unread;
const unreadMessages = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
const unreadFiles = toolAreaOpen && state.currentToolTab === 'files' ? 0 : state.unreadFiles;
return { ...state, toolAreaOpen, unread };
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
}
case 'SET_TOOL_TAB':
{
const { toolTab } = action.payload;
const unread = toolTab === 'chat' ? 0 : state.unread;
const unreadMessages = toolTab === 'chat' ? 0 : state.unreadMessages;
const unreadFiles = toolTab === 'files' ? 0 : state.unreadFiles;
return { ...state, currentToolTab: toolTab, unread };
return { ...state, currentToolTab: toolTab, unreadMessages, unreadFiles };
}
case 'ADD_NEW_RESPONSE_MESSAGE':
@ -32,7 +35,17 @@ const toolarea = (state = initialState, action) =>
return state;
}
return { ...state, unread: state.unread + 1 };
return { ...state, unreadMessages: state.unreadMessages + 1 };
}
case 'ADD_FILE':
{
if (state.toolAreaOpen && state.currentToolTab === 'files')
{
return state;
}
return { ...state, unreadFiles: state.unreadFiles + 1 };
}
default:

View File

@ -213,6 +213,14 @@ export const sendChatMessage = (text, name, picture) =>
};
};
export const sendFile = (file, name, picture) =>
{
return {
type : 'SEND_FILE',
payload : { file, name, picture }
};
};
// This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, timeout }) =>
{

View File

@ -231,6 +231,12 @@ export default ({ dispatch, getState }) => (next) =>
break;
}
case 'SEND_FILE':
{
client.sendFile(action.payload);
break;
}
}
return next(action);

View File

@ -410,6 +410,14 @@ export const addUserMessage = (text) =>
};
};
export const addUserFile = (file) =>
{
return {
type : 'ADD_NEW_USER_FILE',
payload : { file }
};
};
export const addResponseMessage = (message) =>
{
return {
@ -433,6 +441,22 @@ export const dropMessages = () =>
};
};
export const addFile = (payload) =>
{
return {
type : 'ADD_FILE',
payload
};
};
export const addFileHistory = (fileHistory) =>
{
return {
type : 'ADD_FILE_HISTORY',
payload : { fileHistory }
};
};
export const setPicture = (picture) =>
({
type : 'SET_PICTURE',

46
app/lib/store.js 100644
View File

@ -0,0 +1,46 @@
import {
applyMiddleware,
createStore,
compose
} from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import reducers from './redux/reducers';
import roomClientMiddleware from './redux/roomClientMiddleware';
const reduxMiddlewares =
[
thunk,
roomClientMiddleware
];
if (process.env.NODE_ENV === 'development')
{
const reduxLogger = createLogger(
{
duration : true,
timestamp : false,
level : 'log',
logErrors : true
});
reduxMiddlewares.push(reduxLogger);
}
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extensions options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(...reduxMiddlewares)
// other store enhancers if any
);
export const store = createStore(
reducers,
undefined,
enhancer
);

1267
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,11 +9,15 @@
"dependencies": {
"babel-runtime": "^6.26.0",
"classnames": "^2.2.6",
"create-torrent": "^3.32.1",
"debug": "^3.1.0",
"domready": "^1.0.8",
"drag-drop": "^4.2.0",
"file-saver": "^1.3.8",
"fscreen": "^1.0.2",
"hark": "^1.2.2",
"js-cookie": "^2.2.0",
"magnet-uri": "^5.2.3",
"marked": "^0.4.0",
"mediasoup-client": "^2.1.1",
"prop-types": "^15.6.2",
@ -33,7 +37,8 @@
"redux-thunk": "^2.3.0",
"resize-observer-polyfill": "^1.5.0",
"riek": "^1.1.0",
"url-parse": "^1.4.1"
"url-parse": "^1.4.1",
"webtorrent": "^0.101.0"
},
"devDependencies": {
"babel-core": "^6.26.3",

View File

@ -0,0 +1,4 @@
<svg fill="#FFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zm-6 .67l2.59-2.58L17 11.5l-5 5-5-5 1.41-1.41L11 12.67V3h2z"/>
<path fill="none" d="M0 0h24v24H0z"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,4 @@
<svg fill="#FFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1,95 @@
[data-component='FileSharing'] {
display: flex;
flex-direction: column;
height: 100%;
> .sharing-toolbar {
> .share-file {
cursor: pointer;
width: 100%;
background: #252525;
border: 1px solid #151515;
padding: 1rem;
border-bottom: 5px solid #151515;
border-radius: 3px 3px 0 0;
&.disabled {
cursor: not-allowed;
}
}
}
> .shared-files {
flex-grow: 1;
overflow-y: scroll;
> .file-entry {
background-color: rgba(0,0,0,0.1);
border-radius: 5px;
width: 100%;
padding: 0.5rem;
display: flex;
margin-top: 0.5rem;
&:last-child {
margin-bottom: 1.5rem;
}
> .file-avatar {
height: 2rem;
border-radius: 50%;
}
> .file-content {
flex-grow: 1;
padding-left: 0.5rem;
> p:not(:first-child) {
margin-top: 0.5rem;
}
> .file-info {
display: flex;
padding-top: 0.5rem;
align-items: center;
> .button {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: #252525;
border: 1px solid #151515;
padding: 0.3rem;
border-bottom: 5px solid #151515;
border-radius: 3px 3px 0 0;
}
> p {
flex-grow: 1;
padding-left: 0.5rem;
}
}
}
}
}
}
#holding-overlay {
display: none;
}
.drag #holding-overlay {
display: flex;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: #FFF;
align-items: center;
justify-content: center;
font-size: 2rem;
z-index: 2000;
}

View File

@ -89,7 +89,7 @@
font-weight: bold;
transition: background ease 0.2s;
text-align: center;
width: 33.33%;
width: 25%;
font-size: 1.3vmin;
height: 3vmin;

View File

@ -35,6 +35,7 @@ body {
#multiparty-meeting {
height: 100%;
width: 100%;
}
// Components
@import './components/Room';
@ -52,7 +53,7 @@ body {
@import './components/FullScreenView';
@import './components/FullView';
@import './components/Filmstrip';
}
@import './components/FileSharing';
// Hack to detect in JS the current media query
#multiparty-meeting-media-query-detector {

View File

@ -16,6 +16,15 @@ module.exports =
},
// Listening port for https server.
listeningPort : 3443,
turnServers : [
{
urls : [
'turn:example.com:443?transport=tcp'
],
username : 'example',
credential : 'example'
}
],
mediasoup :
{
// mediasoup Server settings.

View File

@ -2,6 +2,7 @@
const EventEmitter = require('events').EventEmitter;
const protooServer = require('protoo-server');
const WebTorrent = require('webtorrent-hybrid');
const Logger = require('./Logger');
const config = require('../config');
@ -11,6 +12,14 @@ const BITRATE_FACTOR = 0.75;
const logger = new Logger('Room');
const torrentClient = new WebTorrent({
tracker : {
rtcConfig : {
iceServers : config.turnServers
}
}
});
class Room extends EventEmitter
{
constructor(roomId, mediaServer)
@ -28,6 +37,8 @@ class Room extends EventEmitter
this._chatHistory = [];
this._fileHistory = [];
try
{
// Protoo Room instance.
@ -272,6 +283,37 @@ class Room extends EventEmitter
break;
}
case 'send-file':
{
accept();
const fileData = request.data.file;
this._fileHistory.push(fileData);
if (!torrentClient.get(fileData.file.magnet))
{
torrentClient.add(fileData.file.magnet);
}
this._protooRoom.spread('file-receive', {
file : fileData
}, [ protooPeer ]);
break;
}
case 'file-history':
{
accept();
protooPeer.send('file-history-receive', {
fileHistory : this._fileHistory
});
break;
}
case 'raisehand-message':
{
accept();

4388
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,8 @@
"express": "^4.16.3",
"mediasoup": "^2.1.0",
"passport-dataporten": "^1.3.0",
"protoo-server": "^2.0.7"
"protoo-server": "^2.0.7",
"webtorrent-hybrid": "^1.0.6"
},
"devDependencies": {
"gulp": "^4.0.0",