Make demo public
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* <%= pkg.name %> v<%= pkg.version %>
|
||||
* <%= pkg.description %>
|
||||
* Copyright: 2017-<%= currentYear %> <%= pkg.author %>
|
||||
* License: <%= pkg.license %>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports =
|
||||
{
|
||||
protoo :
|
||||
{
|
||||
listenPort : 3443
|
||||
}
|
||||
};
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Tasks:
|
||||
*
|
||||
* gulp prod
|
||||
* Generates the browser app in production mode.
|
||||
*
|
||||
* gulp dev
|
||||
* Generates the browser app in development mode.
|
||||
*
|
||||
* gulp live
|
||||
* Generates the browser app in development mode, opens it and watches
|
||||
* for changes in the source code.
|
||||
*
|
||||
* gulp
|
||||
* Alias for `gulp live`.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const gulpif = require('gulp-if');
|
||||
const gutil = require('gulp-util');
|
||||
const plumber = require('gulp-plumber');
|
||||
const touch = require('gulp-touch');
|
||||
const rename = require('gulp-rename');
|
||||
const header = require('gulp-header');
|
||||
const browserify = require('browserify');
|
||||
const watchify = require('watchify');
|
||||
const envify = require('envify/custom');
|
||||
const uglify = require('gulp-uglify');
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const del = require('del');
|
||||
const mkdirp = require('mkdirp');
|
||||
const ncp = require('ncp');
|
||||
const eslint = require('gulp-eslint');
|
||||
const stylus = require('gulp-stylus');
|
||||
const cssBase64 = require('gulp-css-base64');
|
||||
const nib = require('nib');
|
||||
const browserSync = require('browser-sync');
|
||||
|
||||
const PKG = require('./package.json');
|
||||
const BANNER = fs.readFileSync('banner.txt').toString();
|
||||
const BANNER_OPTIONS =
|
||||
{
|
||||
pkg : PKG,
|
||||
currentYear : (new Date()).getFullYear()
|
||||
};
|
||||
const OUTPUT_DIR = '../server/public';
|
||||
|
||||
// Default environment.
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
function logError(error)
|
||||
{
|
||||
gutil.log(gutil.colors.red(String(error)));
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
function bundle(options)
|
||||
{
|
||||
options = options || {};
|
||||
|
||||
let watch = !!options.watch;
|
||||
let bundler = browserify(
|
||||
{
|
||||
entries : path.join(__dirname, PKG.main),
|
||||
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', 'react' ],
|
||||
plugins : [ 'transform-runtime', 'transform-object-assign' ]
|
||||
})
|
||||
.transform(envify(
|
||||
{
|
||||
NODE_ENV : process.env.NODE_ENV,
|
||||
_ : 'purge'
|
||||
}));
|
||||
|
||||
if (watch)
|
||||
{
|
||||
bundler = watchify(bundler);
|
||||
|
||||
bundler.on('update', () =>
|
||||
{
|
||||
let start = Date.now();
|
||||
|
||||
gutil.log('bundling...');
|
||||
rebundle();
|
||||
gutil.log('bundle took %sms', (Date.now() - start));
|
||||
});
|
||||
}
|
||||
|
||||
function rebundle()
|
||||
{
|
||||
return bundler.bundle()
|
||||
.on('error', logError)
|
||||
.pipe(source(`${PKG.name}.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(rename(`${PKG.name}.js`))
|
||||
.pipe(gulpif(process.env.NODE_ENV === 'production',
|
||||
uglify()
|
||||
))
|
||||
.pipe(header(BANNER, BANNER_OPTIONS))
|
||||
.pipe(gulp.dest(OUTPUT_DIR));
|
||||
}
|
||||
|
||||
return rebundle();
|
||||
}
|
||||
|
||||
gulp.task('clean', () => del(OUTPUT_DIR, { force: true }));
|
||||
|
||||
gulp.task('env:dev', (done) =>
|
||||
{
|
||||
gutil.log('setting "dev" environment');
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('env:prod', (done) =>
|
||||
{
|
||||
gutil.log('setting "prod" environment');
|
||||
|
||||
process.env.NODE_ENV = 'production';
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('lint', () =>
|
||||
{
|
||||
let src = [ 'gulpfile.js', 'lib/**/*.js', 'lib/**/*.jsx' ];
|
||||
|
||||
return gulp.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(eslint(
|
||||
{
|
||||
plugins : [ 'react', 'import' ],
|
||||
extends : [ 'eslint:recommended', 'plugin:react/recommended' ],
|
||||
settings :
|
||||
{
|
||||
react :
|
||||
{
|
||||
pragma : 'React', // Pragma to use, default to 'React'.
|
||||
version : '15' // React version, default to the latest React stable release.
|
||||
}
|
||||
},
|
||||
parserOptions :
|
||||
{
|
||||
ecmaVersion : 6,
|
||||
sourceType : 'module',
|
||||
ecmaFeatures :
|
||||
{
|
||||
impliedStrict : true,
|
||||
jsx : true
|
||||
}
|
||||
},
|
||||
envs :
|
||||
[
|
||||
'browser',
|
||||
'es6',
|
||||
'node',
|
||||
'commonjs'
|
||||
],
|
||||
'rules' :
|
||||
{
|
||||
'no-console' : 0,
|
||||
'no-undef' : 2,
|
||||
'no-unused-vars' : [ 2, { vars: 'all', args: 'after-used' }],
|
||||
'no-empty' : 0,
|
||||
'quotes' : [ 2, 'single', { avoidEscape: true } ],
|
||||
'semi' : [ 2, 'always' ],
|
||||
'no-multi-spaces' : 0,
|
||||
'no-whitespace-before-property' : 2,
|
||||
'space-before-blocks' : 2,
|
||||
'space-before-function-paren' : [ 2, 'never' ],
|
||||
'space-in-parens' : [ 2, 'never' ],
|
||||
'spaced-comment' : [ 2, 'always' ],
|
||||
'comma-spacing' : [ 2, { before: false, after: true } ],
|
||||
'jsx-quotes' : [ 2, 'prefer-single' ],
|
||||
'react/display-name' : [ 2, { ignoreTranspilerName: false } ],
|
||||
'react/forbid-prop-types' : 0,
|
||||
'react/jsx-boolean-value' : 1,
|
||||
'react/jsx-closing-bracket-location' : 1,
|
||||
'react/jsx-curly-spacing' : 1,
|
||||
'react/jsx-equals-spacing' : 1,
|
||||
'react/jsx-handler-names' : 1,
|
||||
'react/jsx-indent-props' : [ 2, 'tab' ],
|
||||
'react/jsx-indent' : [ 2, 'tab' ],
|
||||
'react/jsx-key' : 1,
|
||||
'react/jsx-max-props-per-line' : 0,
|
||||
'react/jsx-no-bind' : 0,
|
||||
'react/jsx-no-duplicate-props' : 1,
|
||||
'react/jsx-no-literals' : 0,
|
||||
'react/jsx-no-undef' : 1,
|
||||
'react/jsx-pascal-case' : 1,
|
||||
'react/jsx-sort-prop-types' : 0,
|
||||
'react/jsx-sort-props' : 0,
|
||||
'react/jsx-uses-react' : 1,
|
||||
'react/jsx-uses-vars' : 1,
|
||||
'react/no-danger' : 1,
|
||||
'react/no-deprecated' : 1,
|
||||
'react/no-did-mount-set-state' : 1,
|
||||
'react/no-did-update-set-state' : 1,
|
||||
'react/no-direct-mutation-state' : 1,
|
||||
'react/no-is-mounted' : 1,
|
||||
'react/no-multi-comp' : 0,
|
||||
'react/no-set-state' : 0,
|
||||
'react/no-string-refs' : 0,
|
||||
'react/no-unknown-property' : 1,
|
||||
'react/prefer-es6-class' : 1,
|
||||
'react/prop-types' : 1,
|
||||
'react/react-in-jsx-scope' : 1,
|
||||
'react/self-closing-comp' : 1,
|
||||
'react/sort-comp' : 0,
|
||||
'react/jsx-wrap-multilines' : [ 1, { declaration: false, assignment: false, return: true } ],
|
||||
'import/extensions' : 1
|
||||
}
|
||||
}))
|
||||
.pipe(eslint.format());
|
||||
});
|
||||
|
||||
gulp.task('css', () =>
|
||||
{
|
||||
return gulp.src('stylus/index.styl')
|
||||
.pipe(plumber())
|
||||
.pipe(stylus(
|
||||
{
|
||||
use : nib(),
|
||||
compress : process.env.NODE_ENV === 'production'
|
||||
}))
|
||||
.on('error', logError)
|
||||
.pipe(cssBase64(
|
||||
{
|
||||
baseDir : '.',
|
||||
maxWeightResource : 50000 // So big ttf fonts are not included, nice.
|
||||
}))
|
||||
.pipe(rename(`${PKG.name}.css`))
|
||||
.pipe(gulp.dest(OUTPUT_DIR))
|
||||
.pipe(touch());
|
||||
});
|
||||
|
||||
gulp.task('html', () =>
|
||||
{
|
||||
return gulp.src('index.html')
|
||||
.pipe(gulp.dest(OUTPUT_DIR));
|
||||
});
|
||||
|
||||
gulp.task('resources', (done) =>
|
||||
{
|
||||
let dst = path.join(OUTPUT_DIR, 'resources');
|
||||
|
||||
mkdirp.sync(dst);
|
||||
ncp('resources', dst, { stopOnErr: true }, (error) =>
|
||||
{
|
||||
if (error && error[0].code !== 'ENOENT')
|
||||
throw new Error(`resources copy failed: ${error}`);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('bundle', () =>
|
||||
{
|
||||
return bundle({ watch: false });
|
||||
});
|
||||
|
||||
gulp.task('bundle:watch', () =>
|
||||
{
|
||||
return bundle({ watch: true });
|
||||
});
|
||||
|
||||
gulp.task('livebrowser', (done) =>
|
||||
{
|
||||
const config = require('../server/config');
|
||||
|
||||
browserSync(
|
||||
{
|
||||
open : 'external',
|
||||
host : config.domain,
|
||||
server :
|
||||
{
|
||||
baseDir : OUTPUT_DIR
|
||||
},
|
||||
https : config.tls,
|
||||
ghostMode : false,
|
||||
files : path.join(OUTPUT_DIR, '**', '*')
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('browser', (done) =>
|
||||
{
|
||||
const config = require('../server/config');
|
||||
|
||||
browserSync(
|
||||
{
|
||||
open : 'external',
|
||||
host : config.domain,
|
||||
server :
|
||||
{
|
||||
baseDir : OUTPUT_DIR
|
||||
},
|
||||
https : config.tls,
|
||||
ghostMode : false
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('watch', (done) =>
|
||||
{
|
||||
// Watch changes in HTML.
|
||||
gulp.watch([ 'index.html' ], gulp.series(
|
||||
'html'
|
||||
));
|
||||
|
||||
// Watch changes in Stylus files.
|
||||
gulp.watch([ 'stylus/**/*.styl' ], gulp.series(
|
||||
'css'
|
||||
));
|
||||
|
||||
// Watch changes in resources.
|
||||
gulp.watch([ 'resources/**/*' ], gulp.series(
|
||||
'resources', 'css'
|
||||
));
|
||||
|
||||
// Watch changes in JS files.
|
||||
gulp.watch([ 'gulpfile.js', 'lib/**/*.js', 'lib/**/*.jsx' ], gulp.series(
|
||||
'lint'
|
||||
));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('prod', gulp.series(
|
||||
'env:prod',
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle',
|
||||
'html',
|
||||
'css',
|
||||
'resources'
|
||||
));
|
||||
|
||||
gulp.task('dev', gulp.series(
|
||||
'env:dev',
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle',
|
||||
'html',
|
||||
'css',
|
||||
'resources'
|
||||
));
|
||||
|
||||
gulp.task('live', gulp.series(
|
||||
'env:dev',
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle:watch',
|
||||
'html',
|
||||
'css',
|
||||
'resources',
|
||||
'watch',
|
||||
'livebrowser'
|
||||
));
|
||||
|
||||
gulp.task('open', gulp.series('browser'));
|
||||
|
||||
gulp.task('default', gulp.series('live'));
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>mediasoup demo</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 demo - Cutting Edge WebRTC Video Conferencing'>
|
||||
|
||||
<link rel='stylesheet' href='/mediasoup-demo-app.css'>
|
||||
|
||||
<script src='/resources/js/antiglobal.js'></script>
|
||||
<script>
|
||||
window.localStorage.setItem('debug', '* -engine* -socket* *WARN* *ERROR*');
|
||||
|
||||
if (window.antiglobal)
|
||||
{
|
||||
window.antiglobal('___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__', 'RTCPeerConnection');
|
||||
setInterval(window.antiglobal, 5000);
|
||||
}
|
||||
</script>
|
||||
<script async src='/mediasoup-demo-app.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='mediasoup-demo-app-container'></div>
|
||||
<div id='mediasoup-demo-app-media-query-detector'></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,897 @@
|
||||
'use strict';
|
||||
|
||||
import events from 'events';
|
||||
import browser from 'bowser';
|
||||
import sdpTransform from 'sdp-transform';
|
||||
import Logger from './Logger';
|
||||
import protooClient from 'protoo-client';
|
||||
import urlFactory from './urlFactory';
|
||||
import utils from './utils';
|
||||
|
||||
const logger = new Logger('Client');
|
||||
|
||||
const DO_GETUSERMEDIA = true;
|
||||
const ENABLE_SIMULCAST = false;
|
||||
const VIDEO_CONSTRAINS =
|
||||
{
|
||||
qvga : { width: { ideal: 320 }, height: { ideal: 240 }},
|
||||
vga : { width: { ideal: 640 }, height: { ideal: 480 }},
|
||||
hd : { width: { ideal: 1280 }, height: { ideal: 720 }}
|
||||
};
|
||||
|
||||
export default class Client extends events.EventEmitter
|
||||
{
|
||||
constructor(peerId, roomId)
|
||||
{
|
||||
logger.debug('constructor() [peerId:"%s", roomId:"%s"]', peerId, roomId);
|
||||
|
||||
super();
|
||||
this.setMaxListeners(Infinity);
|
||||
|
||||
let url = urlFactory.getProtooUrl(peerId, roomId);
|
||||
let transport = new protooClient.WebSocketTransport(url);
|
||||
|
||||
// protoo-client Peer instance.
|
||||
this._protooPeer = new protooClient.Peer(transport);
|
||||
|
||||
// RTCPeerConnection instance.
|
||||
this._peerconnection = null;
|
||||
|
||||
// Webcam map indexed by deviceId.
|
||||
this._webcams = new Map();
|
||||
|
||||
// Local Webcam device.
|
||||
this._webcam = null;
|
||||
|
||||
// Local MediaStream instance.
|
||||
this._localStream = null;
|
||||
|
||||
// Closed flag.
|
||||
this._closed = false;
|
||||
|
||||
// Local video resolution.
|
||||
this._localVideoResolution = 'vga';
|
||||
|
||||
this._protooPeer.on('open', () =>
|
||||
{
|
||||
logger.debug('protoo Peer "open" event');
|
||||
});
|
||||
|
||||
this._protooPeer.on('disconnected', () =>
|
||||
{
|
||||
logger.warn('protoo Peer "disconnected" event');
|
||||
|
||||
// Close RTCPeerConnection.
|
||||
try
|
||||
{
|
||||
this._peerconnection.close();
|
||||
}
|
||||
catch (error) {}
|
||||
|
||||
// Close local MediaStream.
|
||||
if (this._localStream)
|
||||
utils.closeMediaStream(this._localStream);
|
||||
|
||||
this.emit('disconnected');
|
||||
});
|
||||
|
||||
this._protooPeer.on('close', () =>
|
||||
{
|
||||
if (this._closed)
|
||||
return;
|
||||
|
||||
logger.warn('protoo Peer "close" event');
|
||||
|
||||
this.close();
|
||||
});
|
||||
|
||||
this._protooPeer.on('request', this._handleRequest.bind(this));
|
||||
}
|
||||
|
||||
close()
|
||||
{
|
||||
if (this._closed)
|
||||
return;
|
||||
|
||||
this._closed = true;
|
||||
|
||||
logger.debug('close()');
|
||||
|
||||
// Close protoo Peer.
|
||||
this._protooPeer.close();
|
||||
|
||||
// Close RTCPeerConnection.
|
||||
try
|
||||
{
|
||||
this._peerconnection.close();
|
||||
}
|
||||
catch (error) {}
|
||||
|
||||
// Close local MediaStream.
|
||||
if (this._localStream)
|
||||
utils.closeMediaStream(this._localStream);
|
||||
|
||||
// Emit 'close' event.
|
||||
this.emit('close');
|
||||
}
|
||||
|
||||
removeVideo(dontNegotiate)
|
||||
{
|
||||
logger.debug('removeVideo()');
|
||||
|
||||
let stream = this._localStream;
|
||||
let videoTrack = stream.getVideoTracks()[0];
|
||||
|
||||
if (!videoTrack)
|
||||
{
|
||||
logger.warn('removeVideo() | no video track');
|
||||
|
||||
return Promise.reject(new Error('no video track'));
|
||||
}
|
||||
|
||||
stream.removeTrack(videoTrack);
|
||||
videoTrack.stop();
|
||||
|
||||
// NOTE: For Firefox (modern WenRTC API).
|
||||
if (this._peerconnection.removeTrack)
|
||||
{
|
||||
let sender;
|
||||
|
||||
for (sender of this._peerconnection.getSenders())
|
||||
{
|
||||
if (sender.track === videoTrack)
|
||||
break;
|
||||
}
|
||||
|
||||
this._peerconnection.removeTrack(sender);
|
||||
}
|
||||
|
||||
if (!dontNegotiate)
|
||||
{
|
||||
this.emit('localstream', stream, null);
|
||||
|
||||
return this._requestRenegotiation();
|
||||
}
|
||||
}
|
||||
|
||||
addVideo()
|
||||
{
|
||||
logger.debug('addVideo()');
|
||||
|
||||
let stream = this._localStream;
|
||||
let videoTrack;
|
||||
let videoResolution = this._localVideoResolution; // Keep previous resolution.
|
||||
|
||||
if (stream)
|
||||
videoTrack = stream.getVideoTracks()[0];
|
||||
|
||||
if (videoTrack)
|
||||
{
|
||||
logger.warn('addVideo() | there is already a video track');
|
||||
|
||||
return Promise.reject(new Error('there is already a video track'));
|
||||
}
|
||||
|
||||
return this._getLocalStream(
|
||||
{
|
||||
video : VIDEO_CONSTRAINS[videoResolution]
|
||||
})
|
||||
.then((newStream) =>
|
||||
{
|
||||
let newVideoTrack = newStream.getVideoTracks()[0];
|
||||
|
||||
if (stream)
|
||||
{
|
||||
stream.addTrack(newVideoTrack);
|
||||
|
||||
// NOTE: For Firefox (modern WenRTC API).
|
||||
if (this._peerconnection.addTrack)
|
||||
this._peerconnection.addTrack(newVideoTrack, this._localStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._localStream = newStream;
|
||||
this._peerconnection.addStream(newStream);
|
||||
}
|
||||
|
||||
this.emit('localstream', this._localStream, videoResolution);
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
return this._requestRenegotiation();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('addVideo() failed: %o', error);
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
changeWebcam()
|
||||
{
|
||||
logger.debug('changeWebcam()');
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
return this._updateWebcams();
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let array = Array.from(this._webcams.keys());
|
||||
let len = array.length;
|
||||
let deviceId = this._webcam ? this._webcam.deviceId : undefined;
|
||||
let idx = array.indexOf(deviceId);
|
||||
|
||||
if (idx < len - 1)
|
||||
idx++;
|
||||
else
|
||||
idx = 0;
|
||||
|
||||
this._webcam = this._webcams.get(array[idx]);
|
||||
|
||||
this._emitWebcamType();
|
||||
|
||||
if (len < 2)
|
||||
return;
|
||||
|
||||
logger.debug(
|
||||
'changeWebcam() | new selected webcam [deviceId:"%s"]',
|
||||
this._webcam.deviceId);
|
||||
|
||||
// Reset video resolution to VGA.
|
||||
this._localVideoResolution = 'vga';
|
||||
|
||||
// For Chrome (old WenRTC API).
|
||||
// Replace the track (so new SSRC) and renegotiate.
|
||||
if (!this._peerconnection.removeTrack)
|
||||
{
|
||||
this.removeVideo(true);
|
||||
|
||||
return this.addVideo();
|
||||
}
|
||||
// For Firefox (modern WebRTC API).
|
||||
// Avoid renegotiation.
|
||||
else
|
||||
{
|
||||
return this._getLocalStream(
|
||||
{
|
||||
video : VIDEO_CONSTRAINS[this._localVideoResolution]
|
||||
})
|
||||
.then((newStream) =>
|
||||
{
|
||||
let newVideoTrack = newStream.getVideoTracks()[0];
|
||||
let stream = this._localStream;
|
||||
let oldVideoTrack = stream.getVideoTracks()[0];
|
||||
let sender;
|
||||
|
||||
for (sender of this._peerconnection.getSenders())
|
||||
{
|
||||
if (sender.track === oldVideoTrack)
|
||||
break;
|
||||
}
|
||||
|
||||
sender.replaceTrack(newVideoTrack);
|
||||
stream.removeTrack(oldVideoTrack);
|
||||
oldVideoTrack.stop();
|
||||
stream.addTrack(newVideoTrack);
|
||||
|
||||
this.emit('localstream', stream, this._localVideoResolution);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('changeWebcam() failed: %o', error);
|
||||
});
|
||||
}
|
||||
|
||||
changeVideoResolution()
|
||||
{
|
||||
logger.debug('changeVideoResolution()');
|
||||
|
||||
let newVideoResolution;
|
||||
|
||||
switch (this._localVideoResolution)
|
||||
{
|
||||
case 'qvga':
|
||||
newVideoResolution = 'vga';
|
||||
break;
|
||||
case 'vga':
|
||||
newVideoResolution = 'hd';
|
||||
break;
|
||||
case 'hd':
|
||||
newVideoResolution = 'qvga';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unknown resolution "${this._localVideoResolution}"`);
|
||||
}
|
||||
|
||||
this._localVideoResolution = newVideoResolution;
|
||||
|
||||
// For Chrome (old WenRTC API).
|
||||
// Replace the track (so new SSRC) and renegotiate.
|
||||
if (!this._peerconnection.removeTrack)
|
||||
{
|
||||
this.removeVideo(true);
|
||||
|
||||
return this.addVideo();
|
||||
}
|
||||
// For Firefox (modern WebRTC API).
|
||||
// Avoid renegotiation.
|
||||
else
|
||||
{
|
||||
return this._getLocalStream(
|
||||
{
|
||||
video : VIDEO_CONSTRAINS[this._localVideoResolution]
|
||||
})
|
||||
.then((newStream) =>
|
||||
{
|
||||
let newVideoTrack = newStream.getVideoTracks()[0];
|
||||
let stream = this._localStream;
|
||||
let oldVideoTrack = stream.getVideoTracks()[0];
|
||||
let sender;
|
||||
|
||||
for (sender of this._peerconnection.getSenders())
|
||||
{
|
||||
if (sender.track === oldVideoTrack)
|
||||
break;
|
||||
}
|
||||
|
||||
sender.replaceTrack(newVideoTrack);
|
||||
stream.removeTrack(oldVideoTrack);
|
||||
oldVideoTrack.stop();
|
||||
stream.addTrack(newVideoTrack);
|
||||
|
||||
this.emit('localstream', stream, newVideoResolution);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('changeVideoResolution() failed: %o', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getStats()
|
||||
{
|
||||
return this._peerconnection.getStats()
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('pc.getStats() failed: %o', error);
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
disableRemoteVideo(msid)
|
||||
{
|
||||
this._protooPeer.send('disableremotevideo', { msid, disable: true })
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.warn('disableRemoteVideo() failed: %o', error);
|
||||
});
|
||||
}
|
||||
|
||||
enableRemoteVideo(msid)
|
||||
{
|
||||
this._protooPeer.send('disableremotevideo', { msid, disable: false })
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.warn('enableRemoteVideo() failed: %o', error);
|
||||
});
|
||||
}
|
||||
|
||||
_handleRequest(request, accept, reject)
|
||||
{
|
||||
logger.debug('_handleRequest() [method:%s, data:%o]', request.method, request.data);
|
||||
|
||||
switch(request.method)
|
||||
{
|
||||
case 'joinme':
|
||||
{
|
||||
let videoResolution = this._localVideoResolution;
|
||||
|
||||
Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
return this._updateWebcams();
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
if (DO_GETUSERMEDIA)
|
||||
{
|
||||
return this._getLocalStream(
|
||||
{
|
||||
audio : true,
|
||||
video : VIDEO_CONSTRAINS[videoResolution]
|
||||
})
|
||||
.then((stream) =>
|
||||
{
|
||||
logger.debug('got local stream [resolution:%s]', videoResolution);
|
||||
|
||||
// Close local MediaStream if any.
|
||||
if (this._localStream)
|
||||
utils.closeMediaStream(this._localStream);
|
||||
|
||||
this._localStream = stream;
|
||||
|
||||
// Emit 'localstream' event.
|
||||
this.emit('localstream', stream, videoResolution);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
return this._createPeerConnection();
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
return this._peerconnection.createOffer(
|
||||
{
|
||||
offerToReceiveAudio : 1,
|
||||
offerToReceiveVideo : 1
|
||||
});
|
||||
})
|
||||
.then((offer) =>
|
||||
{
|
||||
let capabilities = offer.sdp;
|
||||
let parsedSdp = sdpTransform.parse(capabilities);
|
||||
|
||||
logger.debug('capabilities [parsed:%O, sdp:%s]', parsedSdp, capabilities);
|
||||
|
||||
// Accept the protoo request.
|
||||
accept(
|
||||
{
|
||||
capabilities : capabilities,
|
||||
usePlanB : utils.isPlanB()
|
||||
});
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
logger.debug('"joinme" request accepted');
|
||||
|
||||
// Emit 'join' event.
|
||||
this.emit('join');
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('"joinme" request failed: %o', error);
|
||||
|
||||
reject(500, error.message);
|
||||
throw error;
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'peers':
|
||||
{
|
||||
this.emit('peers', request.data.peers);
|
||||
accept();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'addpeer':
|
||||
{
|
||||
this.emit('addpeer', request.data.peer);
|
||||
accept();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updatepeer':
|
||||
{
|
||||
this.emit('updatepeer', request.data.peer);
|
||||
accept();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'removepeer':
|
||||
{
|
||||
this.emit('removepeer', request.data.peer);
|
||||
accept();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'offer':
|
||||
{
|
||||
let offer = new RTCSessionDescription(request.data.offer);
|
||||
let parsedSdp = sdpTransform.parse(offer.sdp);
|
||||
|
||||
logger.debug('received offer [parsed:%O, sdp:%s]', parsedSdp, offer.sdp);
|
||||
|
||||
Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
return this._peerconnection.setRemoteDescription(offer);
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
return this._peerconnection.createAnswer();
|
||||
})
|
||||
// Play with simulcast.
|
||||
.then((answer) =>
|
||||
{
|
||||
if (!ENABLE_SIMULCAST)
|
||||
return answer;
|
||||
|
||||
// Chrome Plan B simulcast.
|
||||
if (utils.isPlanB())
|
||||
{
|
||||
// Just for the initial offer.
|
||||
// NOTE: Otherwise Chrome crashes.
|
||||
// TODO: This prevents simulcast to be applied to new tracks.
|
||||
if (this._peerconnection.localDescription && this._peerconnection.localDescription.sdp)
|
||||
return answer;
|
||||
|
||||
// TODO: Should be done just for VP8.
|
||||
let parsedSdp = sdpTransform.parse(answer.sdp);
|
||||
let videoMedia;
|
||||
|
||||
for (let m of parsedSdp.media)
|
||||
{
|
||||
if (m.type === 'video')
|
||||
{
|
||||
videoMedia = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoMedia || !videoMedia.ssrcs)
|
||||
return answer;
|
||||
|
||||
logger.debug('setting video simulcast (PlanB)');
|
||||
|
||||
let ssrc1;
|
||||
let ssrc2;
|
||||
let ssrc3;
|
||||
let cname;
|
||||
let msid;
|
||||
|
||||
for (let ssrcObj of videoMedia.ssrcs)
|
||||
{
|
||||
// Chrome uses:
|
||||
// a=ssrc:xxxx msid:yyyy zzzz
|
||||
// a=ssrc:xxxx mslabel:yyyy
|
||||
// a=ssrc:xxxx label:zzzz
|
||||
// Where yyyy is the MediaStream.id and zzzz the MediaStreamTrack.id.
|
||||
switch (ssrcObj.attribute)
|
||||
{
|
||||
case 'cname':
|
||||
ssrc1 = ssrcObj.id;
|
||||
cname = ssrcObj.value;
|
||||
break;
|
||||
|
||||
case 'msid':
|
||||
msid = ssrcObj.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ssrc2 = ssrc1 + 1;
|
||||
ssrc3 = ssrc1 + 2;
|
||||
|
||||
videoMedia.ssrcGroups =
|
||||
[
|
||||
{
|
||||
semantics : 'SIM',
|
||||
ssrcs : `${ssrc1} ${ssrc2} ${ssrc3}`
|
||||
}
|
||||
];
|
||||
|
||||
videoMedia.ssrcs =
|
||||
[
|
||||
{
|
||||
id : ssrc1,
|
||||
attribute : 'cname',
|
||||
value : cname,
|
||||
},
|
||||
{
|
||||
id : ssrc1,
|
||||
attribute : 'msid',
|
||||
value : msid,
|
||||
},
|
||||
{
|
||||
id : ssrc2,
|
||||
attribute : 'cname',
|
||||
value : cname,
|
||||
},
|
||||
{
|
||||
id : ssrc2,
|
||||
attribute : 'msid',
|
||||
value : msid,
|
||||
},
|
||||
{
|
||||
id : ssrc3,
|
||||
attribute : 'cname',
|
||||
value : cname,
|
||||
},
|
||||
{
|
||||
id : ssrc3,
|
||||
attribute : 'msid',
|
||||
value : msid,
|
||||
}
|
||||
];
|
||||
|
||||
let modifiedAnswer =
|
||||
{
|
||||
type : 'answer',
|
||||
sdp : sdpTransform.write(parsedSdp)
|
||||
};
|
||||
|
||||
return modifiedAnswer;
|
||||
}
|
||||
// Firefox way.
|
||||
else
|
||||
{
|
||||
let parsedSdp = sdpTransform.parse(answer.sdp);
|
||||
let videoMedia;
|
||||
|
||||
logger.debug('created answer [parsed:%O, sdp:%s]', parsedSdp, answer.sdp);
|
||||
|
||||
for (let m of parsedSdp.media)
|
||||
{
|
||||
if (m.type === 'video' && m.direction === 'sendonly')
|
||||
{
|
||||
videoMedia = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoMedia)
|
||||
return answer;
|
||||
|
||||
logger.debug('setting video simulcast (Unified-Plan)');
|
||||
|
||||
videoMedia.simulcast_03 =
|
||||
{
|
||||
value : 'send rid=1,2'
|
||||
};
|
||||
|
||||
videoMedia.rids =
|
||||
[
|
||||
{ id: '1', direction: 'send' },
|
||||
{ id: '2', direction: 'send' }
|
||||
];
|
||||
|
||||
let modifiedAnswer =
|
||||
{
|
||||
type : 'answer',
|
||||
sdp : sdpTransform.write(parsedSdp)
|
||||
};
|
||||
|
||||
return modifiedAnswer;
|
||||
}
|
||||
})
|
||||
.then((answer) =>
|
||||
{
|
||||
return this._peerconnection.setLocalDescription(answer);
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let answer = this._peerconnection.localDescription;
|
||||
let parsedSdp = sdpTransform.parse(answer.sdp);
|
||||
|
||||
logger.debug('sent answer [parsed:%O, sdp:%s]', parsedSdp, answer.sdp);
|
||||
|
||||
accept(
|
||||
{
|
||||
answer :
|
||||
{
|
||||
type : answer.type,
|
||||
sdp : answer.sdp
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('"offer" request failed: %o', error);
|
||||
|
||||
reject(500, error.message);
|
||||
throw error;
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
// If Firefox trigger 'forcestreamsupdate' event due to bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
if (browser.firefox || browser.gecko)
|
||||
{
|
||||
// Not sure, but it thinks that the timeout does the trick.
|
||||
setTimeout(() => this.emit('forcestreamsupdate'), 500);
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
logger.error('unknown method');
|
||||
|
||||
reject(404, 'unknown method');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_updateWebcams()
|
||||
{
|
||||
logger.debug('_updateWebcams()');
|
||||
|
||||
// Reset the list.
|
||||
this._webcams = new Map();
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
return navigator.mediaDevices.enumerateDevices();
|
||||
})
|
||||
.then((devices) =>
|
||||
{
|
||||
for (let device of devices)
|
||||
{
|
||||
if (device.kind !== 'videoinput')
|
||||
continue;
|
||||
|
||||
this._webcams.set(device.deviceId, device);
|
||||
}
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let array = Array.from(this._webcams.values());
|
||||
let len = array.length;
|
||||
let currentWebcamId = this._webcam ? this._webcam.deviceId : undefined;
|
||||
|
||||
logger.debug('_updateWebcams() [webcams:%o]', array);
|
||||
|
||||
if (len === 0)
|
||||
this._webcam = null;
|
||||
else if (!this._webcams.has(currentWebcamId))
|
||||
this._webcam = array[0];
|
||||
|
||||
this.emit('numwebcams', len);
|
||||
|
||||
this._emitWebcamType();
|
||||
});
|
||||
}
|
||||
|
||||
_getLocalStream(constraints)
|
||||
{
|
||||
logger.debug('_getLocalStream() [constraints:%o, webcam:%o]',
|
||||
constraints, this._webcam);
|
||||
|
||||
if (this._webcam)
|
||||
constraints.video.deviceId = { exact: this._webcam.deviceId };
|
||||
|
||||
return navigator.mediaDevices.getUserMedia(constraints);
|
||||
}
|
||||
|
||||
_createPeerConnection()
|
||||
{
|
||||
logger.debug('_createPeerConnection()');
|
||||
|
||||
this._peerconnection = new RTCPeerConnection({ iceServers: [] });
|
||||
|
||||
// TODO: TMP
|
||||
global.CLIENT = this;
|
||||
global.PC = this._peerconnection;
|
||||
|
||||
if (this._localStream)
|
||||
this._peerconnection.addStream(this._localStream);
|
||||
|
||||
this._peerconnection.addEventListener('iceconnectionstatechange', () =>
|
||||
{
|
||||
let state = this._peerconnection.iceConnectionState;
|
||||
|
||||
logger.debug('peerconnection "iceconnectionstatechange" event [state:%s]', state);
|
||||
|
||||
this.emit('connectionstate', state);
|
||||
});
|
||||
|
||||
this._peerconnection.addEventListener('addstream', (event) =>
|
||||
{
|
||||
let stream = event.stream;
|
||||
|
||||
logger.debug('peerconnection "addstream" event [stream:%o]', stream);
|
||||
|
||||
this.emit('addstream', stream);
|
||||
|
||||
// NOTE: For testing.
|
||||
let interval = setInterval(() =>
|
||||
{
|
||||
if (!stream.active)
|
||||
{
|
||||
logger.warn('stream inactive [stream:%o]', stream);
|
||||
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
stream.addEventListener('addtrack', (event) =>
|
||||
{
|
||||
let track = event.track;
|
||||
|
||||
logger.debug('stream "addtrack" event [track:%o]', track);
|
||||
|
||||
this.emit('addtrack', track);
|
||||
|
||||
// Firefox does not implement 'stream.onremovetrack' so let's use 'track.ended'.
|
||||
// But... track "ended" is neither fired.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
track.addEventListener('ended', () =>
|
||||
{
|
||||
logger.debug('track "ended" event [track:%o]', track);
|
||||
|
||||
this.emit('removetrack', track);
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: Not implemented in Firefox.
|
||||
stream.addEventListener('removetrack', (event) =>
|
||||
{
|
||||
let track = event.track;
|
||||
|
||||
logger.debug('stream "removetrack" event [track:%o]', track);
|
||||
|
||||
this.emit('removetrack', track);
|
||||
});
|
||||
});
|
||||
|
||||
this._peerconnection.addEventListener('removestream', (event) =>
|
||||
{
|
||||
let stream = event.stream;
|
||||
|
||||
logger.debug('peerconnection "removestream" event [stream:%o]', stream);
|
||||
|
||||
this.emit('removestream', stream);
|
||||
});
|
||||
}
|
||||
|
||||
_requestRenegotiation()
|
||||
{
|
||||
logger.debug('_requestRenegotiation()');
|
||||
|
||||
return this._protooPeer.send('reofferme');
|
||||
}
|
||||
|
||||
_restartIce()
|
||||
{
|
||||
logger.debug('_restartIce()');
|
||||
|
||||
return this._protooPeer.send('restartice')
|
||||
.then(() =>
|
||||
{
|
||||
logger.debug('_restartIce() succeded');
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('_restartIce() failed: %o', error);
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
_emitWebcamType()
|
||||
{
|
||||
let webcam = this._webcam;
|
||||
|
||||
if (!webcam)
|
||||
return;
|
||||
|
||||
if (/(back|rear)/i.test(webcam.label))
|
||||
{
|
||||
logger.debug('_emitWebcamType() | it seems to be a back camera');
|
||||
|
||||
this.emit('webcamtype', 'back');
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug('_emitWebcamType() | it seems to be a front camera');
|
||||
|
||||
this.emit('webcamtype', 'front');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
import debug from 'debug';
|
||||
|
||||
const APP_NAME = 'mediasoup-demo';
|
||||
|
||||
export default class Logger
|
||||
{
|
||||
constructor(prefix)
|
||||
{
|
||||
if (prefix)
|
||||
{
|
||||
this._debug = debug(APP_NAME + ':' + prefix);
|
||||
this._warn = debug(APP_NAME + ':WARN:' + prefix);
|
||||
this._error = debug(APP_NAME + ':ERROR:' + prefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._debug = debug(APP_NAME);
|
||||
this._warn = debug(APP_NAME + ':WARN');
|
||||
this._error = debug(APP_NAME + ':ERROR');
|
||||
}
|
||||
|
||||
this._debug.log = console.info.bind(console);
|
||||
this._warn.log = console.warn.bind(console);
|
||||
this._error.log = console.error.bind(console);
|
||||
}
|
||||
|
||||
get debug()
|
||||
{
|
||||
return this._debug;
|
||||
}
|
||||
|
||||
get warn()
|
||||
{
|
||||
return this._warn;
|
||||
}
|
||||
|
||||
get error()
|
||||
{
|
||||
return this._error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
|
||||
import Logger from '../Logger';
|
||||
import muiTheme from './muiTheme';
|
||||
import Notifier from './Notifier';
|
||||
import Room from './Room';
|
||||
|
||||
const logger = new Logger('App'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class App extends React.Component
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<div data-component='App'>
|
||||
<Notifier ref='Notifier'/>
|
||||
|
||||
<Room
|
||||
peerId={props.peerId}
|
||||
roomId={props.roomId}
|
||||
onNotify={this.handleNotify.bind(this)}
|
||||
onHideNotification={this.handleHideNotification.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
handleNotify(data)
|
||||
{
|
||||
this.refs.Notifier.notify(data);
|
||||
}
|
||||
|
||||
handleHideNotification(uid)
|
||||
{
|
||||
this.refs.Notifier.hideNotification(uid);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes =
|
||||
{
|
||||
peerId : React.PropTypes.string.isRequired,
|
||||
roomId : React.PropTypes.string.isRequired
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import MicOffIcon from 'material-ui/svg-icons/av/mic-off';
|
||||
import VideoCamOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import ChangeVideoCamIcon from 'material-ui/svg-icons/av/repeat';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('LocalVideo'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class LocalVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
micMuted : false,
|
||||
webcam : props.stream && !!props.stream.getVideoTracks()[0],
|
||||
togglingWebcam : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='LocalVideo' className={`state-${props.connectionState}`}>
|
||||
{props.stream ?
|
||||
<Video
|
||||
stream={props.stream}
|
||||
resolution={props.resolution}
|
||||
muted
|
||||
mirror={props.webcamType === 'front'}
|
||||
onResolutionChange={this.handleResolutionChange.bind(this)}
|
||||
/>
|
||||
:null}
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteMic.bind(this)}
|
||||
>
|
||||
<MicOffIcon
|
||||
color={!state.micMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={state.togglingWebcam}
|
||||
onClick={this.handleClickWebcam.bind(this)}
|
||||
>
|
||||
<VideoCamOffIcon
|
||||
color={state.webcam ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
{props.multipleWebcams ?
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={!state.webcam || state.togglingWebcam}
|
||||
onClick={this.handleClickChangeWebcam.bind(this)}
|
||||
>
|
||||
<ChangeVideoCamIcon
|
||||
color='#fff'
|
||||
/>
|
||||
</IconButton>
|
||||
:null}
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peerId}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
this.setState({ webcam: nextProps.stream && !!nextProps.stream.getVideoTracks()[0] });
|
||||
}
|
||||
|
||||
handleClickMuteMic()
|
||||
{
|
||||
logger.debug('handleClickMuteMic()');
|
||||
|
||||
let value = !this.state.micMuted;
|
||||
|
||||
this.props.onMicMute(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ micMuted: value });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickWebcam()
|
||||
{
|
||||
logger.debug('handleClickWebcam()');
|
||||
|
||||
let value = !this.state.webcam;
|
||||
|
||||
this.setState({ togglingWebcam: true });
|
||||
|
||||
this.props.onWebcamToggle(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ webcam: value, togglingWebcam: false });
|
||||
})
|
||||
.catch(() =>
|
||||
{
|
||||
this.setState({ togglingWebcam: false });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickChangeWebcam()
|
||||
{
|
||||
logger.debug('handleClickChangeWebcam()');
|
||||
|
||||
this.props.onWebcamChange();
|
||||
}
|
||||
|
||||
handleResolutionChange()
|
||||
{
|
||||
logger.debug('handleResolutionChange()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
}
|
||||
|
||||
LocalVideo.propTypes =
|
||||
{
|
||||
peerId : React.PropTypes.string.isRequired,
|
||||
stream : React.PropTypes.object,
|
||||
resolution : React.PropTypes.string,
|
||||
multipleWebcams : React.PropTypes.bool.isRequired,
|
||||
webcamType : React.PropTypes.string,
|
||||
connectionState : React.PropTypes.string,
|
||||
onMicMute : React.PropTypes.func.isRequired,
|
||||
onWebcamToggle : React.PropTypes.func.isRequired,
|
||||
onWebcamChange : React.PropTypes.func.isRequired,
|
||||
onResolutionChange : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import NotificationSystem from 'react-notification-system';
|
||||
|
||||
const STYLE =
|
||||
{
|
||||
NotificationItem :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '6px 10px',
|
||||
backgroundColor : 'rgba(255,255,255, 0.9)',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 400,
|
||||
fontSize : '1rem',
|
||||
cursor : 'default',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none',
|
||||
userSelect : 'none',
|
||||
transition : '0.15s ease-in-out'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '2px solid rgba(255,0,78, 0.75)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid rgba(73,206,62, 0.75)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid #ff0014'
|
||||
}
|
||||
},
|
||||
Title :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
margin : '0 0 8px 0',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 500,
|
||||
fontSize : '1.1rem',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : 'rgba(255,0,78, 0.85)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : 'rgba(73,206,62, 0.9)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#ff0014'
|
||||
}
|
||||
},
|
||||
Dismiss :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
display : 'none'
|
||||
}
|
||||
},
|
||||
Action :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '8px 24px',
|
||||
fontSize : '1.2rem',
|
||||
cursor : 'pointer',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
backgroundColor : 'rgba(255,0,78, 1)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
backgroundColor : 'rgba(73,206,62, 0.75)'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class Notifier extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<NotificationSystem ref='NotificationSystem' style={STYLE} allowHTML={false}/>
|
||||
);
|
||||
}
|
||||
|
||||
notify(data)
|
||||
{
|
||||
let data2;
|
||||
|
||||
switch (data.level)
|
||||
{
|
||||
case 'info' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'success' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'error' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 3
|
||||
}, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`unknown level "${data.level}"`);
|
||||
}
|
||||
|
||||
this.refs.NotificationSystem.addNotification(data2);
|
||||
}
|
||||
|
||||
hideNotification(uid)
|
||||
{
|
||||
this.refs.NotificationSystem.removeNotification(uid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import VolumeOffIcon from 'material-ui/svg-icons/av/volume-off';
|
||||
import VideoOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import classnames from 'classnames';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('RemoteVideo'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class RemoteVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
audioMuted : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let hasVideo = !!props.stream.getVideoTracks()[0];
|
||||
|
||||
global.SS = props.stream;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='RemoteVideo'
|
||||
className={classnames({ fullsize: !!props.fullsize })}
|
||||
>
|
||||
<Video
|
||||
stream={props.stream}
|
||||
muted={state.audioMuted}
|
||||
/>
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteAudio.bind(this)}
|
||||
>
|
||||
<VolumeOffIcon
|
||||
color={!state.audioMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickDisableVideo.bind(this)}
|
||||
>
|
||||
<VideoOffIcon
|
||||
color={hasVideo ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peer.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleClickMuteAudio()
|
||||
{
|
||||
logger.debug('handleClickMuteAudio()');
|
||||
|
||||
let value = !this.state.audioMuted;
|
||||
|
||||
this.setState({ audioMuted: value });
|
||||
}
|
||||
|
||||
handleClickDisableVideo()
|
||||
{
|
||||
logger.debug('handleClickDisableVideo()');
|
||||
|
||||
let stream = this.props.stream;
|
||||
let msid = stream.id;
|
||||
let hasVideo = !!stream.getVideoTracks()[0];
|
||||
|
||||
if (hasVideo)
|
||||
this.props.onDisableVideo(msid);
|
||||
else
|
||||
this.props.onEnableVideo(msid);
|
||||
}
|
||||
}
|
||||
|
||||
RemoteVideo.propTypes =
|
||||
{
|
||||
peer : React.PropTypes.object.isRequired,
|
||||
stream : React.PropTypes.object.isRequired,
|
||||
fullsize : React.PropTypes.bool,
|
||||
onDisableVideo : React.PropTypes.func.isRequired,
|
||||
onEnableVideo : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,487 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ClipboardButton from 'react-clipboard.js';
|
||||
import TransitionAppear from './TransitionAppear';
|
||||
import LocalVideo from './LocalVideo';
|
||||
import RemoteVideo from './RemoteVideo';
|
||||
import Stats from './Stats';
|
||||
import Logger from '../Logger';
|
||||
import utils from '../utils';
|
||||
import Client from '../Client';
|
||||
|
||||
const logger = new Logger('Room');
|
||||
const STATS_INTERVAL = 1000;
|
||||
|
||||
export default class Room extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
peers : {},
|
||||
localStream : null,
|
||||
localVideoResolution : null, // qvga / vga / hd / fullhd.
|
||||
multipleWebcams : false,
|
||||
webcamType : null,
|
||||
connectionState : null,
|
||||
remoteStreams : {},
|
||||
showStats : false,
|
||||
stats : null
|
||||
};
|
||||
|
||||
// Mounted flag
|
||||
this._mounted = false;
|
||||
// Client instance
|
||||
this._client = null;
|
||||
// Timer to retrieve RTC stats.
|
||||
this._statsTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let numPeers = Object.keys(state.remoteStreams).length;
|
||||
|
||||
return (
|
||||
<TransitionAppear duration={2000}>
|
||||
<div data-component='Room'>
|
||||
|
||||
<div className='room-link-wrapper'>
|
||||
<div className='room-link'>
|
||||
<ClipboardButton
|
||||
component='a'
|
||||
className='link'
|
||||
button-href={window.location.href}
|
||||
data-clipboard-text={window.location.href}
|
||||
onSuccess={this.handleRoomLinkCopied.bind(this)}
|
||||
onClick={() => {}} // Avoid link action.
|
||||
>
|
||||
invite people to this room
|
||||
</ClipboardButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='remote-videos'>
|
||||
{
|
||||
Object.keys(state.remoteStreams).map((msid) =>
|
||||
{
|
||||
let stream = state.remoteStreams[msid];
|
||||
let peer;
|
||||
|
||||
for (let peerId of Object.keys(state.peers))
|
||||
{
|
||||
peer = state.peers[peerId];
|
||||
|
||||
if (peer.msids.indexOf(msid) !== -1)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!peer)
|
||||
return;
|
||||
|
||||
return (
|
||||
<TransitionAppear key={msid} duration={500}>
|
||||
<RemoteVideo
|
||||
peer={peer}
|
||||
stream={stream}
|
||||
fullsize={numPeers === 1}
|
||||
onDisableVideo={this.handleDisableRemoteVideo.bind(this)}
|
||||
onEnableVideo={this.handleEnableRemoteVideo.bind(this)}
|
||||
/>
|
||||
</TransitionAppear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<TransitionAppear duration={500}>
|
||||
<div className='local-video'>
|
||||
<LocalVideo
|
||||
peerId={props.peerId}
|
||||
stream={state.localStream}
|
||||
resolution={state.localVideoResolution}
|
||||
multipleWebcams={state.multipleWebcams}
|
||||
webcamType={state.webcamType}
|
||||
connectionState={state.connectionState}
|
||||
onMicMute={this.handleLocalMute.bind(this)}
|
||||
onWebcamToggle={this.handleLocalWebcamToggle.bind(this)}
|
||||
onWebcamChange={this.handleLocalWebcamChange.bind(this)}
|
||||
onResolutionChange={this.handleLocalResolutionChange.bind(this)}
|
||||
/>
|
||||
|
||||
{state.showStats ?
|
||||
<TransitionAppear duration={500}>
|
||||
<Stats
|
||||
stats={state.stats || new Map()}
|
||||
onClose={this.handleStatsClose.bind(this)}
|
||||
/>
|
||||
</TransitionAppear>
|
||||
:
|
||||
<div
|
||||
className='show-stats'
|
||||
onClick={this.handleClickShowStats.bind(this)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
// Set flag
|
||||
this._mounted = true;
|
||||
|
||||
// Run the client
|
||||
this._runClient();
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
// Unset flag
|
||||
this._mounted = false;
|
||||
|
||||
// Close client
|
||||
this._client.removeAllListeners();
|
||||
this._client.close();
|
||||
|
||||
// Close local MediaStream
|
||||
if (state.localStream)
|
||||
utils.closeMediaStream(state.localStream);
|
||||
}
|
||||
|
||||
handleRoomLinkCopied()
|
||||
{
|
||||
logger.debug('handleRoomLinkCopied()');
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
position : 'tr',
|
||||
title : 'Room URL copied to the clipboard',
|
||||
message : 'Share it with others to join this room'
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalMute(value)
|
||||
{
|
||||
logger.debug('handleLocalMute() [value:%s]', value);
|
||||
|
||||
let micTrack = this.state.localStream.getAudioTracks()[0];
|
||||
|
||||
if (!micTrack)
|
||||
return Promise.reject(new Error('no audio track'));
|
||||
|
||||
micTrack.enabled = !value;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
handleLocalWebcamToggle(value)
|
||||
{
|
||||
logger.debug('handleLocalWebcamToggle() [value:%s]', value);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
if (value)
|
||||
return this._client.addVideo();
|
||||
else
|
||||
return this._client.removeVideo();
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let localStream = this.state.localStream;
|
||||
|
||||
this.setState({ localStream });
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalWebcamChange()
|
||||
{
|
||||
logger.debug('handleLocalWebcamChange()');
|
||||
|
||||
this._client.changeWebcam();
|
||||
}
|
||||
|
||||
handleLocalResolutionChange()
|
||||
{
|
||||
logger.debug('handleLocalResolutionChange()');
|
||||
|
||||
this._client.changeVideoResolution();
|
||||
}
|
||||
|
||||
handleStatsClose()
|
||||
{
|
||||
logger.debug('handleStatsClose()');
|
||||
|
||||
this.setState({ showStats: false });
|
||||
this._stopStats();
|
||||
}
|
||||
|
||||
handleClickShowStats()
|
||||
{
|
||||
logger.debug('handleClickShowStats()');
|
||||
|
||||
this.setState({ showStats: true });
|
||||
this._startStats();
|
||||
}
|
||||
|
||||
handleDisableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleDisableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
this._client.disableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
handleEnableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleEnableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
this._client.enableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
_runClient()
|
||||
{
|
||||
let peerId = this.props.peerId;
|
||||
let roomId = this.props.roomId;
|
||||
|
||||
logger.debug('_runClient() [peerId:"%s", roomId:"%s"]', peerId, roomId);
|
||||
|
||||
this._client = new Client(peerId, roomId);
|
||||
|
||||
this._client.on('localstream', (stream, resolution) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
localStream : stream,
|
||||
localVideoResolution : resolution
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('join', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
title : 'Yes!',
|
||||
message : 'You are in the room!',
|
||||
image : '/resources/images/room.svg',
|
||||
imageWidth : 80,
|
||||
imageHeight : 80
|
||||
});
|
||||
|
||||
// Start retrieving WebRTC stats (unless mobile)
|
||||
if (utils.isDesktop())
|
||||
{
|
||||
this.setState({ showStats: true });
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
this._startStats();
|
||||
}, STATS_INTERVAL / 2);
|
||||
}
|
||||
});
|
||||
|
||||
this._client.on('close', (error) =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
if (error)
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Error',
|
||||
message : error.message
|
||||
});
|
||||
}
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('disconnected', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Warning',
|
||||
message : 'app disconnected'
|
||||
});
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('numwebcams', (num) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
multipleWebcams : (num > 1 ? true : false)
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('webcamtype', (type) =>
|
||||
{
|
||||
this.setState({ webcamType: type });
|
||||
});
|
||||
|
||||
this._client.on('peers', (peers) =>
|
||||
{
|
||||
let peersObject = {};
|
||||
|
||||
for (let peer of peers)
|
||||
{
|
||||
peersObject[peer.id] = peer;
|
||||
}
|
||||
|
||||
this.setState({ peers: peersObject });
|
||||
});
|
||||
|
||||
this._client.on('addpeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
message : `${peer.id} joined the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('updatepeer', (peer) =>
|
||||
{
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('removepeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'info',
|
||||
message : `${peer.id} left the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
delete peers[peer.id];
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('connectionstate', (state) =>
|
||||
{
|
||||
this.setState({ connectionState: state });
|
||||
});
|
||||
|
||||
this._client.on('addstream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
remoteStreams[stream.id] = stream;
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removestream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
delete remoteStreams[stream.id];
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('addtrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removetrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('forcestreamsupdate', () =>
|
||||
{
|
||||
// Just firef for Firefox due to bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
_startStats()
|
||||
{
|
||||
logger.debug('_startStats()');
|
||||
|
||||
getStats.call(this);
|
||||
|
||||
function getStats()
|
||||
{
|
||||
this._client.getStats()
|
||||
.then((stats) =>
|
||||
{
|
||||
if (!this._mounted)
|
||||
return;
|
||||
|
||||
this.setState({ stats });
|
||||
|
||||
this._statsTimer = setTimeout(() =>
|
||||
{
|
||||
getStats.call(this);
|
||||
}, STATS_INTERVAL);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('getStats() failed: %o', error);
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
this._statsTimer = setTimeout(() =>
|
||||
{
|
||||
getStats.call(this);
|
||||
}, STATS_INTERVAL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_stopStats()
|
||||
{
|
||||
logger.debug('_stopStats()');
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
clearTimeout(this._statsTimer);
|
||||
}
|
||||
}
|
||||
|
||||
Room.propTypes =
|
||||
{
|
||||
peerId : React.PropTypes.string.isRequired,
|
||||
roomId : React.PropTypes.string.isRequired,
|
||||
onNotify : React.PropTypes.func.isRequired,
|
||||
onHideNotification : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,497 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import browser from 'bowser';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('Stats'); // eslint-disable-line no-unused-vars
|
||||
|
||||
// TODO: TMP
|
||||
global.BROWSER = browser;
|
||||
|
||||
export default class Stats extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport : null,
|
||||
audio : null,
|
||||
video : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount()
|
||||
{
|
||||
let stats = this.props.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stats = nextProps.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Stats'>
|
||||
<div
|
||||
className='close'
|
||||
onClick={this.handleCloseClick.bind(this)}
|
||||
/>
|
||||
{
|
||||
Object.keys(state.stats).map((blockName) =>
|
||||
{
|
||||
let block = state.stats[blockName];
|
||||
|
||||
if (!block)
|
||||
return;
|
||||
|
||||
let items = Object.keys(block).map((itemName) =>
|
||||
{
|
||||
let value = block[itemName];
|
||||
|
||||
if (value === undefined)
|
||||
return;
|
||||
|
||||
return (
|
||||
<div key={itemName} className='item'>
|
||||
<div className='key'>{itemName}</div>
|
||||
<div className='value'>{value}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (!items.length)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div key={blockName} className='block'>
|
||||
<h1>{blockName}</h1>
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleCloseClick()
|
||||
{
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
_processStats(stats)
|
||||
{
|
||||
if (browser.check({ chrome: '58' }, true))
|
||||
{
|
||||
this._processStatsChrome58(stats);
|
||||
}
|
||||
else if (browser.check({ chrome: '40' }, true))
|
||||
{
|
||||
this._processStatsChromeOld(stats);
|
||||
}
|
||||
else if (browser.check({ firefox: '40' }, true))
|
||||
{
|
||||
this._processStatsFirefox(stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn('_processStats() | unsupported browser [name:"%s", version:%s]',
|
||||
browser.name, browser.version);
|
||||
}
|
||||
}
|
||||
|
||||
_processStatsChrome58(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'transport':
|
||||
{
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.writable)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.availableOutgoingBitrate / 1000) + ' kbps';
|
||||
transport['current RTT'] =
|
||||
Math.round(group.currentRoundTripTime * 1000) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'codec':
|
||||
{
|
||||
let mimeType = group.mimeType.split('/');
|
||||
let kind = mimeType[0];
|
||||
let codec = mimeType[1];
|
||||
let block;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = codec;
|
||||
block['payload type'] = group.payloadType;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'track':
|
||||
{
|
||||
if (group.kind !== 'video')
|
||||
break;
|
||||
|
||||
video['frame size'] = group.frameWidth + ' x ' + group.frameHeight;
|
||||
video['frames sent'] = group.framesSent;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.protocol;
|
||||
transport['local IP'] = localCandidate.ip;
|
||||
transport['local port'] = localCandidate.port;
|
||||
transport['remote IP'] = remoteCandidate.ip;
|
||||
transport['remote port'] = remoteCandidate.port;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsChromeOld(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'googCandidatePair':
|
||||
{
|
||||
if (group.googActiveConnection !== 'true')
|
||||
break;
|
||||
|
||||
let localAddress = group.googLocalAddress.split(':');
|
||||
let remoteAddress = group.googRemoteAddress.split(':');
|
||||
let localIP = localAddress[0];
|
||||
let localPort = localAddress[1];
|
||||
let remoteIP = remoteAddress[0];
|
||||
let remotePort = remoteAddress[1];
|
||||
|
||||
transport['protocol'] = group.googTransportType;
|
||||
transport['local IP'] = localIP;
|
||||
transport['local port'] = localPort;
|
||||
transport['remote IP'] = remoteIP;
|
||||
transport['remote port'] = remotePort;
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
transport['RTT'] = Math.round(group.googRtt) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'VideoBwe':
|
||||
{
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.googAvailableSendBandwidth / 1000) + ' kbps';
|
||||
transport['transmit bitrate'] =
|
||||
Math.round(group.googTransmitBitrate / 1000) + ' kbps';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ssrc':
|
||||
{
|
||||
if (group.packetsSent === undefined)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = group.googCodecName;
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
block['packets lost'] = group.packetsLost;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame size'] =
|
||||
group.googFrameWidthSent + ' x ' + group.googFrameHeightSent;
|
||||
video['frame rate'] = group.googFrameRateSent;
|
||||
}
|
||||
|
||||
block['NACK count'] = group.googNacksReceived;
|
||||
block['PLI count'] = group.googPlisReceived;
|
||||
block['FIR count'] = group.googFirsReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsFirefox(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
// TODO: REMOVE
|
||||
global.STATS = stats;
|
||||
|
||||
switch (group.type)
|
||||
{
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.selected)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['bitrate'] =
|
||||
Math.round(group.bitrateMean / 1000) + ' kbps';
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame rate'] = Math.round(group.framerateMean);
|
||||
}
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.transport;
|
||||
transport['local IP'] = localCandidate.ipAddress;
|
||||
transport['local port'] = localCandidate.portNumber;
|
||||
transport['remote IP'] = remoteCandidate.ipAddress;
|
||||
transport['remote port'] = remoteCandidate.portNumber;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Stats.propTypes =
|
||||
{
|
||||
stats : React.PropTypes.object.isRequired,
|
||||
onClose : React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
|
||||
const DEFAULT_DURATION = 1000;
|
||||
|
||||
export default class TransitionAppear extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let duration = props.hasOwnProperty('duration') ? props.duration : DEFAULT_DURATION;
|
||||
|
||||
return (
|
||||
<ReactCSSTransitionGroup
|
||||
component={FakeTransitionWrapper}
|
||||
transitionName='transition'
|
||||
transitionAppear={!!duration}
|
||||
transitionAppearTimeout={duration}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
{this.props.children}
|
||||
</ReactCSSTransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TransitionAppear.propTypes =
|
||||
{
|
||||
children : React.PropTypes.any,
|
||||
duration : React.PropTypes.number
|
||||
};
|
||||
|
||||
class FakeTransitionWrapper extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let children = React.Children.toArray(this.props.children);
|
||||
|
||||
return children[0] || null;
|
||||
}
|
||||
}
|
||||
|
||||
FakeTransitionWrapper.propTypes =
|
||||
{
|
||||
children : React.PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,233 @@
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import Logger from '../Logger';
|
||||
import classnames from 'classnames';
|
||||
import hark from 'hark';
|
||||
|
||||
const logger = new Logger('Video'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class Video extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
width : 0,
|
||||
height : 0,
|
||||
resolution : null,
|
||||
volume : 0 // Integer from 0 to 10.
|
||||
};
|
||||
|
||||
let stream = props.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// Current MediaStreamTracks info.
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
// Periodic timer to show video dimensions.
|
||||
this._videoResolutionTimer = null;
|
||||
|
||||
// Hark instance.
|
||||
this._hark = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Video'>
|
||||
{state.width ?
|
||||
(
|
||||
<div
|
||||
className={classnames('resolution', { clickable: !!props.resolution })}
|
||||
onClick={this.handleResolutionClick.bind(this)}
|
||||
>
|
||||
<p>{state.width}x{state.height}</p>
|
||||
{props.resolution ?
|
||||
<p>{props.resolution}</p>
|
||||
:null}
|
||||
</div>
|
||||
)
|
||||
:null}
|
||||
<div className='volume'>
|
||||
<div className={classnames('bar', `level${state.volume}`)}/>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({ mirror: props.mirror })}
|
||||
autoPlay
|
||||
muted={props.muted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
let stream = this.props.stream;
|
||||
let video = this.refs.video;
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
this._showVideoResolution();
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
this._showVideoResolution();
|
||||
}, 500);
|
||||
|
||||
if (stream.getAudioTracks().length > 0)
|
||||
{
|
||||
this._hark = hark(stream);
|
||||
|
||||
this._hark.on('speaking', () =>
|
||||
{
|
||||
logger.debug('hark "speaking" event');
|
||||
});
|
||||
|
||||
this._hark.on('stopped_speaking', () =>
|
||||
{
|
||||
logger.debug('hark "stopped_speaking" event');
|
||||
|
||||
this.setState({ volume: 0 });
|
||||
});
|
||||
|
||||
this._hark.on('volume_change', (volume, threshold) =>
|
||||
{
|
||||
if (volume < threshold)
|
||||
return;
|
||||
|
||||
// logger.debug('hark "volume_change" event [volume:%sdB, threshold:%sdB]', volume, threshold);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
volume : Math.round((volume - threshold) * (-10) / threshold)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stream = nextProps.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// If there is something different in the stream, re-render it.
|
||||
|
||||
let previousTracksHash = this._tracksHash;
|
||||
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
if (this._tracksHash !== previousTracksHash)
|
||||
this.refs.video.srcObject = stream;
|
||||
}
|
||||
|
||||
handleResolutionClick()
|
||||
{
|
||||
if (!this.props.resolution)
|
||||
return;
|
||||
|
||||
logger.debug('handleResolutionClick()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
|
||||
_getTracksHash(stream)
|
||||
{
|
||||
return stream.getTracks()
|
||||
.map((track) =>
|
||||
{
|
||||
return track.id;
|
||||
})
|
||||
.join('|');
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
let video = this.refs.video;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
width : video.videoWidth,
|
||||
height : video.videoHeight
|
||||
});
|
||||
}
|
||||
|
||||
_cleanStream(stream)
|
||||
{
|
||||
// Hack for Firefox bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
let previousNumTracks = tracks.length;
|
||||
|
||||
// Remove ended tracks.
|
||||
for (let track of tracks)
|
||||
{
|
||||
if (track.readyState === 'ended')
|
||||
{
|
||||
logger.warn('_cleanStream() | removing ended track [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are multiple live audio tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getAudioTracks().length > 1)
|
||||
{
|
||||
let track = stream.getAudioTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live audio track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
// If there are multiple live video tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getVideoTracks().length > 1)
|
||||
{
|
||||
let track = stream.getVideoTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live video track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
let numTracks = stream.getTracks().length;
|
||||
|
||||
if (numTracks !== previousNumTracks)
|
||||
logger.warn('_cleanStream() | num tracks changed from %s to %s', previousNumTracks, numTracks);
|
||||
}
|
||||
}
|
||||
|
||||
Video.propTypes =
|
||||
{
|
||||
stream : React.PropTypes.object.isRequired,
|
||||
resolution : React.PropTypes.string,
|
||||
muted : React.PropTypes.bool,
|
||||
mirror : React.PropTypes.bool,
|
||||
onResolutionChange : React.PropTypes.func
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
import getMuiTheme from 'material-ui/styles/getMuiTheme';
|
||||
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
|
||||
import { grey500 } from 'material-ui/styles/colors';
|
||||
|
||||
// NOTE: I should clone it
|
||||
let theme = lightBaseTheme;
|
||||
|
||||
theme.palette.borderColor = grey500;
|
||||
|
||||
let muiTheme = getMuiTheme(lightBaseTheme);
|
||||
|
||||
export default muiTheme;
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
import browser from 'bowser';
|
||||
import webrtc from 'webrtc-adapter'; // eslint-disable-line no-unused-vars
|
||||
import domready from 'domready';
|
||||
import UrlParse from 'url-parse';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import injectTapEventPlugin from 'react-tap-event-plugin';
|
||||
import randomString from 'random-string';
|
||||
import Logger from './Logger';
|
||||
import utils from './utils';
|
||||
import App from './components/App';
|
||||
|
||||
const REGEXP_FRAGMENT_ROOM_ID = new RegExp('^#room-id=([0-9a-zA-Z]+)$');
|
||||
const logger = new Logger();
|
||||
|
||||
logger.debug('detected browser [name:"%s", version:%s]', browser.name, browser.version);
|
||||
|
||||
injectTapEventPlugin();
|
||||
|
||||
domready(() =>
|
||||
{
|
||||
logger.debug('DOM ready');
|
||||
|
||||
// Load stuff and run
|
||||
utils.initialize()
|
||||
.then(run)
|
||||
.catch((error) =>
|
||||
{
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
function run()
|
||||
{
|
||||
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
|
||||
|
||||
let container = document.getElementById('mediasoup-demo-app-container');
|
||||
let urlParser = new UrlParse(window.location.href, true);
|
||||
let match = urlParser.hash.match(REGEXP_FRAGMENT_ROOM_ID);
|
||||
let peerId = randomString({ length: 8 }).toLowerCase();
|
||||
let roomId;
|
||||
|
||||
if (match)
|
||||
{
|
||||
roomId = match[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
roomId = randomString({ length: 8 }).toLowerCase();
|
||||
window.location = `#room-id=${roomId}`;
|
||||
}
|
||||
|
||||
ReactDOM.render(<App peerId={peerId} roomId={roomId}/>, container);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
module.exports =
|
||||
{
|
||||
getProtooUrl(peerId, roomId)
|
||||
{
|
||||
let hostname = window.location.hostname;
|
||||
let port = config.protoo.listenPort;
|
||||
let url = `wss://${hostname}:${port}/?peer-id=${peerId}&room-id=${roomId}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
import browser from 'bowser';
|
||||
import Logger from './Logger';
|
||||
|
||||
const logger = new Logger('utils');
|
||||
|
||||
let mediaQueryDetectorElem;
|
||||
|
||||
module.exports =
|
||||
{
|
||||
initialize()
|
||||
{
|
||||
logger.debug('initialize()');
|
||||
|
||||
// Media query detector stuff
|
||||
mediaQueryDetectorElem = document.getElementById('mediasoup-demo-app-media-query-detector');
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
isDesktop()
|
||||
{
|
||||
return !!mediaQueryDetectorElem.offsetParent;
|
||||
},
|
||||
|
||||
isMobile()
|
||||
{
|
||||
return !mediaQueryDetectorElem.offsetParent;
|
||||
},
|
||||
|
||||
isPlanB()
|
||||
{
|
||||
if (browser.chrome || browser.chromium || browser.opera)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
},
|
||||
|
||||
closeMediaStream(stream)
|
||||
{
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
|
||||
for (let i=0, len=tracks.length; i < len; i++)
|
||||
{
|
||||
tracks[i].stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "mediasoup-demo-app",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "mediasoup demo app",
|
||||
"author": "Iñaki Baz Castillo <ibc@aliax.net>",
|
||||
"license": "All Rights Reserved",
|
||||
"main": "lib/index.jsx",
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.23.0",
|
||||
"bowser": "^1.6.1",
|
||||
"classnames": "^2.2.5",
|
||||
"debug": "^2.6.4",
|
||||
"domready": "^1.0.8",
|
||||
"hark": "ibc/hark#main-with-raf",
|
||||
"material-ui": "^0.17.4",
|
||||
"protoo-client": "^1.1.4",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^15.5.4",
|
||||
"react-addons-css-transition-group": "^15.5.2",
|
||||
"react-clipboard.js": "^1.0.1",
|
||||
"react-dom": "^15.5.4",
|
||||
"react-notification-system": "ibc/react-notification-system#master",
|
||||
"react-tap-event-plugin": "^2.0.1",
|
||||
"sdp-transform": "^2.3.0",
|
||||
"url-parse": "^1.1.8",
|
||||
"webrtc-adapter": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-transform-object-assign": "^6.22.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babelify": "^7.3.0",
|
||||
"browser-sync": "^2.18.8",
|
||||
"browserify": "^14.3.0",
|
||||
"del": "^2.2.2",
|
||||
"envify": "^4.0.0",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-react": "^6.10.3",
|
||||
"gulp": "git://github.com/gulpjs/gulp.git#4.0",
|
||||
"gulp-css-base64": "^1.3.4",
|
||||
"gulp-eslint": "^3.0.1",
|
||||
"gulp-header": "^1.8.8",
|
||||
"gulp-if": "^2.0.2",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-stylus": "^2.6.0",
|
||||
"gulp-touch": "^1.0.1",
|
||||
"gulp-uglify": "^2.1.2",
|
||||
"gulp-util": "^3.0.8",
|
||||
"mkdirp": "^0.5.1",
|
||||
"ncp": "^2.0.0",
|
||||
"nib": "^1.1.2",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.9.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 458 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
id="svg4347"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="buddy.svg">
|
||||
<defs
|
||||
id="defs4349" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="5.3985295"
|
||||
inkscape:cy="12.974855"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showborder="true"
|
||||
inkscape:window-width="979"
|
||||
inkscape:window-height="809"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata4352">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1004.3622)">
|
||||
<rect
|
||||
style="fill:none"
|
||||
y="1004.8992"
|
||||
x="1.3571341"
|
||||
height="47.070076"
|
||||
width="44.699638"
|
||||
id="rect4438" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 29.874052,1037.8071 c -0.170513,-1.8828 -0.105106,-3.1968 -0.105106,-4.917 0.852576,-0.4474 2.380226,-3.2994 2.638334,-5.7089 0.670389,-0.054 1.727363,-0.7089 2.036865,-3.2912 0.167015,-1.3862 -0.496371,-2.1665 -0.900473,-2.4118 1.090848,-3.2807 3.356621,-13.4299 -4.190513,-14.4788 -0.776672,-1.364 -2.765648,-2.0544 -5.350262,-2.0544 -10.340807,0.1905 -11.588155,7.8088 -9.321205,16.5332 -0.402943,0.2453 -1.066322,1.0256 -0.900475,2.4118 0.310672,2.5823 1.366476,3.2362 2.036857,3.2912 0.256949,2.4083 1.845322,5.2615 2.700243,5.7089 0,1.7202 0.06426,3.0342 -0.106272,4.917 -2.046213,5.5007 -15.8522501,3.9567 -16.4899366,14.5661 H 46.303254 c -0.636524,-10.6094 -14.38299,-9.0654 -16.429202,-14.5661 z"
|
||||
id="path4440"
|
||||
style="opacity:0.5" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 265 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="tiny" x="0px" y="0px" width="480px" height="480px" viewBox="0 0 480 480" xml:space="preserve">
|
||||
<g id="all_friends">
|
||||
<path d="M185.161,285.818c-9.453-7.317-17-18.963-20.438-28.691c-0.406-0.371-0.949-0.846-1.373-1.23 c-5.297-4.638-15.156-13.284-15.156-32.656c0-9.143,1.748-15.652,5.514-20.48c0.518-0.651,1.174-1.407,1.984-2.188v-19.394 c0-23.428,16.861-54.488,53.471-65.856c-6.391-14.644-22.232-30.543-53.656-30.543c-43.455,0-57.205,30.422-57.205,45.851 c0,8.313,0,22.479,0,23.514c0,1.049-6.188-2.826-6.188,11.153c0,15.946,10.752,17.047,12.658,23.061 c3.375,10.691,13.877,22.314,20.826,22.314c0,0,0.531,2.209,0.531,6.278c0,3.296-0.078,3.24-2.402,3.24 c-37.77,0-63.721,39.909-63.721,52.653c0,16.292,0,34.201,0,34.201h80.076C153.303,297.144,168.805,289.123,185.161,285.818z"/>
|
||||
<path d="M356.256,220.19c-2.312,0-2.408,0.056-2.408-3.24c0-4.069,0.531-6.278,0.531-6.278c6.955,0,17.469-11.623,20.844-22.314 c1.906-6.014,12.658-7.114,12.658-23.061c0-13.979-6.174-10.104-6.174-11.153c0-1.035,0-15.2,0-23.514 c0-15.429-13.781-45.851-57.232-45.851c-31.486,0-47.342,15.985-53.701,30.659c36.389,11.459,53.154,42.386,53.154,65.74v19.397 c4.125,3.957,7.486,10.812,7.486,22.664c0,19.377-9.859,28.022-15.158,32.661c-0.439,0.384-0.971,0.854-1.375,1.229 c-3.422,9.733-10.986,21.37-20.439,28.688c16.379,3.313,31.877,11.334,45.078,21.227h80.488c0,0,0-17.909,0-34.201 C420.008,260.1,394.04,220.19,356.256,220.19z"/>
|
||||
<path d="M278.368,298.86c-2.814,0-2.926-8.948-2.926-12.952c0-4.927,0.643-7.619,0.643-7.619c8.439,0,21.189-14.091,25.283-27.064 c2.312-7.296,15.355-8.634,15.355-27.984c0-16.956-7.498-12.257-7.498-13.525c0-1.26,0-18.445,0-28.536 c0-18.712-16.703-55.618-69.422-55.618c-52.705,0-69.406,36.906-69.406,55.618c0,10.091,0,27.276,0,28.536 c0,1.269-7.516-3.431-7.516,13.525c0,19.351,13.047,20.688,15.359,27.984c4.107,12.974,16.844,27.064,25.283,27.064 c0,0,0.639,2.692,0.639,7.619c0,4.004-0.096,12.952-2.922,12.952c-45.811,0-86.328,48.422-86.328,63.89c0,19.765,0,32.467,0,32.467 h124.891h124.893c0,0,0-12.702,0-32.467C364.696,347.282,324.178,298.86,278.368,298.86z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg fill="#FFFFFF" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n;n="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,n.antiglobal=e()}}(function(){return function e(n,o,t){function r(i,u){if(!o[i]){if(!n[i]){var l="function"==typeof require&&require;if(!u&&l)return l(i,!0);if(f)return f(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var d=o[i]={exports:{}};n[i][0].call(d.exports,function(e){var o=n[i][1][e];return r(o?o:e)},d,d.exports,e,n,o,t)}return o[i].exports}for(var f="function"==typeof require&&require,i=0;i<t.length;i++)r(t[i]);return r}({1:[function(e,n,o){(function(e){function o(){var e,n,o,u=t(),l=Array.prototype.slice.call(arguments),a=[],d=[],c=!1;for(e=0,n=u.length;e<n;e++)o=u[e],r.indexOf(o)===-1&&l.indexOf(o)===-1&&(a.push(o),c=!0);for(e=0,n=r.length;e<n;e++)o=r[e],u.indexOf(o)===-1&&(d.push(o),c=!0);if(r=u.concat(l),c){var s="antiglobal() | globals do not match:";for(e=0,n=a.length;e<n;e++)o=a[e],s=s+"\n+ "+o;for(e=0,n=d.length;e<n;e++)o=d[e],s=s+"\n- "+o;if(f&&console.error(s),i)throw new Error(s)}return!c}function t(){var n=[];for(var o in e)e.hasOwnProperty(o)&&"antiglobal"!==o&&n.push(o);return n}var r=t(),f=!0,i=!1;o.reset=function(){r=t()},Object.defineProperties(o,{log:{get:function(){return f},set:function(e){f=Boolean(e)}},throw:{get:function(){return i},set:function(e){i=Boolean(e)}}}),n.exports=o}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[1])(1)});
|
||||
@@ -0,0 +1,5 @@
|
||||
[data-component='App'] {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
[data-component='LocalVideo'] {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
|
||||
TransitionAppear(500ms);
|
||||
|
||||
+desktop() {
|
||||
height: 220px;
|
||||
width: 220px;
|
||||
border: 2px solid rgba(#fff, 0.5);
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
height: 180px;
|
||||
width: 180px;
|
||||
border: 2px solid rgba(#fff, 0.5);
|
||||
}
|
||||
|
||||
&.state-checking {
|
||||
border-color: orange;
|
||||
}
|
||||
|
||||
&.state-checking {
|
||||
animation: LocalVideo-state-checking .75s infinite linear;
|
||||
}
|
||||
|
||||
&.state-connected,
|
||||
&.state-completed {
|
||||
border-color: rgba(#49ce3e, 0.9);
|
||||
}
|
||||
|
||||
&.state-failed,
|
||||
&.state-disconnected,
|
||||
&.state-closed {
|
||||
border-color: rgba(#ff2000, 0.75);
|
||||
}
|
||||
|
||||
> .controls {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.25s;
|
||||
|
||||
> .control {
|
||||
pointer-events: auto;
|
||||
flex: 0 0 auto;
|
||||
margin: 4px !important;
|
||||
margin-left: 0 !important;
|
||||
height: 32px !important;
|
||||
width: 32px !important;
|
||||
padding: 0 !important;
|
||||
background-color: rgba(#000, 0.25) !important;
|
||||
border-radius: 100%;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(#000, 0.85) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
bottom: 4px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> .peer-id {
|
||||
padding: 4px 14px;
|
||||
font-size: 14px;
|
||||
color: rgba(#fff, 0.75);
|
||||
background: rgba(#000, 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes LocalVideo-state-checking {
|
||||
50% {
|
||||
border-color: rgba(orange, 0.9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
[data-component='RemoteVideo'] {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
transition-duration: 0.25s;
|
||||
|
||||
TransitionAppear(500ms);
|
||||
|
||||
&.fullsize {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
> .info {
|
||||
justify-content: flex-end;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
+desktop() {
|
||||
height: 350px;
|
||||
width: 400px;
|
||||
margin: 4px;
|
||||
border: 4px solid rgba(#fff, 0.2);
|
||||
|
||||
&.fullsize {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
border: 4px solid rgba(#fff, 0.2);
|
||||
border-bottom-width: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
|
||||
&.fullsize {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .controls {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.25s;
|
||||
opacity: 0.8;
|
||||
|
||||
> .control {
|
||||
pointer-events: auto;
|
||||
flex: 0 0 auto;
|
||||
margin: 4px !important;
|
||||
margin-left: 0 !important;
|
||||
height: 32px !important;
|
||||
width: 32px !important;
|
||||
padding: 0 !important;
|
||||
background-color: rgba(#000, 0.25) !important;
|
||||
border-radius: 100%;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(#000, 0.85) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
bottom: 4px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
+desktop() {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
justify-content: flex-end;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
> .peer-id {
|
||||
padding: 6px 16px;
|
||||
font-size: 16px;
|
||||
color: rgba(#fff, 0.75);
|
||||
background: rgba(#000, 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
[data-component='Room'] {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
+desktop() {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .room-link-wrapper {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
> .room-link {
|
||||
width: auto;
|
||||
background-color: rgba(#fff, 0.8);
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
box-shadow: 0px 3px 12px 2px rgba(#111, 0.4);
|
||||
|
||||
> a.link {
|
||||
display: block;;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
padding: 10px 20px;
|
||||
color: #104758;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.25s;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .remote-videos {
|
||||
+desktop() {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding-bottom: 150px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
> .local-video {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
z-index: 100;
|
||||
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
|
||||
|
||||
+desktop() {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
> .show-stats {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: -40px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-image: url('/resources/images/stats.svg');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-color: rgba(#000, 0.25);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
transition-duration: 0.25s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
[data-component='Stats'] {
|
||||
TransitionAppear(500ms);
|
||||
|
||||
+desktop() {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 0;
|
||||
min-width: 100px;
|
||||
background-color: rgba(#1c446f, 0.75);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(#1c446f, 0.75);
|
||||
padding: 40px 10px;
|
||||
font-size: 0.9rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
> .close {
|
||||
background-image: url('/resources/images/close.svg');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
opacity: 0.75;
|
||||
transition-duration: 0.25s;
|
||||
|
||||
+desktop() {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
> .block {
|
||||
padding: 5px;
|
||||
|
||||
+desktop() {
|
||||
border-right: 1px solid rgba(#fff, 0.15);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> h1 {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
> .item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 3px 0;
|
||||
font-size: 0.95em;
|
||||
font-weight: 300;
|
||||
|
||||
> .key {
|
||||
text-align: right;
|
||||
color: rgba(#fff, 0.9);
|
||||
|
||||
+desktop() {
|
||||
width: 85px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
> .value {
|
||||
margin-left: 10px;
|
||||
text-align: left;
|
||||
color: #aae22b;
|
||||
|
||||
+desktop() {
|
||||
width: 85px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
[data-component='Video'] {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
> .resolution {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 4px 8px;
|
||||
border-bottom-right-radius: 5px;
|
||||
background: rgba(#000, 0.5);
|
||||
transition-property: background;
|
||||
transition-duration: 0.25s;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(#000, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
font-size: 11px;
|
||||
color: rgba(#fff, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
> .volume {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
top: 0
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> .bar {
|
||||
width: 6px;
|
||||
border-radius: 6px;
|
||||
background: rgba(yellow, 0.65);
|
||||
transition-property: height background-color;
|
||||
transition-duration: 0.25s;
|
||||
|
||||
&.level0 { height: 0; background-color: rgba(yellow, 0.65); }
|
||||
&.level1 { height: 10%; background-color: rgba(yellow, 0.65); }
|
||||
&.level2 { height: 20%; background-color: rgba(yellow, 0.65); }
|
||||
&.level3 { height: 30%; background-color: rgba(yellow, 0.65); }
|
||||
&.level4 { height: 40%; background-color: rgba(orange, 0.65); }
|
||||
&.level5 { height: 50%; background-color: rgba(orange, 0.65); }
|
||||
&.level6 { height: 60%; background-color: rgba(red, 0.65); }
|
||||
&.level7 { height: 70%; background-color: rgba(red, 0.65); }
|
||||
&.level8 { height: 80%; background-color: rgba(#000, 0.65); }
|
||||
&.level9 { height: 90%; background-color: rgba(#000, 0.65); }
|
||||
&.level10 { height: 100%; background-color: rgba(#000, 0.65); }
|
||||
}
|
||||
}
|
||||
|
||||
> video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
background-color: rgba(#041918, 0.65);
|
||||
background-image: url('/resources/images/buddy.svg');
|
||||
background-position: bottom;
|
||||
background-size: auto 85%;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src:
|
||||
local('Roboto Light'),
|
||||
local('Roboto-Light'), url('/resources/fonts/Roboto-light-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src:
|
||||
local('Roboto Light'),
|
||||
local('Roboto-Light'),
|
||||
url('/resources/fonts/Roboto-light.woff2') format('woff2'),
|
||||
url('/resources/fonts/Roboto-light.ttf') format('ttf');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
local('Roboto'),
|
||||
local('Roboto-Regular'),
|
||||
url('/resources/fonts/Roboto-regular-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
local('Roboto'),
|
||||
local('Roboto-Regular'),
|
||||
url('/resources/fonts/Roboto-regular.woff2') format('woff2'),
|
||||
url('/resources/fonts/Roboto-regular.ttf') format('ttf');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src:
|
||||
local('Roboto Medium'),
|
||||
local('Roboto-Medium'),
|
||||
url('/resources/fonts/Roboto-medium-ext.woff2') format('woff2');
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src:
|
||||
local('Roboto Medium'),
|
||||
local('Roboto-Medium'),
|
||||
url('/resources/fonts/Roboto-medium.woff2') format('woff2'),
|
||||
url('/resources/fonts/Roboto-medium.ttf') format('ttf');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@import 'nib';
|
||||
|
||||
global-reset();
|
||||
|
||||
@import './mixins';
|
||||
@import './fonts';
|
||||
|
||||
html {
|
||||
font-family: 'Roboto';
|
||||
background-image: url('/resources/images/body-bg-2.jpg');
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
|
||||
+desktop() {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#mediasoup-demo-app-container {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
// Components
|
||||
@import './components/App';
|
||||
@import './components/Room';
|
||||
@import './components/LocalVideo';
|
||||
@import './components/RemoteVideo';
|
||||
@import './components/Video';
|
||||
@import './components/Stats';
|
||||
}
|
||||
|
||||
// Hack to detect in JS the current media query
|
||||
#mediasoup-demo-app-media-query-detector {
|
||||
position: relative;
|
||||
z-index: -1000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
|
||||
// In desktop let it "visible" so elem.offsetParent returns the parent element
|
||||
+desktop() {}
|
||||
|
||||
// In mobile ensure it's not displayed so elem.offsetParent returns null
|
||||
+mobile() {
|
||||
display: none;
|
||||
position: fixed; // Required for old IE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
placeholder()
|
||||
&::-webkit-input-placeholder
|
||||
{block}
|
||||
&:-moz-placeholder
|
||||
{block}
|
||||
&::-moz-placeholder
|
||||
{block}
|
||||
&:-ms-input-placeholder
|
||||
{block}
|
||||
|
||||
text-fill-color()
|
||||
-webkit-text-fill-color: arguments;
|
||||
-moz-text-fill-color: arguments;
|
||||
text-fill-color: arguments;
|
||||
|
||||
mobile()
|
||||
@media (max-device-width: 720px)
|
||||
{block}
|
||||
|
||||
desktop()
|
||||
@media (min-device-width: 721px)
|
||||
{block}
|
||||
|
||||
TransitionAppear($duration = 1s, $appearOpacity = 0, $activeOpacity = 1)
|
||||
will-change: opacity;
|
||||
|
||||
&.transition-appear
|
||||
opacity: $appearOpacity;
|
||||
|
||||
&.transition-appear.transition-appear-active
|
||||
transition-property: opacity;
|
||||
transition-duration: $duration;
|
||||
opacity: $activeOpacity;
|
||||
Reference in New Issue
Block a user