diff --git a/CHANGELOG.md b/CHANGELOG.md index 09cf6e4..8084d93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +### 1.0 +* Fixed toolarea button based on feedback from users +* Added possibility to move video to separate window +* Added SIP gateway + ### RC1 1.0 * First stable release? diff --git a/README.md b/README.md index cd32c15..50fdca4 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Try it online at https://letsmeet.no. You can add /roomname to the URL for speci * File sharing * Different video layouts +There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To test it, call: roomname@letsmeet.no. + ## Installation * Clone the project: diff --git a/app/chooseRoom.html b/app/chooseRoom.html new file mode 100644 index 0000000..97d00de --- /dev/null +++ b/app/chooseRoom.html @@ -0,0 +1,74 @@ + + + + + Multiparty Meeting + + + + +
+
+ + + + + diff --git a/app/gulpfile.js b/app/gulpfile.js index 126061e..a04e04f 100644 --- a/app/gulpfile.js +++ b/app/gulpfile.js @@ -137,7 +137,7 @@ gulp.task('lint', () => .pipe(eslint.format()); }); -gulp.task('lint-fix', function() +gulp.task('lint-fix', function() { return gulp.src(LINTING_FILES) .pipe(plumber()) @@ -168,7 +168,7 @@ gulp.task('css', () => gulp.task('html', () => { - return gulp.src('index.html') + return gulp.src('*.html') .pipe(change(changeHTML)) .pipe(gulp.dest(OUTPUT_DIR)); }); @@ -241,7 +241,7 @@ gulp.task('browser', (done) => gulp.task('watch', (done) => { // Watch changes in HTML. - gulp.watch([ 'index.html' ], gulp.series( + gulp.watch([ '*.html' ], gulp.series( 'html' )); diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index fa283d7..a902c73 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -179,6 +179,13 @@ export default class RoomClient this.notify('Changed layout to filmstrip view.'); break; } + + case 'm': // Toggle microphone + { + this.toggleMic(); + this.notify('Muted/unmuted your microphone.'); + break; + } } } }); @@ -391,6 +398,16 @@ export default class RoomClient } } + toggleMic() + { + logger.debug('toggleMic()'); + + if (this._micProducer.locallyPaused) + this.unmuteMic(); + else + this.muteMic(); + } + muteMic() { logger.debug('muteMic()'); @@ -1349,7 +1366,7 @@ export default class RoomClient if (this._produce) { if (this._room.canSend('audio')) - await this._setMicProducer(); + this._setMicProducer(); // Add our webcam (unless the cookie says no). if (this._room.canSend('video')) @@ -1357,7 +1374,7 @@ export default class RoomClient const devicesCookie = cookiesManager.getDevices(); if (!devicesCookie || devicesCookie.webcamEnabled) - await this.enableWebcam(); + this.enableWebcam(); } } diff --git a/app/lib/components/Me.jsx b/app/lib/components/Me.jsx index dcedb94..a8de66d 100644 --- a/app/lib/components/Me.jsx +++ b/app/lib/components/Me.jsx @@ -15,14 +15,14 @@ class Me extends React.Component controlsVisible : false }; - handleMouseOver = () => + handleMouseOver = () => { this.setState({ controlsVisible : true }); }; - handleMouseOut = () => + handleMouseOut = () => { this.setState({ controlsVisible : false @@ -108,23 +108,29 @@ class Me extends React.Component >
{connected ? -
+
{ micState === 'on' ? onMuteMic() : onUnmuteMic(); }} /> - +
{ @@ -161,15 +167,6 @@ class Me extends React.Component
:null } - - {this._tooltip ? - - :null - }
); } diff --git a/app/lib/components/Peer.jsx b/app/lib/components/Peer.jsx index ee0861f..7a4777f 100644 --- a/app/lib/components/Peer.jsx +++ b/app/lib/components/Peer.jsx @@ -40,7 +40,8 @@ class Peer extends Component onUnmuteMic, toggleConsumerFullscreen, toggleConsumerWindow, - style + style, + windowConsumer } = this.props; const micEnabled = ( @@ -128,7 +129,10 @@ class Peer extends Component />
{ e.stopPropagation(); @@ -205,6 +209,7 @@ Peer.propTypes = micConsumer : appPropTypes.Consumer, webcamConsumer : appPropTypes.Consumer, screenConsumer : appPropTypes.Consumer, + windowConsumer : PropTypes.number, onMuteMic : PropTypes.func.isRequired, onUnmuteMic : PropTypes.func.isRequired, streamDimensions : PropTypes.object, @@ -229,7 +234,8 @@ const mapStateToProps = (state, { name }) => peer, micConsumer, webcamConsumer, - screenConsumer + screenConsumer, + windowConsumer : state.room.windowConsumer }; }; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 653b377..47a5a44 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -13,7 +13,7 @@ import Me from './Me'; import Peers from './Peers'; import AudioPeers from './PeerAudio/AudioPeers'; import Notifications from './Notifications'; -import ToolAreaButton from './ToolArea/ToolAreaButton'; +// import ToolAreaButton from './ToolArea/ToolAreaButton'; import ToolArea from './ToolArea/ToolArea'; import FullScreenView from './FullScreenView'; import VideoWindow from './VideoWindow/VideoWindow'; @@ -97,8 +97,6 @@ class Room extends React.Component - - {room.advancedMode ?
diff --git a/app/lib/components/Settings.jsx b/app/lib/components/Settings.jsx index b27a20f..394887a 100644 --- a/app/lib/components/Settings.jsx +++ b/app/lib/components/Settings.jsx @@ -5,6 +5,7 @@ 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', @@ -58,20 +59,34 @@ const Settings = ({ onChange={(device) => handleChangeAudioDevice(device.value)} placeholder={audioDevicesText} /> - - - +
+ + +
- handleChangeMode(mode.value)} - /> +
+ handleChangeMode(mode.value)} + /> +
); diff --git a/app/lib/components/ToolArea/ToolArea.jsx b/app/lib/components/ToolArea/ToolArea.jsx index 925bd31..5165796 100644 --- a/app/lib/components/ToolArea/ToolArea.jsx +++ b/app/lib/components/ToolArea/ToolArea.jsx @@ -24,7 +24,7 @@ class ToolArea extends React.Component unreadMessages, unreadFiles, toggleToolArea, - closeToolArea + unread } = this.props; const VisibleTab = { @@ -50,11 +50,21 @@ class ToolArea extends React.Component })} >
+ className='toolarea-button' + onClick={toggleToolArea} + > + +
+

Toolbox

+ + {!toolAreaOpen && unread > 0 && ( + = 10 })}> + {unread} + + )} +
({ currentToolTab : state.toolarea.currentToolTab, unreadMessages : state.toolarea.unreadMessages, unreadFiles : state.toolarea.unreadFiles, - toolAreaOpen : state.toolarea.toolAreaOpen + toolAreaOpen : state.toolarea.toolAreaOpen, + unread : state.toolarea.unreadMessages + + state.toolarea.unreadFiles }); const mapDispatchToProps = { diff --git a/app/lib/components/ToolArea/ToolAreaButton.jsx b/app/lib/components/ToolArea/ToolAreaButton.jsx index c33047c..20b0024 100644 --- a/app/lib/components/ToolArea/ToolAreaButton.jsx +++ b/app/lib/components/ToolArea/ToolAreaButton.jsx @@ -27,7 +27,7 @@ class ToolAreaButton extends React.Component className={classnames('button toolarea-button', { on : toolAreaOpen })} - data-tip='Open tool box' + data-tip='Open tools' data-type='dark' onClick={() => toggleToolArea()} /> diff --git a/app/lib/components/VideoWindow/NewWindow.jsx b/app/lib/components/VideoWindow/NewWindow.jsx index db32f73..cb4b13e 100644 --- a/app/lib/components/VideoWindow/NewWindow.jsx +++ b/app/lib/components/VideoWindow/NewWindow.jsx @@ -165,7 +165,7 @@ class NewWindow extends React.PureComponent { if (this.window) { - if (this.fullscreen.fullscreenEnabled) + if (this.fullscreen && this.fullscreen.fullscreenEnabled) { this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); } diff --git a/app/lib/index.jsx b/app/lib/index.jsx index 6ab6183..1348b09 100644 --- a/app/lib/index.jsx +++ b/app/lib/index.jsx @@ -32,7 +32,7 @@ function run() const peerName = randomString({ length: 8 }).toLowerCase(); const urlParser = new UrlParse(window.location.href, true); let roomId = (urlParser.pathname).substr(1) - ? (urlParser.pathname).substr(1) : urlParser.query.roomId; + ? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase(); const produce = urlParser.query.produce !== 'false'; let displayName = urlParser.query.displayName; const isSipEndpoint = urlParser.query.sipEndpoint === 'true'; diff --git a/app/lib/store.js b/app/lib/store.js index 1208e8c..f3652f2 100644 --- a/app/lib/store.js +++ b/app/lib/store.js @@ -18,7 +18,9 @@ if (process.env.NODE_ENV === 'development') { const reduxLogger = createLogger( { - predicate : (getState, action) => action.type !== 'SET_PRODUCER_VOLUME', + // filter VOLUME level actions from log + predicate : (getState, action) => ! (action.type == 'SET_PRODUCER_VOLUME' + || action.type == 'SET_CONSUMER_VOLUME'), duration : true, timestamp : false, level : 'log', diff --git a/app/package.json b/app/package.json index ab2510a..314e8f2 100644 --- a/app/package.json +++ b/app/package.json @@ -36,6 +36,7 @@ "redux-thunk": "^2.3.0", "resize-observer-polyfill": "^1.5.0", "riek": "^1.1.0", + "socket.io-client": "^2.1.1", "url-parse": "^1.4.3", "webtorrent": "^0.102.4" }, diff --git a/app/resources/images/cancel.svg b/app/resources/images/cancel.svg new file mode 100644 index 0000000..32cba1a --- /dev/null +++ b/app/resources/images/cancel.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/stylus/components/Me.styl b/app/stylus/components/Me.styl index 31fe5a6..2d70e8f 100644 --- a/app/stylus/components/Me.styl +++ b/app/stylus/components/Me.styl @@ -33,7 +33,7 @@ &.visible { opacity: 1; } - + > .button { flex: 0 0 auto; margin: 0.2vmin; @@ -43,13 +43,17 @@ background-repeat: no-repeat; background-color: rgba(#000, 0.5); cursor: pointer; + opacity: 0; transition-property: opacity, background-color; transition-duration: 0.15s; + &.visible { + opacity: 0.85; + } + +desktop() { width: 24px; height: 24px; - opacity: 0.85; &:hover { opacity: 1; diff --git a/app/stylus/components/Sidebar.styl b/app/stylus/components/Sidebar.styl index be324c0..0c69fa8 100644 --- a/app/stylus/components/Sidebar.styl +++ b/app/stylus/components/Sidebar.styl @@ -9,13 +9,13 @@ align-items: center; +desktop() { - left: 20px; - width: 36px; + left: 1.0em; + width: 2.6em; } +mobile() { - left: 10px; - width: 32px; + left: 0.5em; + width: 2.6em; } > .button { @@ -34,13 +34,13 @@ justify-content: center; +desktop() { - height: 36px; - width: 36px; + height: 2.5em; + width: 2.5em; } +mobile() { - height: 32px; - width: 32px; + height: 2.5em; + width: 2.5em; } &.on { @@ -110,7 +110,7 @@ } &.leave-meeting { - background-image: url('/resources/images/leave-meeting.svg'); + background-image: url('/resources/images/cancel.svg'); } } } diff --git a/app/stylus/components/ToolArea.styl b/app/stylus/components/ToolArea.styl index 7fb18d2..9283d42 100644 --- a/app/stylus/components/ToolArea.styl +++ b/app/stylus/components/ToolArea.styl @@ -63,6 +63,93 @@ } } + > .toolarea-button { + text-align: center; + writing-mode: vertical-rl; + text-orientation: mixed; + list-style: none; + height: 115px; + width: 35px; + left: -35px; + top: 50%; + transform: translate(0, -50%); + position: absolute; + cursor: pointer; + + > .badge { + border-radius: 50%; + writing-mode: horizontal-tb; + font-size: 1rem; + background: #b12525; + color: #fff; + text-align: center; + margin-top: -10px; + line-height: 1rem; + margin-left: -0px; + position: absolute; + padding: 0.2rem 0.4rem; + top: 0; + left: 0; + + &.long { + border-radius: 25% / 50%; + margin-top: -13px; + margin-left: -4px; + } + } + + > .content { + border: 1px solid #AAA; + width: 100%; + height: 100%; + display: flex; + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; + background: #FFF; + color: #333; + z-index: 2; + border-right-color: #FFF; + + &:before, &:after { + border: 1px solid #AAA; + position: absolute; + width: 6px; + height: 6px; + content: ""; + } + + &:before { + top: -6px; + right: 0; + border-bottom-right-radius: 6px; + border-width: 0px 1px 1px 0px; + box-shadow: 0px 3px 0 #FFF; + } + + &:after { + bottom: -6px; + right: 0; + border-top-right-radius: 6px; + border-width: 1px 1px 0px 0px; + box-shadow: 0px -3px 0 #FFF; + } + + > .toolarea-icon { + background-position: center; + background-size: 75%; + background-repeat: no-repeat; + border-radius: 100%; + height: 32px; + width: 32px; + + background-image: url('/resources/images/icon_tool_area_black.svg'); + } + + > p { + padding: 9px; + } + } + } } @media (min-width: 600px) { @@ -158,10 +245,6 @@ background-image: url('/resources/images/icon_tool_area_black.svg'); } } - - &.toolarea-close-button { - background-image: url('/resources/images/arrow_right.svg'); - } } > .badge { diff --git a/server/lib/Room.js b/server/lib/Room.js index f306ccb..5e7530b 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -346,6 +346,25 @@ class Room extends EventEmitter ); }); + signalingPeer.socket.on('request-consumer-keyframe', (request, cb) => + { + cb(null); + + const { consumerId } = request; + const mediaPeer = this._mediaRoom.getPeerByName(signalingPeer.peerName); + const consumer = mediaPeer.consumers + .find((_consumer) => _consumer.id === consumerId); + + if (!consumer) + { + logger.warn('consumer with id "%s" not found', consumerId); + + return; + } + + consumer.requestKeyFrame(); + }); + signalingPeer.socket.on('disconnect', () => { logger.debug('Peer "close" event [peer:"%s"]', signalingPeer.peerName); diff --git a/server/lib/homer.js b/server/lib/homer.js index 4b02387..83106c2 100644 --- a/server/lib/homer.js +++ b/server/lib/homer.js @@ -155,17 +155,20 @@ function handleTransport(transport, baseEvent, stream) const statsInterval = setInterval(() => { - transport.getStats() - .then((stats) => - { - emit( - Object.assign({}, baseEvent, - { - event : 'transport.stats', - stats : stats - }), - stream); - }); + if (typeof transport.getStats === 'function') + { + transport.getStats() + .then((stats) => + { + emit( + Object.assign({}, baseEvent, + { + event : 'transport.stats', + stats : stats + }), + stream); + }); + } }, STATS_INTERVAL); transport.on('close', (originator) => diff --git a/server/package.json b/server/package.json index 098bd70..404ecaf 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ "compression": "^1.7.3", "debug": "^4.1.0", "express": "^4.16.3", - "mediasoup": "^2.3.3", + "mediasoup": "^2.4.3", "passport-dataporten": "^1.3.0", "socket.io": "^2.1.1" }, diff --git a/server/server.js b/server/server.js index 8daa1f0..9a0583b 100755 --- a/server/server.js +++ b/server/server.js @@ -57,7 +57,7 @@ app.all('*', (req, res, next) => app.use(dataporten.passport.initialize()); app.use(dataporten.passport.session()); -app.get('/login', (req, res, next) => +app.get('/login', (req, res, next) => { dataporten.passport.authenticate('dataporten', { state : base64.encode(JSON.stringify({ @@ -65,12 +65,17 @@ app.get('/login', (req, res, next) => peerName : req.query.peerName, code : utils.random(10) })) - + })(req, res, next); }); dataporten.setupLogout(app, '/logout'); +app.get('/', function (req, res) { + console.log(req.url); + res.sendFile(`${__dirname}/public/chooseRoom.html`); +}) + app.get( '/auth-callback', dataporten.passport.authenticate('dataporten', { failureRedirect: '/login' }),