|
|
@ -1,11 +1,7 @@
|
|||
node_modules/
|
||||
|
||||
/app/config.*
|
||||
!/app/config.example.js
|
||||
|
||||
/server/config.*
|
||||
!/server/config.example.js
|
||||
/server/public/
|
||||
/server/certs/*
|
||||
!/server/certs/mediasoup-demo.localhost.cert.pem
|
||||
!/server/certs/mediasoup-demo.localhost.key.pem
|
||||
/server/certs/
|
||||
!/server/certs/mediasoup-demo.localhost.*
|
||||
|
|
|
|||
|
|
@ -34,12 +34,6 @@ $ cd app
|
|||
$ npm install
|
||||
```
|
||||
|
||||
* Copy `config.example.js` as `config.js`:
|
||||
|
||||
```bash
|
||||
$ cp config.example.js config.js
|
||||
```
|
||||
|
||||
* Globally install `gulp-cli` NPM module (may need `sudo`):
|
||||
|
||||
```bash
|
||||
|
|
@ -77,7 +71,7 @@ $ gulp prod
|
|||
|
||||
* Upload the entire `server` folder to your server and make your web server (Apache, Nginx...) expose the `server/public` folder.
|
||||
|
||||
* Edit your `server/config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc). Also set the proper remote WebSocket port in `client/config.js`.
|
||||
* Edit your `server/config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc).
|
||||
|
||||
* Within your server, run the server side Node.js application. We recommend using the [forever](https://www.npmjs.com/package/forever) NPM daemon launcher, but any other can be used:
|
||||
|
||||
|
|
|
|||
283
app/.eslintrc.js
|
|
@ -1,96 +1,229 @@
|
|||
module.exports =
|
||||
{
|
||||
env :
|
||||
env:
|
||||
{
|
||||
'browser' : true,
|
||||
'es6' : true,
|
||||
'node' : true,
|
||||
'commonjs' : true
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
plugins :
|
||||
plugins:
|
||||
[
|
||||
'react',
|
||||
'import'
|
||||
'import',
|
||||
'react'
|
||||
],
|
||||
extends :
|
||||
extends:
|
||||
[
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended'
|
||||
],
|
||||
settings :
|
||||
settings:
|
||||
{
|
||||
react :
|
||||
react:
|
||||
{
|
||||
pragma : 'React',
|
||||
version : '15'
|
||||
pragma: 'React',
|
||||
version: '15'
|
||||
}
|
||||
},
|
||||
parserOptions :
|
||||
parserOptions:
|
||||
{
|
||||
ecmaVersion : 6,
|
||||
sourceType : 'module',
|
||||
ecmaFeatures :
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures:
|
||||
{
|
||||
impliedStrict : true,
|
||||
jsx : true
|
||||
impliedStrict: true,
|
||||
experimentalObjectRestSpread: true,
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
rules :
|
||||
rules:
|
||||
{
|
||||
'no-console' : 0,
|
||||
'no-undef' : 2,
|
||||
'no-unused-vars' : [ 1, { 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
|
||||
'array-bracket-spacing': [ 2, 'always',
|
||||
{
|
||||
objectsInArrays: true,
|
||||
arraysInArrays: true
|
||||
}],
|
||||
'arrow-parens': [ 2, 'always' ],
|
||||
'arrow-spacing': 2,
|
||||
'block-spacing': [ 2, 'always' ],
|
||||
'brace-style': [ 2, 'allman', { allowSingleLine: true } ],
|
||||
'camelcase': 2,
|
||||
'comma-dangle': 2,
|
||||
'comma-spacing': [ 2, { before: false, after: true } ],
|
||||
'comma-style': 2,
|
||||
'computed-property-spacing': 2,
|
||||
'constructor-super': 2,
|
||||
'func-call-spacing': 2,
|
||||
'generator-star-spacing': 2,
|
||||
'guard-for-in': 2,
|
||||
'indent': [ 2, 'tab', { 'SwitchCase': 1 } ],
|
||||
'key-spacing': [ 2,
|
||||
{
|
||||
singleLine:
|
||||
{
|
||||
beforeColon: false,
|
||||
afterColon: true
|
||||
},
|
||||
multiLine:
|
||||
{
|
||||
beforeColon: true,
|
||||
afterColon: true,
|
||||
align: 'colon'
|
||||
}
|
||||
}],
|
||||
'keyword-spacing': 2,
|
||||
'linebreak-style': [ 2, 'unix' ],
|
||||
'lines-around-comment': [ 2,
|
||||
{
|
||||
allowBlockStart: true,
|
||||
allowObjectStart: true,
|
||||
beforeBlockComment: true,
|
||||
beforeLineComment: false
|
||||
}],
|
||||
'max-len': [ 2, 90,
|
||||
{
|
||||
tabWidth: 2,
|
||||
comments: 110,
|
||||
ignoreUrls: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreRegExpLiterals: true
|
||||
}],
|
||||
'newline-after-var': 2,
|
||||
'newline-before-return': 2,
|
||||
'newline-per-chained-call': 2,
|
||||
'no-alert': 2,
|
||||
'no-caller': 2,
|
||||
'no-case-declarations': 2,
|
||||
'no-catch-shadow': 2,
|
||||
'no-class-assign': 2,
|
||||
'no-confusing-arrow': 2,
|
||||
'no-console': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-debugger': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-div-regex': 2,
|
||||
'no-empty': [ 2, { allowEmptyCatch: true } ],
|
||||
'no-empty-pattern': 2,
|
||||
'no-else-return': 0,
|
||||
'no-eval': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-ex-assign': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-extra-boolean-cast': 2,
|
||||
'no-extra-label': 2,
|
||||
'no-extra-semi': 2,
|
||||
'no-fallthrough': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-global-assign': 2,
|
||||
'no-implicit-coercion': 2,
|
||||
'no-implicit-globals': 2,
|
||||
'no-inner-declarations': 2,
|
||||
'no-invalid-regexp': 2,
|
||||
'no-invalid-this': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-lonely-if': 2,
|
||||
'no-mixed-operators': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multi-spaces': 2,
|
||||
'no-multi-str': 2,
|
||||
'no-multiple-empty-lines': [ 2, { max: 1, maxEOF: 0, maxBOF: 0 } ],
|
||||
'no-native-reassign': 2,
|
||||
'no-negated-in-lhs': 2,
|
||||
'no-new': 2,
|
||||
'no-new-func': 2,
|
||||
'no-new-wrappers': 2,
|
||||
'no-obj-calls': 2,
|
||||
'no-proto': 2,
|
||||
'no-prototype-builtins': 0,
|
||||
'no-redeclare': 2,
|
||||
'no-regex-spaces': 2,
|
||||
'no-restricted-imports': 2,
|
||||
'no-return-assign': 2,
|
||||
'no-self-assign': 2,
|
||||
'no-self-compare': 2,
|
||||
'no-sequences': 2,
|
||||
'no-shadow': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'no-spaced-func': 2,
|
||||
'no-sparse-arrays': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-throw-literal': 2,
|
||||
'no-undef': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unmodified-loop-condition': 2,
|
||||
'no-unreachable': 2,
|
||||
'no-unused-vars': [ 1, { vars: 'all', args: 'after-used' }],
|
||||
'no-use-before-define': [ 2, { functions: false } ],
|
||||
'no-useless-call': 2,
|
||||
'no-useless-computed-key': 2,
|
||||
'no-useless-concat': 2,
|
||||
'no-useless-rename': 2,
|
||||
'no-var': 2,
|
||||
'no-whitespace-before-property': 2,
|
||||
'object-curly-newline': 0,
|
||||
'object-curly-spacing': [ 2, 'always' ],
|
||||
'object-property-newline': [ 2, { allowMultiplePropertiesPerLine: true } ],
|
||||
'prefer-const': 2,
|
||||
'prefer-rest-params': 2,
|
||||
'prefer-spread': 2,
|
||||
'prefer-template': 2,
|
||||
'quotes': [ 2, 'single', { avoidEscape: true } ],
|
||||
'semi': [ 2, 'always' ],
|
||||
'semi-spacing': 2,
|
||||
'space-before-blocks': 2,
|
||||
'space-before-function-paren': [ 2, 'never' ],
|
||||
'space-in-parens': [ 2, 'never' ],
|
||||
'spaced-comment': [ 2, 'always' ],
|
||||
'strict': 2,
|
||||
'valid-typeof': 2,
|
||||
'yoda': 2,
|
||||
// eslint-plugin-import options.
|
||||
'import/extensions': 2,
|
||||
'import/no-duplicates': 2,
|
||||
// eslint-plugin-react options.
|
||||
'jsx-quotes': [ 2, 'prefer-single' ],
|
||||
'react/display-name': [ 2, { ignoreTranspilerName: false } ],
|
||||
'react/forbid-prop-types': 0,
|
||||
'react/jsx-boolean-value': 2,
|
||||
'react/jsx-closing-bracket-location': 2,
|
||||
'react/jsx-curly-spacing': 2,
|
||||
'react/jsx-equals-spacing': 2,
|
||||
'react/jsx-handler-names': 2,
|
||||
'react/jsx-indent-props': [ 2, 'tab' ],
|
||||
'react/jsx-indent': [ 2, 'tab' ],
|
||||
'react/jsx-key': 2,
|
||||
'react/jsx-max-props-per-line': 0,
|
||||
'react/jsx-no-bind': 0,
|
||||
'react/jsx-no-duplicate-props': 2,
|
||||
'react/jsx-no-literals': 0,
|
||||
'react/jsx-no-undef': 2,
|
||||
'react/jsx-pascal-case': 2,
|
||||
'react/jsx-sort-prop-types': 0,
|
||||
'react/jsx-sort-props': 0,
|
||||
'react/jsx-uses-react': 2,
|
||||
'react/jsx-uses-vars': 2,
|
||||
'react/no-danger': 2,
|
||||
'react/no-deprecated': 2,
|
||||
'react/no-did-mount-set-state': 2,
|
||||
'react/no-did-update-set-state': 2,
|
||||
'react/no-direct-mutation-state': 2,
|
||||
'react/no-is-mounted': 2,
|
||||
'react/no-multi-comp': 0,
|
||||
'react/no-set-state': 0,
|
||||
'react/no-string-refs': 0,
|
||||
'react/no-unknown-property': 2,
|
||||
'react/prefer-es6-class': 2,
|
||||
'react/prop-types': 2,
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 0,
|
||||
'react/jsx-wrap-multilines': [ 2,
|
||||
{
|
||||
declaration: false,
|
||||
assignment: false,
|
||||
return: true
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
module.exports =
|
||||
{
|
||||
protoo :
|
||||
{
|
||||
listenPort : 3443
|
||||
}
|
||||
};
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Tasks:
|
||||
*
|
||||
* gulp prod
|
||||
* Generates the browser app in production mode.
|
||||
*
|
||||
* gulp dev
|
||||
* Generates the browser app in development mode.
|
||||
* gulp dist
|
||||
* Generates the browser app in development mode (unless NODE_ENV is set
|
||||
* to 'production').
|
||||
*
|
||||
* gulp live
|
||||
* Generates the browser app in development mode, opens it and watches
|
||||
* for changes in the source code.
|
||||
* Generates the browser app in development mode (unless NODE_ENV is set
|
||||
* to 'production'), opens it and watches for changes in the source code.
|
||||
*
|
||||
* gulp
|
||||
* Alias for `gulp live`.
|
||||
|
|
@ -23,9 +19,9 @@ 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 touch = require('gulp-touch-cmd');
|
||||
const browserify = require('browserify');
|
||||
const watchify = require('watchify');
|
||||
const envify = require('envify/custom');
|
||||
|
|
@ -50,24 +46,25 @@ const BANNER_OPTIONS =
|
|||
};
|
||||
const OUTPUT_DIR = '../server/public';
|
||||
|
||||
// Default environment.
|
||||
process.env.NODE_ENV = 'development';
|
||||
// Set Node 'development' environment (unless externally set).
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
gutil.log(`NODE_ENV: ${process.env.NODE_ENV}`);
|
||||
|
||||
function logError(error)
|
||||
{
|
||||
gutil.log(gutil.colors.red(String(error)));
|
||||
|
||||
throw error;
|
||||
gutil.log(gutil.colors.red(error.stack));
|
||||
}
|
||||
|
||||
function bundle(options)
|
||||
{
|
||||
options = options || {};
|
||||
|
||||
let watch = !!options.watch;
|
||||
const watch = Boolean(options.watch);
|
||||
|
||||
let bundler = browserify(
|
||||
{
|
||||
entries : path.join(__dirname, PKG.main),
|
||||
entries : PKG.main,
|
||||
extensions : [ '.js', '.jsx' ],
|
||||
// required for sourcemaps (must be false otherwise).
|
||||
debug : process.env.NODE_ENV === 'development',
|
||||
|
|
@ -81,7 +78,12 @@ function bundle(options)
|
|||
.transform('babelify',
|
||||
{
|
||||
presets : [ 'es2015', 'react' ],
|
||||
plugins : [ 'transform-runtime', 'transform-object-assign' ]
|
||||
plugins :
|
||||
[
|
||||
'transform-runtime',
|
||||
'transform-object-assign',
|
||||
'transform-object-rest-spread'
|
||||
]
|
||||
})
|
||||
.transform(envify(
|
||||
{
|
||||
|
|
@ -95,7 +97,7 @@ function bundle(options)
|
|||
|
||||
bundler.on('update', () =>
|
||||
{
|
||||
let start = Date.now();
|
||||
const start = Date.now();
|
||||
|
||||
gutil.log('bundling...');
|
||||
rebundle();
|
||||
|
|
@ -107,6 +109,7 @@ function bundle(options)
|
|||
{
|
||||
return bundler.bundle()
|
||||
.on('error', logError)
|
||||
.pipe(plumber())
|
||||
.pipe(source(`${PKG.name}.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(rename(`${PKG.name}.js`))
|
||||
|
|
@ -122,25 +125,14 @@ function bundle(options)
|
|||
|
||||
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' ];
|
||||
const src =
|
||||
[
|
||||
'gulpfile.js',
|
||||
'lib/**/*.js',
|
||||
'lib/**/*.jsx'
|
||||
];
|
||||
|
||||
return gulp.src(src)
|
||||
.pipe(plumber())
|
||||
|
|
@ -176,7 +168,7 @@ gulp.task('html', () =>
|
|||
|
||||
gulp.task('resources', (done) =>
|
||||
{
|
||||
let dst = path.join(OUTPUT_DIR, 'resources');
|
||||
const dst = path.join(OUTPUT_DIR, 'resources');
|
||||
|
||||
mkdirp.sync(dst);
|
||||
ncp('resources', dst, { stopOnErr: true }, (error) =>
|
||||
|
|
@ -262,18 +254,7 @@ gulp.task('watch', (done) =>
|
|||
done();
|
||||
});
|
||||
|
||||
gulp.task('prod', gulp.series(
|
||||
'env:prod',
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle',
|
||||
'html',
|
||||
'css',
|
||||
'resources'
|
||||
));
|
||||
|
||||
gulp.task('dev', gulp.series(
|
||||
'env:dev',
|
||||
gulp.task('dist', gulp.series(
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle',
|
||||
|
|
@ -283,7 +264,6 @@ gulp.task('dev', gulp.series(
|
|||
));
|
||||
|
||||
gulp.task('live', gulp.series(
|
||||
'env:dev',
|
||||
'clean',
|
||||
'lint',
|
||||
'bundle:watch',
|
||||
|
|
|
|||
|
|
@ -2,21 +2,21 @@
|
|||
|
||||
<html>
|
||||
<head>
|
||||
<title>mediasoup demo</title>
|
||||
<title>mediasoup v2 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'>
|
||||
<meta name='description' content='mediasoup v2 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*');
|
||||
window.localStorage.setItem('debug', '* -engine* -socket* -RIE* *WARN* *ERROR*');
|
||||
|
||||
if (window.antiglobal)
|
||||
{
|
||||
window.antiglobal('___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__', 'RTCPeerConnection');
|
||||
setInterval(window.antiglobal, 5000);
|
||||
window.antiglobal('___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__');
|
||||
setInterval(window.antiglobal, 180000);
|
||||
}
|
||||
</script>
|
||||
<script async src='/mediasoup-demo-app.js'></script>
|
||||
|
|
|
|||
|
|
@ -1,934 +0,0 @@
|
|||
'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 * as urlFactory from './urlFactory';
|
||||
import * as 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);
|
||||
|
||||
// TODO: TMP
|
||||
global.CLIENT = this;
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
videoTrack.stop();
|
||||
stream.removeTrack(videoTrack);
|
||||
|
||||
// New API.
|
||||
if (this._peerconnection.removeTrack)
|
||||
{
|
||||
let sender;
|
||||
|
||||
for (sender of this._peerconnection.getSenders())
|
||||
{
|
||||
if (sender.track === videoTrack)
|
||||
break;
|
||||
}
|
||||
|
||||
this._peerconnection.removeTrack(sender);
|
||||
}
|
||||
// Old API.
|
||||
else
|
||||
{
|
||||
this._peerconnection.addStream(stream);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// New API.
|
||||
if (this._peerconnection.addTrack)
|
||||
{
|
||||
this._peerconnection.addTrack(newVideoTrack, stream);
|
||||
}
|
||||
// Old API.
|
||||
else
|
||||
{
|
||||
this._peerconnection.addStream(stream);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._localStream = newStream;
|
||||
|
||||
// New API.
|
||||
if (this._peerconnection.addTrack)
|
||||
{
|
||||
this._peerconnection.addTrack(newVideoTrack, stream);
|
||||
}
|
||||
// Old API.
|
||||
else
|
||||
{
|
||||
this._peerconnection.addStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return this._protooPeer.send('disableremotevideo', { msid, disable: true })
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.warn('disableRemoteVideo() failed: %o', error);
|
||||
});
|
||||
}
|
||||
|
||||
enableRemoteVideo(msid)
|
||||
{
|
||||
return 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;
|
||||
}
|
||||
|
||||
case 'activespeaker':
|
||||
{
|
||||
let data = request.data;
|
||||
|
||||
this.emit('activespeaker', data.peer, data.level);
|
||||
accept();
|
||||
|
||||
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.PC = this._peerconnection;
|
||||
|
||||
if (this._localStream)
|
||||
this._peerconnection.addStream(this._localStream);
|
||||
|
||||
this._peerconnection.addEventListener('iceconnectionstatechange', () =>
|
||||
{
|
||||
let state = this._peerconnection.iceConnectionState;
|
||||
|
||||
if (state === 'failed')
|
||||
logger.warn('peerconnection "iceconnectionstatechange" event [state:failed]');
|
||||
else
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
import debug from 'debug';
|
||||
|
||||
const APP_NAME = 'mediasoup-demo';
|
||||
|
|
@ -10,20 +8,22 @@ export default class Logger
|
|||
{
|
||||
if (prefix)
|
||||
{
|
||||
this._debug = debug(APP_NAME + ':' + prefix);
|
||||
this._warn = debug(APP_NAME + ':WARN:' + prefix);
|
||||
this._error = debug(APP_NAME + ':ERROR:' + 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._warn = debug(`${APP_NAME}:WARN`);
|
||||
this._error = debug(`${APP_NAME}:ERROR`);
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
this._debug.log = console.info.bind(console);
|
||||
this._warn.log = console.warn.bind(console);
|
||||
this._error.log = console.error.bind(console);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
get debug()
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
|
||||
import Logger from '../Logger';
|
||||
import muiTheme from './muiTheme';
|
||||
import Notifier from './Notifier';
|
||||
import Room from './Room';
|
||||
|
||||
const logger = new Logger('App'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class App extends React.Component
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
|
||||
return (
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<div data-component='App'>
|
||||
<Notifier ref='Notifier'/>
|
||||
|
||||
<Room
|
||||
peerId={props.peerId}
|
||||
roomId={props.roomId}
|
||||
onNotify={this.handleNotify.bind(this)}
|
||||
onHideNotification={this.handleHideNotification.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
handleNotify(data)
|
||||
{
|
||||
this.refs.Notifier.notify(data);
|
||||
}
|
||||
|
||||
handleHideNotification(uid)
|
||||
{
|
||||
this.refs.Notifier.hideNotification(uid);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes =
|
||||
{
|
||||
peerId : PropTypes.string.isRequired,
|
||||
roomId : PropTypes.string.isRequired
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { RIEInput } from 'riek';
|
||||
|
||||
export default class EditableInput extends React.Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
value,
|
||||
propName,
|
||||
className,
|
||||
classLoading,
|
||||
classInvalid,
|
||||
editProps,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<RIEInput
|
||||
value={value}
|
||||
propName={propName}
|
||||
className={className}
|
||||
classLoading={classLoading}
|
||||
classInvalid={classInvalid}
|
||||
shouldBlockWhileLoading
|
||||
editProps={editProps}
|
||||
change={(data) => onChange(data)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps)
|
||||
{
|
||||
if (nextProps.value === this.props.value)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
EditableInput.propTypes =
|
||||
{
|
||||
value : PropTypes.string,
|
||||
propName : PropTypes.string.isRequired,
|
||||
className : PropTypes.string,
|
||||
classLoading : PropTypes.string,
|
||||
classInvalid : PropTypes.string,
|
||||
editProps : PropTypes.any,
|
||||
onChange : PropTypes.func.isRequired
|
||||
};
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import MicOffIcon from 'material-ui/svg-icons/av/mic-off';
|
||||
import VideoCamOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import ChangeVideoCamIcon from 'material-ui/svg-icons/av/repeat';
|
||||
import classnames from 'classnames';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('LocalVideo'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class LocalVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
micMuted : false,
|
||||
webcam : props.stream && !!props.stream.getVideoTracks()[0],
|
||||
togglingWebcam : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='LocalVideo'
|
||||
className={classnames(`state-${props.connectionState}`, {
|
||||
'active-speaker' : props.isActiveSpeaker
|
||||
})}
|
||||
>
|
||||
{props.stream ?
|
||||
<Video
|
||||
stream={props.stream}
|
||||
resolution={props.resolution}
|
||||
muted
|
||||
mirror={props.webcamType === 'front'}
|
||||
onResolutionChange={this.handleResolutionChange.bind(this)}
|
||||
/>
|
||||
:null}
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteMic.bind(this)}
|
||||
>
|
||||
<MicOffIcon
|
||||
color={!state.micMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={state.togglingWebcam}
|
||||
onClick={this.handleClickWebcam.bind(this)}
|
||||
>
|
||||
<VideoCamOffIcon
|
||||
color={state.webcam ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
{props.multipleWebcams ?
|
||||
<IconButton
|
||||
className='control'
|
||||
disabled={!state.webcam || state.togglingWebcam}
|
||||
onClick={this.handleClickChangeWebcam.bind(this)}
|
||||
>
|
||||
<ChangeVideoCamIcon
|
||||
color='#fff'
|
||||
/>
|
||||
</IconButton>
|
||||
:null}
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peerId}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
this.setState({ webcam: nextProps.stream && !!nextProps.stream.getVideoTracks()[0] });
|
||||
}
|
||||
|
||||
handleClickMuteMic()
|
||||
{
|
||||
logger.debug('handleClickMuteMic()');
|
||||
|
||||
let value = !this.state.micMuted;
|
||||
|
||||
this.props.onMicMute(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ micMuted: value });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickWebcam()
|
||||
{
|
||||
logger.debug('handleClickWebcam()');
|
||||
|
||||
let value = !this.state.webcam;
|
||||
|
||||
this.setState({ togglingWebcam: true });
|
||||
|
||||
this.props.onWebcamToggle(value)
|
||||
.then(() =>
|
||||
{
|
||||
this.setState({ webcam: value, togglingWebcam: false });
|
||||
})
|
||||
.catch(() =>
|
||||
{
|
||||
this.setState({ togglingWebcam: false });
|
||||
});
|
||||
}
|
||||
|
||||
handleClickChangeWebcam()
|
||||
{
|
||||
logger.debug('handleClickChangeWebcam()');
|
||||
|
||||
this.props.onWebcamChange();
|
||||
}
|
||||
|
||||
handleResolutionChange()
|
||||
{
|
||||
logger.debug('handleResolutionChange()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
}
|
||||
|
||||
LocalVideo.propTypes =
|
||||
{
|
||||
peerId : PropTypes.string.isRequired,
|
||||
stream : PropTypes.object,
|
||||
resolution : PropTypes.string,
|
||||
multipleWebcams : PropTypes.bool.isRequired,
|
||||
webcamType : PropTypes.string,
|
||||
connectionState : PropTypes.string,
|
||||
isActiveSpeaker : PropTypes.bool.isRequired,
|
||||
onMicMute : PropTypes.func.isRequired,
|
||||
onWebcamToggle : PropTypes.func.isRequired,
|
||||
onWebcamChange : PropTypes.func.isRequired,
|
||||
onResolutionChange : PropTypes.func.isRequired
|
||||
};
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import PeerView from './PeerView';
|
||||
|
||||
class Me extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._mounted = false;
|
||||
this._rootNode = null;
|
||||
this._tooltip = true;
|
||||
|
||||
// TODO: Issue when using react-tooltip in Edge:
|
||||
// https://github.com/wwayne/react-tooltip/issues/328
|
||||
if (getDeviceInfo().flag === 'msedge')
|
||||
this._tooltip = false;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
connected,
|
||||
me,
|
||||
micProducer,
|
||||
webcamProducer,
|
||||
onChangeDisplayName,
|
||||
onMuteMic,
|
||||
onUnmuteMic,
|
||||
onEnableWebcam,
|
||||
onDisableWebcam,
|
||||
onChangeWebcam
|
||||
} = this.props;
|
||||
|
||||
let micState;
|
||||
|
||||
if (!me.canSendMic)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer)
|
||||
micState = 'unsupported';
|
||||
else if (!micProducer.locallyPaused && !micProducer.remotelyPaused)
|
||||
micState = 'on';
|
||||
else
|
||||
micState = 'off';
|
||||
|
||||
let webcamState;
|
||||
|
||||
if (!me.canSendWebcam)
|
||||
webcamState = 'unsupported';
|
||||
else if (webcamProducer)
|
||||
webcamState = 'on';
|
||||
else
|
||||
webcamState = 'off';
|
||||
|
||||
let changeWebcamState;
|
||||
|
||||
if (Boolean(webcamProducer) && me.canChangeWebcam)
|
||||
changeWebcamState = 'on';
|
||||
else
|
||||
changeWebcamState = 'unsupported';
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamProducer) &&
|
||||
!webcamProducer.locallyPaused &&
|
||||
!webcamProducer.remotelyPaused
|
||||
);
|
||||
|
||||
let tip;
|
||||
|
||||
if (!me.displayNameSet)
|
||||
tip = 'Click on your name to change it';
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='Me'
|
||||
ref={(node) => (this._rootNode = node)}
|
||||
data-tip={tip}
|
||||
data-tip-disable={!tip}
|
||||
data-type='dark'
|
||||
>
|
||||
{connected ?
|
||||
<div className='controls'>
|
||||
<div
|
||||
className={classnames('button', 'mic', micState)}
|
||||
onClick={() =>
|
||||
{
|
||||
micState === 'on' ? onMuteMic() : onUnmuteMic();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'webcam', webcamState, {
|
||||
disabled : me.webcamInProgress
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
webcamState === 'on' ? onDisableWebcam() : onEnableWebcam();
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'change-webcam', changeWebcamState, {
|
||||
disabled : me.webcamInProgress
|
||||
})}
|
||||
onClick={() => onChangeWebcam()}
|
||||
/>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<PeerView
|
||||
isMe
|
||||
peer={me}
|
||||
audioTrack={micProducer ? micProducer.track : null}
|
||||
videoTrack={webcamProducer ? webcamProducer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
audioCodec={micProducer ? micProducer.codec : null}
|
||||
videoCodec={webcamProducer ? webcamProducer.codec : null}
|
||||
onChangeDisplayName={(displayName) => onChangeDisplayName(displayName)}
|
||||
/>
|
||||
|
||||
{this._tooltip ?
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this._mounted = true;
|
||||
|
||||
if (this._tooltip)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (!this._mounted || this.props.me.displayNameSet)
|
||||
return;
|
||||
|
||||
ReactTooltip.show(this._rootNode);
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
if (this._tooltip)
|
||||
{
|
||||
if (nextProps.me.displayNameSet)
|
||||
ReactTooltip.hide(this._rootNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Me.propTypes =
|
||||
{
|
||||
connected : PropTypes.bool.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
micProducer : appPropTypes.Producer,
|
||||
webcamProducer : appPropTypes.Producer,
|
||||
onChangeDisplayName : PropTypes.func.isRequired,
|
||||
onMuteMic : PropTypes.func.isRequired,
|
||||
onUnmuteMic : PropTypes.func.isRequired,
|
||||
onEnableWebcam : PropTypes.func.isRequired,
|
||||
onDisableWebcam : PropTypes.func.isRequired,
|
||||
onChangeWebcam : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const producersArray = Object.values(state.producers);
|
||||
const micProducer =
|
||||
producersArray.find((producer) => producer.source === 'mic');
|
||||
const webcamProducer =
|
||||
producersArray.find((producer) => producer.source === 'webcam');
|
||||
|
||||
return {
|
||||
connected : state.room.state === 'connected',
|
||||
me : state.me,
|
||||
micProducer : micProducer,
|
||||
webcamProducer : webcamProducer
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onChangeDisplayName : (displayName) =>
|
||||
{
|
||||
dispatch(requestActions.changeDisplayName(displayName));
|
||||
},
|
||||
onMuteMic : () => dispatch(requestActions.muteMic()),
|
||||
onUnmuteMic : () => dispatch(requestActions.unmuteMic()),
|
||||
onEnableWebcam : () => dispatch(requestActions.enableWebcam()),
|
||||
onDisableWebcam : () => dispatch(requestActions.disableWebcam()),
|
||||
onChangeWebcam : () => dispatch(requestActions.changeWebcam())
|
||||
};
|
||||
};
|
||||
|
||||
const MeContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Me);
|
||||
|
||||
export default MeContainer;
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as stateActions from '../redux/stateActions';
|
||||
import { Appear } from './transitions';
|
||||
|
||||
const Notifications = ({ notifications, onClick }) =>
|
||||
{
|
||||
return (
|
||||
<div data-component='Notifications'>
|
||||
{
|
||||
notifications.map((notification) =>
|
||||
{
|
||||
return (
|
||||
<Appear key={notification.id} duration={250}>
|
||||
<div
|
||||
className={classnames('notification', notification.type)}
|
||||
onClick={() => onClick(notification.id)}
|
||||
>
|
||||
<div className='icon' />
|
||||
<p className='text'>{notification.text}</p>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Notifications.propTypes =
|
||||
{
|
||||
notifications : PropTypes.arrayOf(appPropTypes.Notification).isRequired,
|
||||
onClick : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const { notifications } = state;
|
||||
|
||||
return { notifications };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onClick : (notificationId) =>
|
||||
{
|
||||
dispatch(stateActions.removeNotification(notificationId));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const NotificationsContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Notifications);
|
||||
|
||||
export default NotificationsContainer;
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import NotificationSystem from 'react-notification-system';
|
||||
|
||||
const STYLE =
|
||||
{
|
||||
NotificationItem :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '6px 10px',
|
||||
backgroundColor : 'rgba(255,255,255, 0.9)',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 400,
|
||||
fontSize : '1rem',
|
||||
cursor : 'default',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none',
|
||||
userSelect : 'none',
|
||||
transition : '0.15s ease-in-out'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '2px solid rgba(255,0,78, 0.75)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid rgba(73,206,62, 0.75)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#000',
|
||||
borderTop : '4px solid #ff0014'
|
||||
}
|
||||
},
|
||||
Title :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
margin : '0 0 8px 0',
|
||||
fontFamily : 'Roboto',
|
||||
fontWeight : 500,
|
||||
fontSize : '1.1rem',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
color : 'rgba(255,0,78, 0.85)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
color : 'rgba(73,206,62, 0.9)'
|
||||
},
|
||||
error :
|
||||
{
|
||||
color : '#ff0014'
|
||||
}
|
||||
},
|
||||
Dismiss :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
display : 'none'
|
||||
}
|
||||
},
|
||||
Action :
|
||||
{
|
||||
DefaultStyle :
|
||||
{
|
||||
padding : '8px 24px',
|
||||
fontSize : '1.2rem',
|
||||
cursor : 'pointer',
|
||||
userSelect : 'none',
|
||||
WebkitUserSelect : 'none',
|
||||
MozUserSelect : 'none'
|
||||
},
|
||||
info :
|
||||
{
|
||||
backgroundColor : 'rgba(255,0,78, 1)'
|
||||
},
|
||||
success :
|
||||
{
|
||||
backgroundColor : 'rgba(73,206,62, 0.75)'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class Notifier extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<NotificationSystem ref='NotificationSystem' style={STYLE} allowHTML={false}/>
|
||||
);
|
||||
}
|
||||
|
||||
notify(data)
|
||||
{
|
||||
let data2;
|
||||
|
||||
switch (data.level)
|
||||
{
|
||||
case 'info' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'success' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 1
|
||||
}, data);
|
||||
break;
|
||||
|
||||
case 'error' :
|
||||
data2 = Object.assign(
|
||||
{
|
||||
position : 'tr',
|
||||
dismissible : true,
|
||||
autoDismiss : 3
|
||||
}, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`unknown level "${data.level}"`);
|
||||
}
|
||||
|
||||
this.refs.NotificationSystem.addNotification(data2);
|
||||
}
|
||||
|
||||
hideNotification(uid)
|
||||
{
|
||||
this.refs.NotificationSystem.removeNotification(uid);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import PeerView from './PeerView';
|
||||
|
||||
const Peer = (props) =>
|
||||
{
|
||||
const {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer
|
||||
} = props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const videoVisible = (
|
||||
Boolean(webcamConsumer) &&
|
||||
!webcamConsumer.locallyPaused &&
|
||||
!webcamConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
let videoProfile;
|
||||
|
||||
if (webcamConsumer)
|
||||
videoProfile = webcamConsumer.profile;
|
||||
|
||||
return (
|
||||
<div data-component='Peer'>
|
||||
<div className='indicators'>
|
||||
{!micEnabled ?
|
||||
<div className='icon mic-off' />
|
||||
:null
|
||||
}
|
||||
{!videoVisible ?
|
||||
<div className='icon webcam-off' />
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
|
||||
{videoVisible && !webcamConsumer.supported ?
|
||||
<div className='incompatible-video'>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
|
||||
<PeerView
|
||||
peer={peer}
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
videoProfile={videoProfile}
|
||||
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Peer.propTypes =
|
||||
{
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
const webcamConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
|
||||
return {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const PeerContainer = connect(mapStateToProps)(Peer);
|
||||
|
||||
export default PeerContainer;
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Spinner from 'react-spinner';
|
||||
import hark from 'hark';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import EditableInput from './EditableInput';
|
||||
|
||||
export default class PeerView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
volume : 0, // Integer from 0 to 10.,
|
||||
videoWidth : null,
|
||||
videoHeight : null
|
||||
};
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
||||
// Hark instance.
|
||||
// @type {Object}
|
||||
this._hark = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._videoResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
peer,
|
||||
videoVisible,
|
||||
videoProfile,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
onChangeDisplayName
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
volume,
|
||||
videoWidth,
|
||||
videoHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='PeerView'>
|
||||
<div className='info'>
|
||||
<div className={classnames('media', { 'is-me': isMe })}>
|
||||
<div className='box'>
|
||||
{audioCodec ?
|
||||
<p className='codec'>{audioCodec}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{videoCodec ?
|
||||
<p className='codec'>{videoCodec} {videoProfile}</p>
|
||||
:null
|
||||
}
|
||||
|
||||
{(videoVisible && videoWidth !== null) ?
|
||||
<p className='resolution'>{videoWidth}x{videoHeight}</p>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classnames('peer', { 'is-me': isMe })}>
|
||||
{isMe ?
|
||||
<EditableInput
|
||||
value={peer.displayName}
|
||||
propName='displayName'
|
||||
className='display-name editable'
|
||||
classLoading='loading'
|
||||
classInvalid='invalid'
|
||||
shouldBlockWhileLoading
|
||||
editProps={{
|
||||
maxLength : 20,
|
||||
autoCorrect : false,
|
||||
spellCheck : false
|
||||
}}
|
||||
onChange={({ displayName }) => onChangeDisplayName(displayName)}
|
||||
/>
|
||||
:
|
||||
<span className='display-name'>
|
||||
{peer.displayName}
|
||||
</span>
|
||||
}
|
||||
|
||||
<div className='row'>
|
||||
<span
|
||||
className={classnames('device-icon', peer.device.flag)}
|
||||
/>
|
||||
<span className='device-version'>
|
||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({
|
||||
hidden : !videoVisible,
|
||||
'is-me' : isMe,
|
||||
loading : videoProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
muted={isMe}
|
||||
/>
|
||||
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${volume}`)} />
|
||||
</div>
|
||||
|
||||
{videoProfile === 'none' ?
|
||||
<div className='spinner-container'>
|
||||
<Spinner />
|
||||
</div>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack, videoTrack } = this.props;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack, videoTrack } = nextProps;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
}
|
||||
|
||||
_setTracks(audioTrack, videoTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
this._videoTrack = videoTrack;
|
||||
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
this._hideVideoResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (audioTrack || videoTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
if (videoTrack)
|
||||
stream.addTrack(videoTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (audioTrack)
|
||||
this._runHark(stream);
|
||||
|
||||
if (videoTrack)
|
||||
this._showVideoResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_runHark(stream)
|
||||
{
|
||||
if (!stream.getAudioTracks()[0])
|
||||
throw new Error('_runHark() | given stream has no audio track');
|
||||
|
||||
this._hark = hark(stream, { play: false });
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
this._hark.on('volume_change', (dBs, threshold) =>
|
||||
{
|
||||
// The exact formula to convert from dBs (-100..0) to linear (0..1) is:
|
||||
// Math.pow(10, dBs / 20)
|
||||
// However it does not produce a visually useful output, so let exagerate
|
||||
// it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to
|
||||
// minimize component renderings.
|
||||
let volume = Math.round(Math.pow(10, dBs / 85) * 10);
|
||||
|
||||
if (volume === 1)
|
||||
volume = 0;
|
||||
|
||||
if (volume !== this.state.volume)
|
||||
this.setState({ volume: volume });
|
||||
});
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { videoWidth, videoHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
videoWidth : video.videoWidth,
|
||||
videoHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideVideoResolution()
|
||||
{
|
||||
this.setState({ videoWidth: null, videoHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
PeerView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
peer : PropTypes.oneOfType(
|
||||
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
||||
audioTrack : PropTypes.any,
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool.isRequired,
|
||||
videoProfile : PropTypes.string,
|
||||
audioCodec : PropTypes.string,
|
||||
videoCodec : PropTypes.string,
|
||||
onChangeDisplayName : PropTypes.func
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import { Appear } from './transitions';
|
||||
import Peer from './Peer';
|
||||
|
||||
const Peers = ({ peers, activeSpeakerName }) =>
|
||||
{
|
||||
return (
|
||||
<div data-component='Peers'>
|
||||
{
|
||||
peers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
<Appear key={peer.name} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'active-speaker' : peer.name === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<Peer name={peer.name} />
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Peers.propTypes =
|
||||
{
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
activeSpeakerName : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
// TODO: This is not OK since it's creating a new array every time, so triggering a
|
||||
// component rendering.
|
||||
const peersArray = Object.values(state.peers);
|
||||
|
||||
return {
|
||||
peers : peersArray,
|
||||
activeSpeakerName : state.room.activeSpeakerName
|
||||
};
|
||||
};
|
||||
|
||||
const PeersContainer = connect(mapStateToProps)(Peers);
|
||||
|
||||
export default PeersContainer;
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from 'material-ui/IconButton/IconButton';
|
||||
import VolumeOffIcon from 'material-ui/svg-icons/av/volume-off';
|
||||
import VideoOffIcon from 'material-ui/svg-icons/av/videocam-off';
|
||||
import classnames from 'classnames';
|
||||
import Video from './Video';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('RemoteVideo');
|
||||
|
||||
export default class RemoteVideo extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
audioMuted : false
|
||||
};
|
||||
|
||||
let videoTrack = props.stream.getVideoTracks()[0];
|
||||
|
||||
if (videoTrack)
|
||||
{
|
||||
videoTrack.addEventListener('mute', () =>
|
||||
{
|
||||
logger.debug('video track "mute" event');
|
||||
});
|
||||
|
||||
videoTrack.addEventListener('unmute', () =>
|
||||
{
|
||||
logger.debug('video track "unmute" event');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let videoTrack = props.stream.getVideoTracks()[0];
|
||||
let videoEnabled = videoTrack && videoTrack.enabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component='RemoteVideo'
|
||||
className={classnames({
|
||||
fullsize : !!props.fullsize,
|
||||
'active-speaker' : props.isActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Video
|
||||
stream={props.stream}
|
||||
muted={state.audioMuted}
|
||||
videoDisabled={!videoEnabled}
|
||||
/>
|
||||
|
||||
<div className='controls'>
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickMuteAudio.bind(this)}
|
||||
>
|
||||
<VolumeOffIcon
|
||||
color={!state.audioMuted ? '#fff' : '#ff0000'}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
{videoTrack ?
|
||||
<IconButton
|
||||
className='control'
|
||||
onClick={this.handleClickDisableVideo.bind(this)}
|
||||
>
|
||||
<VideoOffIcon
|
||||
color={videoEnabled ? '#fff' : '#ff8a00'}
|
||||
/>
|
||||
</IconButton>
|
||||
:null}
|
||||
</div>
|
||||
|
||||
<div className='info'>
|
||||
<div className='peer-id'>{props.peer.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleClickMuteAudio()
|
||||
{
|
||||
logger.debug('handleClickMuteAudio()');
|
||||
|
||||
let value = !this.state.audioMuted;
|
||||
|
||||
this.setState({ audioMuted: value });
|
||||
}
|
||||
|
||||
handleClickDisableVideo()
|
||||
{
|
||||
logger.debug('handleClickDisableVideo()');
|
||||
|
||||
let videoTrack = this.props.stream.getVideoTracks()[0];
|
||||
let videoEnabled = videoTrack && videoTrack.enabled;
|
||||
let stream = this.props.stream;
|
||||
let msid = stream.jitsiRemoteId || stream.id;
|
||||
|
||||
if (videoEnabled)
|
||||
{
|
||||
this.props.onDisableVideo(msid)
|
||||
.then(() =>
|
||||
{
|
||||
videoTrack.enabled = false;
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.props.onEnableVideo(msid)
|
||||
.then(() =>
|
||||
{
|
||||
videoTrack.enabled = true;
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteVideo.propTypes =
|
||||
{
|
||||
peer : PropTypes.object.isRequired,
|
||||
stream : PropTypes.object.isRequired,
|
||||
fullsize : PropTypes.bool,
|
||||
isActiveSpeaker : PropTypes.bool.isRequired,
|
||||
onDisableVideo : PropTypes.func.isRequired,
|
||||
onEnableVideo : PropTypes.func.isRequired
|
||||
};
|
||||
|
|
@ -1,531 +1,153 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import ClipboardButton from 'react-clipboard.js';
|
||||
import browser from 'bowser';
|
||||
import TransitionAppear from './TransitionAppear';
|
||||
import LocalVideo from './LocalVideo';
|
||||
import RemoteVideo from './RemoteVideo';
|
||||
import Stats from './Stats';
|
||||
import Logger from '../Logger';
|
||||
import * as utils from '../utils';
|
||||
import Client from '../Client';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import { Appear } from './transitions';
|
||||
import Me from './Me';
|
||||
import Peers from './Peers';
|
||||
import Notifications from './Notifications';
|
||||
|
||||
const logger = new Logger('Room');
|
||||
const STATS_INTERVAL = 1000;
|
||||
|
||||
export default class Room extends React.Component
|
||||
const Room = (
|
||||
{
|
||||
room,
|
||||
me,
|
||||
amActiveSpeaker,
|
||||
onRoomLinkCopy,
|
||||
onSetAudioMode,
|
||||
onRestartIce
|
||||
}) =>
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
peers : {},
|
||||
localStream : null,
|
||||
localVideoResolution : null, // qvga / vga / hd / fullhd.
|
||||
multipleWebcams : false,
|
||||
webcamType : null,
|
||||
connectionState : null,
|
||||
remoteStreams : {},
|
||||
showStats : false,
|
||||
stats : null,
|
||||
activeSpeakerId : null
|
||||
};
|
||||
|
||||
// Mounted flag
|
||||
this._mounted = false;
|
||||
// Client instance
|
||||
this._client = null;
|
||||
// Timer to retrieve RTC stats.
|
||||
this._statsTimer = null;
|
||||
|
||||
// TODO: TMP
|
||||
global.ROOM = this;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
let numPeers = Object.keys(state.remoteStreams).length;
|
||||
|
||||
return (
|
||||
<TransitionAppear duration={2000}>
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<Notifications />
|
||||
|
||||
<div className='state'>
|
||||
<div className={classnames('icon', room.state)} />
|
||||
<p className={classnames('text', room.state)}>{room.state}</p>
|
||||
</div>
|
||||
|
||||
<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.
|
||||
button-href={room.url}
|
||||
button-target='_blank'
|
||||
data-clipboard-text={room.url}
|
||||
onSuccess={onRoomLinkCopy}
|
||||
onClick={(event) =>
|
||||
{
|
||||
// If this is a 'Open in new window/tab' don't prevent
|
||||
// click default action.
|
||||
if (
|
||||
event.ctrlKey || event.shiftKey || event.metaKey ||
|
||||
// Middle click (IE > 9 and everyone else).
|
||||
(event.button && event.button === 1)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
invite people to this room
|
||||
invitation link
|
||||
</ClipboardButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='remote-videos'>
|
||||
{
|
||||
Object.keys(state.remoteStreams).map((msid) =>
|
||||
{
|
||||
let stream = state.remoteStreams[msid];
|
||||
let peer;
|
||||
<Peers />
|
||||
|
||||
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}
|
||||
isActiveSpeaker={peer.id === state.activeSpeakerId}
|
||||
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}
|
||||
isActiveSpeaker={props.peerId === state.activeSpeakerId}
|
||||
onMicMute={this.handleLocalMute.bind(this)}
|
||||
onWebcamToggle={this.handleLocalWebcamToggle.bind(this)}
|
||||
onWebcamChange={this.handleLocalWebcamChange.bind(this)}
|
||||
onResolutionChange={this.handleLocalResolutionChange.bind(this)}
|
||||
/>
|
||||
|
||||
{state.showStats ?
|
||||
<TransitionAppear duration={500}>
|
||||
<Stats
|
||||
stats={state.stats || new Map()}
|
||||
onClose={this.handleStatsClose.bind(this)}
|
||||
/>
|
||||
</TransitionAppear>
|
||||
:
|
||||
<div
|
||||
className='show-stats'
|
||||
onClick={this.handleClickShowStats.bind(this)}
|
||||
className={classnames('me-container', {
|
||||
'active-speaker' : amActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Me />
|
||||
</div>
|
||||
|
||||
<div className='sidebar'>
|
||||
<div
|
||||
className={classnames('button', 'audio-only', {
|
||||
on : me.audioOnly,
|
||||
disabled : me.audioOnlyInProgress
|
||||
})}
|
||||
data-tip='Toggle audio only mode'
|
||||
data-type='dark'
|
||||
onClick={() => onSetAudioMode(!me.audioOnly)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'restart-ice', {
|
||||
disabled : me.restartIceInProgress
|
||||
})}
|
||||
data-tip='Restart ICE'
|
||||
data-type='dark'
|
||||
onClick={() => onRestartIce()}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
</div>
|
||||
</TransitionAppear>
|
||||
</Appear>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
// Set flag
|
||||
this._mounted = true;
|
||||
|
||||
// Run the client
|
||||
this._runClient();
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
// Unset flag
|
||||
this._mounted = false;
|
||||
|
||||
// Close client
|
||||
this._client.removeAllListeners();
|
||||
this._client.close();
|
||||
|
||||
// Close local MediaStream
|
||||
if (state.localStream)
|
||||
utils.closeMediaStream(state.localStream);
|
||||
}
|
||||
|
||||
handleRoomLinkCopied()
|
||||
{
|
||||
logger.debug('handleRoomLinkCopied()');
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
position : 'tr',
|
||||
title : 'Room URL copied to the clipboard',
|
||||
message : 'Share it with others to join this room'
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalMute(value)
|
||||
{
|
||||
logger.debug('handleLocalMute() [value:%s]', value);
|
||||
|
||||
let micTrack = this.state.localStream.getAudioTracks()[0];
|
||||
|
||||
if (!micTrack)
|
||||
return Promise.reject(new Error('no audio track'));
|
||||
|
||||
micTrack.enabled = !value;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
handleLocalWebcamToggle(value)
|
||||
{
|
||||
logger.debug('handleLocalWebcamToggle() [value:%s]', value);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
if (value)
|
||||
return this._client.addVideo();
|
||||
else
|
||||
return this._client.removeVideo();
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let localStream = this.state.localStream;
|
||||
|
||||
this.setState({ localStream });
|
||||
});
|
||||
}
|
||||
|
||||
handleLocalWebcamChange()
|
||||
{
|
||||
logger.debug('handleLocalWebcamChange()');
|
||||
|
||||
this._client.changeWebcam();
|
||||
}
|
||||
|
||||
handleLocalResolutionChange()
|
||||
{
|
||||
logger.debug('handleLocalResolutionChange()');
|
||||
|
||||
if (!utils.canChangeResolution())
|
||||
{
|
||||
logger.warn('changing local resolution not implemented for this browser');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._client.changeVideoResolution();
|
||||
}
|
||||
|
||||
handleStatsClose()
|
||||
{
|
||||
logger.debug('handleStatsClose()');
|
||||
|
||||
this.setState({ showStats: false });
|
||||
this._stopStats();
|
||||
}
|
||||
|
||||
handleClickShowStats()
|
||||
{
|
||||
logger.debug('handleClickShowStats()');
|
||||
|
||||
this.setState({ showStats: true });
|
||||
this._startStats();
|
||||
}
|
||||
|
||||
handleDisableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleDisableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
return this._client.disableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
handleEnableRemoteVideo(msid)
|
||||
{
|
||||
logger.debug('handleEnableRemoteVideo() [msid:"%s"]', msid);
|
||||
|
||||
return this._client.enableRemoteVideo(msid);
|
||||
}
|
||||
|
||||
_runClient()
|
||||
{
|
||||
let peerId = this.props.peerId;
|
||||
let roomId = this.props.roomId;
|
||||
|
||||
logger.debug('_runClient() [peerId:"%s", roomId:"%s"]', peerId, roomId);
|
||||
|
||||
this._client = new Client(peerId, roomId);
|
||||
|
||||
this._client.on('localstream', (stream, resolution) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
localStream : stream,
|
||||
localVideoResolution : resolution
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('join', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
title : 'Yes!',
|
||||
message : 'You are in the room!',
|
||||
image : '/resources/images/room.svg',
|
||||
imageWidth : 80,
|
||||
imageHeight : 80
|
||||
});
|
||||
|
||||
// Start retrieving WebRTC stats (unless mobile or Edge).
|
||||
if (utils.isDesktop() && !browser.msedge)
|
||||
{
|
||||
this.setState({ showStats: true });
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
this._startStats();
|
||||
}, STATS_INTERVAL / 2);
|
||||
}
|
||||
});
|
||||
|
||||
this._client.on('close', (error) =>
|
||||
{
|
||||
// Clear remote streams (for reconnections) and more stuff.
|
||||
this.setState(
|
||||
{
|
||||
remoteStreams : {},
|
||||
activeSpeakerId : null
|
||||
});
|
||||
|
||||
if (error)
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Error',
|
||||
message : error.message
|
||||
});
|
||||
}
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('disconnected', () =>
|
||||
{
|
||||
// Clear remote streams (for reconnections).
|
||||
this.setState({ remoteStreams: {} });
|
||||
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'error',
|
||||
title : 'Warning',
|
||||
message : 'app disconnected'
|
||||
});
|
||||
|
||||
// Stop retrieving WebRTC stats.
|
||||
this._stopStats();
|
||||
});
|
||||
|
||||
this._client.on('numwebcams', (num) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
multipleWebcams : (num > 1 ? true : false)
|
||||
});
|
||||
});
|
||||
|
||||
this._client.on('webcamtype', (type) =>
|
||||
{
|
||||
this.setState({ webcamType: type });
|
||||
});
|
||||
|
||||
this._client.on('peers', (peers) =>
|
||||
{
|
||||
let peersObject = {};
|
||||
|
||||
for (let peer of peers)
|
||||
{
|
||||
peersObject[peer.id] = peer;
|
||||
}
|
||||
|
||||
this.setState({ peers: peersObject });
|
||||
});
|
||||
|
||||
this._client.on('addpeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'success',
|
||||
message : `${peer.id} joined the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('updatepeer', (peer) =>
|
||||
{
|
||||
let peers = this.state.peers;
|
||||
|
||||
peers[peer.id] = peer;
|
||||
this.setState({ peers });
|
||||
});
|
||||
|
||||
this._client.on('removepeer', (peer) =>
|
||||
{
|
||||
this.props.onNotify(
|
||||
{
|
||||
level : 'info',
|
||||
message : `${peer.id} left the room`
|
||||
});
|
||||
|
||||
let peers = this.state.peers;
|
||||
|
||||
peer = peers[peer.id];
|
||||
if (!peer)
|
||||
return;
|
||||
|
||||
delete peers[peer.id];
|
||||
|
||||
// NOTE: This shouldn't be needed but Safari 11 does not fire pc "removestream"
|
||||
// nor stream "removetrack" nor track "ended", so we need to cleanup remote
|
||||
// streams when a peer leaves.
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
for (let msid of peer.msids)
|
||||
{
|
||||
delete remoteStreams[msid];
|
||||
}
|
||||
|
||||
this.setState({ peers, remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('connectionstate', (state) =>
|
||||
{
|
||||
this.setState({ connectionState: state });
|
||||
});
|
||||
|
||||
this._client.on('addstream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
let streamId = stream.jitsiRemoteId || stream.id;
|
||||
|
||||
remoteStreams[streamId] = stream;
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removestream', (stream) =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
let streamId = stream.jitsiRemoteId || stream.id;
|
||||
|
||||
delete remoteStreams[streamId];
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('addtrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('removetrack', () =>
|
||||
{
|
||||
let remoteStreams = this.state.remoteStreams;
|
||||
|
||||
this.setState({ remoteStreams });
|
||||
});
|
||||
|
||||
this._client.on('forcestreamsupdate', () =>
|
||||
{
|
||||
// Just firef for Firefox due to bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
this._client.on('activespeaker', (peer) =>
|
||||
{
|
||||
this.setState(
|
||||
{
|
||||
activeSpeakerId : (peer ? peer.id : null)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_startStats()
|
||||
{
|
||||
logger.debug('_startStats()');
|
||||
|
||||
getStats.call(this);
|
||||
|
||||
function getStats()
|
||||
{
|
||||
this._client.getStats()
|
||||
.then((stats) =>
|
||||
{
|
||||
if (!this._mounted)
|
||||
return;
|
||||
|
||||
this.setState({ stats });
|
||||
|
||||
this._statsTimer = setTimeout(() =>
|
||||
{
|
||||
getStats.call(this);
|
||||
}, STATS_INTERVAL);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('getStats() failed: %o', error);
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
// this._statsTimer = setTimeout(() =>
|
||||
// {
|
||||
// getStats.call(this);
|
||||
// }, STATS_INTERVAL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_stopStats()
|
||||
{
|
||||
logger.debug('_stopStats()');
|
||||
|
||||
this.setState({ stats: null });
|
||||
|
||||
clearTimeout(this._statsTimer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Room.propTypes =
|
||||
{
|
||||
peerId : PropTypes.string.isRequired,
|
||||
roomId : PropTypes.string.isRequired,
|
||||
onNotify : PropTypes.func.isRequired,
|
||||
onHideNotification : PropTypes.func.isRequired
|
||||
room : appPropTypes.Room.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
amActiveSpeaker : PropTypes.bool.isRequired,
|
||||
onRoomLinkCopy : PropTypes.func.isRequired,
|
||||
onSetAudioMode : PropTypes.func.isRequired,
|
||||
onRestartIce : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
room : state.room,
|
||||
me : state.me,
|
||||
amActiveSpeaker : state.me.name === state.room.activeSpeakerName
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onRoomLinkCopy : () =>
|
||||
{
|
||||
dispatch(requestActions.notify(
|
||||
{
|
||||
text : 'Room link copied to the clipboard'
|
||||
}));
|
||||
},
|
||||
onSetAudioMode : (enable) =>
|
||||
{
|
||||
if (enable)
|
||||
dispatch(requestActions.enableAudioOnly());
|
||||
else
|
||||
dispatch(requestActions.disableAudioOnly());
|
||||
},
|
||||
onRestartIce : () =>
|
||||
{
|
||||
dispatch(requestActions.restartIce());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const RoomContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Room);
|
||||
|
||||
export default RoomContainer;
|
||||
|
|
|
|||
|
|
@ -1,585 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import browser from 'bowser';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = new Logger('Stats'); // eslint-disable-line no-unused-vars
|
||||
|
||||
// TODO: TMP
|
||||
global.BROWSER = browser;
|
||||
|
||||
export default class Stats extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport : null,
|
||||
audio : null,
|
||||
video : null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount()
|
||||
{
|
||||
let stats = this.props.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stats = nextProps.stats;
|
||||
|
||||
this._processStats(stats);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Stats'>
|
||||
<div
|
||||
className='close'
|
||||
onClick={this.handleCloseClick.bind(this)}
|
||||
/>
|
||||
{
|
||||
Object.keys(state.stats).map((blockName) =>
|
||||
{
|
||||
let block = state.stats[blockName];
|
||||
|
||||
if (!block)
|
||||
return;
|
||||
|
||||
let items = Object.keys(block).map((itemName) =>
|
||||
{
|
||||
let value = block[itemName];
|
||||
|
||||
if (value === undefined)
|
||||
return;
|
||||
|
||||
return (
|
||||
<div key={itemName} className='item'>
|
||||
<div className='key'>{itemName}</div>
|
||||
<div className='value'>{value}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (!items.length)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div key={blockName} className='block'>
|
||||
<h1>{blockName}</h1>
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleCloseClick()
|
||||
{
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
_processStats(stats)
|
||||
{
|
||||
// global.STATS = stats; // TODO: REMOVE
|
||||
|
||||
if (browser.check({ chrome: '58' }, true))
|
||||
{
|
||||
this._processStatsChrome58(stats);
|
||||
}
|
||||
else if (browser.check({ chrome: '40' }, true))
|
||||
{
|
||||
this._processStatsChromeOld(stats);
|
||||
}
|
||||
else if (browser.check({ firefox: '40' }, true))
|
||||
{
|
||||
this._processStatsFirefox(stats);
|
||||
}
|
||||
else if (browser.check({ safari: '11' }, true))
|
||||
{
|
||||
this._processStatsSafari11(stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn('_processStats() | unsupported browser [name:"%s", version:%s]',
|
||||
browser.name, browser.version);
|
||||
}
|
||||
}
|
||||
|
||||
_processStatsChrome58(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'transport':
|
||||
{
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.writable)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.availableOutgoingBitrate / 1000) + ' kbps';
|
||||
transport['current RTT'] =
|
||||
Math.round(group.currentRoundTripTime * 1000) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'codec':
|
||||
{
|
||||
let mimeType = group.mimeType.split('/');
|
||||
let kind = mimeType[0];
|
||||
let codec = mimeType[1];
|
||||
let block;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
if (codec === 'rtx')
|
||||
break;
|
||||
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = codec;
|
||||
block['payload type'] = group.payloadType;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'track':
|
||||
{
|
||||
if (group.kind !== 'video')
|
||||
break;
|
||||
|
||||
video['frame size'] = group.frameWidth + ' x ' + group.frameHeight;
|
||||
video['frames sent'] = group.framesSent;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.protocol;
|
||||
transport['local IP'] = localCandidate.ip;
|
||||
transport['local port'] = localCandidate.port;
|
||||
transport['remote IP'] = remoteCandidate.ip;
|
||||
transport['remote port'] = remoteCandidate.port;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsChromeOld(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'googCandidatePair':
|
||||
{
|
||||
if (group.googActiveConnection !== 'true')
|
||||
break;
|
||||
|
||||
let localAddress = group.googLocalAddress.split(':');
|
||||
let remoteAddress = group.googRemoteAddress.split(':');
|
||||
let localIP = localAddress[0];
|
||||
let localPort = localAddress[1];
|
||||
let remoteIP = remoteAddress[0];
|
||||
let remotePort = remoteAddress[1];
|
||||
|
||||
transport['protocol'] = group.googTransportType;
|
||||
transport['local IP'] = localIP;
|
||||
transport['local port'] = localPort;
|
||||
transport['remote IP'] = remoteIP;
|
||||
transport['remote port'] = remotePort;
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
transport['RTT'] = Math.round(group.googRtt) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'VideoBwe':
|
||||
{
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.googAvailableSendBandwidth / 1000) + ' kbps';
|
||||
transport['transmit bitrate'] =
|
||||
Math.round(group.googTransmitBitrate / 1000) + ' kbps';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ssrc':
|
||||
{
|
||||
if (group.packetsSent === undefined)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['codec'] = group.googCodecName;
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
block['packets lost'] = group.packetsLost;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame size'] =
|
||||
group.googFrameWidthSent + ' x ' + group.googFrameHeightSent;
|
||||
video['frame rate'] = group.googFrameRateSent;
|
||||
}
|
||||
|
||||
block['NACK count'] = group.googNacksReceived;
|
||||
block['PLI count'] = group.googPlisReceived;
|
||||
block['FIR count'] = group.googFirsReceived;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsFirefox(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
let selectedCandidatePair = null;
|
||||
let localCandidates = {};
|
||||
let remoteCandidates = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.selected)
|
||||
break;
|
||||
|
||||
selectedCandidatePair = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'local-candidate':
|
||||
{
|
||||
localCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remote-candidate':
|
||||
{
|
||||
remoteCandidates[group.id] = group;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
{
|
||||
block['bitrate'] =
|
||||
Math.round(group.bitrateMean / 1000) + ' kbps';
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
video['frame rate'] = Math.round(group.framerateMean);
|
||||
}
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
if (selectedCandidatePair)
|
||||
{
|
||||
let localCandidate = localCandidates[selectedCandidatePair.localCandidateId];
|
||||
let remoteCandidate = remoteCandidates[selectedCandidatePair.remoteCandidateId];
|
||||
|
||||
transport['protocol'] = localCandidate.transport;
|
||||
transport['local IP'] = localCandidate.ipAddress;
|
||||
transport['local port'] = localCandidate.portNumber;
|
||||
transport['remote IP'] = remoteCandidate.ipAddress;
|
||||
transport['remote port'] = remoteCandidate.portNumber;
|
||||
}
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_processStatsSafari11(stats)
|
||||
{
|
||||
let transport = {};
|
||||
let audio = {};
|
||||
let video = {};
|
||||
|
||||
for (let group of stats.values())
|
||||
{
|
||||
switch (group.type)
|
||||
{
|
||||
case 'candidate-pair':
|
||||
{
|
||||
if (!group.writable)
|
||||
break;
|
||||
|
||||
transport['bytes sent'] = group.bytesSent;
|
||||
transport['bytes received'] = group.bytesReceived;
|
||||
transport['available bitrate'] =
|
||||
Math.round(group.availableOutgoingBitrate / 1000) + ' kbps';
|
||||
transport['current RTT'] =
|
||||
Math.round(group.currentRoundTripTime * 1000) + ' ms';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'outbound-rtp':
|
||||
{
|
||||
if (group.isRemote)
|
||||
break;
|
||||
|
||||
let block;
|
||||
|
||||
switch (group.mediaType)
|
||||
{
|
||||
case 'audio':
|
||||
block = audio;
|
||||
break;
|
||||
case 'video':
|
||||
block = video;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!block)
|
||||
break;
|
||||
|
||||
block['ssrc'] = group.ssrc;
|
||||
block['bytes sent'] = group.bytesSent;
|
||||
block['packets sent'] = group.packetsSent;
|
||||
|
||||
if (block === video)
|
||||
block['frames encoded'] = group.framesEncoded;
|
||||
|
||||
block['NACK count'] = group.nackCount;
|
||||
block['PLI count'] = group.pliCount;
|
||||
block['FIR count'] = group.firCount;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post checks.
|
||||
|
||||
if (!video.ssrc)
|
||||
video = {};
|
||||
|
||||
if (!audio.ssrc)
|
||||
audio = {};
|
||||
|
||||
// Set state.
|
||||
this.setState(
|
||||
{
|
||||
stats :
|
||||
{
|
||||
transport,
|
||||
audio,
|
||||
video
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Stats.propTypes =
|
||||
{
|
||||
stats : PropTypes.object.isRequired,
|
||||
onClose : PropTypes.func.isRequired
|
||||
};
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import TransitionGroup from 'react-transition-group/TransitionGroup';
|
||||
|
||||
const DEFAULT_DURATION = 1000;
|
||||
|
||||
export default class TransitionAppear extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let duration = props.hasOwnProperty('duration') ? props.duration : DEFAULT_DURATION;
|
||||
|
||||
return (
|
||||
<TransitionGroup
|
||||
component={FakeTransitionWrapper}
|
||||
transitionName='transition'
|
||||
transitionAppear={!!duration}
|
||||
transitionAppearTimeout={duration}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
{this.props.children}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TransitionAppear.propTypes =
|
||||
{
|
||||
children : PropTypes.any,
|
||||
duration : PropTypes.number
|
||||
};
|
||||
|
||||
class FakeTransitionWrapper extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let children = React.Children.toArray(this.props.children);
|
||||
|
||||
return children[0] || null;
|
||||
}
|
||||
}
|
||||
|
||||
FakeTransitionWrapper.propTypes =
|
||||
{
|
||||
children : PropTypes.any
|
||||
};
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Logger from '../Logger';
|
||||
import classnames from 'classnames';
|
||||
import hark from 'hark';
|
||||
|
||||
const logger = new Logger('Video'); // eslint-disable-line no-unused-vars
|
||||
|
||||
export default class Video extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
width : 0,
|
||||
height : 0,
|
||||
resolution : null,
|
||||
volume : 0 // Integer from 0 to 10.
|
||||
};
|
||||
|
||||
let stream = props.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// Current MediaStreamTracks info.
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
// Periodic timer to show video dimensions.
|
||||
this._videoResolutionTimer = null;
|
||||
|
||||
// Hark instance.
|
||||
this._hark = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let props = this.props;
|
||||
let state = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='Video'>
|
||||
{state.width ?
|
||||
(
|
||||
<div
|
||||
className={classnames('resolution', { clickable: !!props.resolution })}
|
||||
onClick={this.handleResolutionClick.bind(this)}
|
||||
>
|
||||
<p>{state.width}x{state.height}</p>
|
||||
{props.resolution ?
|
||||
<p>{props.resolution}</p>
|
||||
:null}
|
||||
</div>
|
||||
)
|
||||
:null}
|
||||
<div className='volume'>
|
||||
<div className={classnames('bar', `level${state.volume}`)}/>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames(
|
||||
{
|
||||
mirror : props.mirror,
|
||||
hidden : props.videoDisabled
|
||||
})}
|
||||
autoPlay
|
||||
muted={props.muted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
let stream = this.props.stream;
|
||||
let video = this.refs.video;
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
this._showVideoResolution();
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
this._showVideoResolution();
|
||||
}, 500);
|
||||
|
||||
if (stream.getAudioTracks().length > 0)
|
||||
{
|
||||
this._hark = hark(stream);
|
||||
|
||||
this._hark.on('speaking', () =>
|
||||
{
|
||||
// logger.debug('hark "speaking" event');
|
||||
});
|
||||
|
||||
this._hark.on('stopped_speaking', () =>
|
||||
{
|
||||
// logger.debug('hark "stopped_speaking" event');
|
||||
|
||||
this.setState({ volume: 0 });
|
||||
});
|
||||
|
||||
this._hark.on('volume_change', (volume, threshold) =>
|
||||
{
|
||||
if (volume < threshold)
|
||||
return;
|
||||
|
||||
// logger.debug('hark "volume_change" event [volume:%sdB, threshold:%sdB]', volume, threshold);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
volume : Math.round((volume - threshold) * (-10) / threshold)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
|
||||
if (this._hark)
|
||||
this._hark.stop();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
let stream = nextProps.stream;
|
||||
|
||||
// Clean stream.
|
||||
// Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
this._cleanStream(stream);
|
||||
|
||||
// If there is something different in the stream, re-render it.
|
||||
|
||||
let previousTracksHash = this._tracksHash;
|
||||
|
||||
this._tracksHash = this._getTracksHash(stream);
|
||||
|
||||
if (this._tracksHash !== previousTracksHash)
|
||||
this.refs.video.srcObject = stream;
|
||||
}
|
||||
|
||||
handleResolutionClick()
|
||||
{
|
||||
if (!this.props.resolution)
|
||||
return;
|
||||
|
||||
logger.debug('handleResolutionClick()');
|
||||
|
||||
this.props.onResolutionChange();
|
||||
}
|
||||
|
||||
_getTracksHash(stream)
|
||||
{
|
||||
return stream.getTracks()
|
||||
.map((track) =>
|
||||
{
|
||||
let trackId = track.jitsiRemoteId || track.id;
|
||||
|
||||
return trackId;
|
||||
})
|
||||
.join('|');
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
let video = this.refs.video;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
width : video.videoWidth,
|
||||
height : video.videoHeight
|
||||
});
|
||||
}
|
||||
|
||||
_cleanStream(stream)
|
||||
{
|
||||
// Hack for Firefox bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1347578
|
||||
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
let previousNumTracks = tracks.length;
|
||||
|
||||
// Remove ended tracks.
|
||||
for (let track of tracks)
|
||||
{
|
||||
if (track.readyState === 'ended')
|
||||
{
|
||||
logger.warn('_cleanStream() | removing ended track [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are multiple live audio tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getAudioTracks().length > 1)
|
||||
{
|
||||
let track = stream.getAudioTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live audio track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
// If there are multiple live video tracks (related to the bug?) just keep
|
||||
// the last one.
|
||||
while (stream.getVideoTracks().length > 1)
|
||||
{
|
||||
let track = stream.getVideoTracks()[0];
|
||||
|
||||
logger.warn('_cleanStream() | removing live video track due the presence of others [track:%o]', track);
|
||||
|
||||
stream.removeTrack(track);
|
||||
}
|
||||
|
||||
let numTracks = stream.getTracks().length;
|
||||
|
||||
if (numTracks !== previousNumTracks)
|
||||
logger.warn('_cleanStream() | num tracks changed from %s to %s', previousNumTracks, numTracks);
|
||||
}
|
||||
}
|
||||
|
||||
Video.propTypes =
|
||||
{
|
||||
stream : PropTypes.object.isRequired,
|
||||
resolution : PropTypes.string,
|
||||
muted : PropTypes.bool,
|
||||
videoDisabled : PropTypes.bool,
|
||||
mirror : PropTypes.bool,
|
||||
onResolutionChange : PropTypes.func
|
||||
};
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Room = PropTypes.shape(
|
||||
{
|
||||
url : PropTypes.string.isRequired,
|
||||
state : PropTypes.oneOf(
|
||||
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
|
||||
activeSpeakerName : PropTypes.string
|
||||
});
|
||||
|
||||
export const Device = PropTypes.shape(
|
||||
{
|
||||
flag : PropTypes.string.isRequired,
|
||||
name : PropTypes.string.isRequired,
|
||||
version : PropTypes.string
|
||||
});
|
||||
|
||||
export const Me = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
displayNameSet : PropTypes.bool.isRequired,
|
||||
device : Device.isRequired,
|
||||
canSendMic : PropTypes.bool.isRequired,
|
||||
canSendWebcam : PropTypes.bool.isRequired,
|
||||
canChangeWebcam : PropTypes.bool.isRequired,
|
||||
webcamInProgress : PropTypes.bool.isRequired,
|
||||
audioOnly : PropTypes.bool.isRequired,
|
||||
audioOnlyInProgress : PropTypes.bool.isRequired,
|
||||
restartIceInProgress : PropTypes.bool.isRequired
|
||||
});
|
||||
|
||||
export const Producer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam' ]).isRequired,
|
||||
deviceLabel : PropTypes.string,
|
||||
type : PropTypes.oneOf([ 'front', 'back' ]),
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
export const Peer = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
device : Device.isRequired,
|
||||
consumers : PropTypes.arrayOf(PropTypes.number).isRequired
|
||||
});
|
||||
|
||||
export const Consumer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
peerName : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam' ]).isRequired,
|
||||
supported : PropTypes.bool.isRequired,
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
profile : PropTypes.oneOf([ 'none', 'low', 'medium', 'high' ]),
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string
|
||||
});
|
||||
|
||||
export const Notification = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.string.isRequired,
|
||||
type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
|
||||
timeout : PropTypes.number
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import getMuiTheme from 'material-ui/styles/getMuiTheme';
|
||||
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
|
||||
import { grey500 } from 'material-ui/styles/colors';
|
||||
|
||||
// NOTE: I should clone it
|
||||
let theme = lightBaseTheme;
|
||||
|
||||
theme.palette.borderColor = grey500;
|
||||
|
||||
let muiTheme = getMuiTheme(lightBaseTheme);
|
||||
|
||||
export default muiTheme;
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
const Appear = ({ duration, children }) => (
|
||||
<CSSTransition
|
||||
in
|
||||
classNames='Appear'
|
||||
timeout={duration || 1000}
|
||||
appear
|
||||
>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
Appear.propTypes =
|
||||
{
|
||||
duration : PropTypes.number,
|
||||
children : PropTypes.any
|
||||
};
|
||||
|
||||
export { Appear };
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import jsCookie from 'js-cookie';
|
||||
|
||||
const USER_COOKIE = 'mediasoup-demo.user';
|
||||
const DEVICES_COOKIE = 'mediasoup-demo.devices';
|
||||
|
||||
export function getUser()
|
||||
{
|
||||
return jsCookie.getJSON(USER_COOKIE);
|
||||
}
|
||||
|
||||
export function setUser({ displayName })
|
||||
{
|
||||
jsCookie.set(USER_COOKIE, { displayName });
|
||||
}
|
||||
|
||||
export function getDevices()
|
||||
{
|
||||
return jsCookie.getJSON(DEVICES_COOKIE);
|
||||
}
|
||||
|
||||
export function setDevices({ webcamEnabled })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import sdpTransform from 'sdp-transform';
|
||||
|
||||
/**
|
||||
* RTCSessionDescription implementation.
|
||||
*/
|
||||
export default class RTCSessionDescription {
|
||||
/**
|
||||
* RTCSessionDescription constructor.
|
||||
* @param {Object} [data]
|
||||
* @param {String} [data.type] - 'offer' / 'answer'.
|
||||
* @param {String} [data.sdp] - SDP string.
|
||||
* @param {Object} [data._sdpObject] - SDP object generated by the
|
||||
* sdp-transform library.
|
||||
*/
|
||||
constructor(data) {
|
||||
// @type {String}
|
||||
this._sdp = null;
|
||||
|
||||
// @type {Object}
|
||||
this._sdpObject = null;
|
||||
|
||||
// @type {String}
|
||||
this._type = null;
|
||||
|
||||
switch (data.type) {
|
||||
case 'offer':
|
||||
break;
|
||||
case 'answer':
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`invalid type "${data.type}"`);
|
||||
}
|
||||
|
||||
this._type = data.type;
|
||||
|
||||
if (typeof data.sdp === 'string') {
|
||||
this._sdp = data.sdp;
|
||||
try {
|
||||
this._sdpObject = sdpTransform.parse(data.sdp);
|
||||
} catch (error) {
|
||||
throw new Error(`invalid sdp: ${error}`);
|
||||
}
|
||||
} else if (typeof data._sdpObject === 'object') {
|
||||
this._sdpObject = data._sdpObject;
|
||||
try {
|
||||
this._sdp = sdpTransform.write(data._sdpObject);
|
||||
} catch (error) {
|
||||
throw new Error(`invalid sdp object: ${error}`);
|
||||
}
|
||||
} else {
|
||||
throw new TypeError('invalid sdp or _sdpObject');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sdp field.
|
||||
* @return {String}
|
||||
*/
|
||||
get sdp() {
|
||||
return this._sdp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sdp field.
|
||||
* NOTE: This is not allowed per spec, but lib-jitsi-meet uses it.
|
||||
* @param {String} sdp
|
||||
*/
|
||||
set sdp(sdp) {
|
||||
try {
|
||||
this._sdpObject = sdpTransform.parse(sdp);
|
||||
} catch (error) {
|
||||
throw new Error(`invalid sdp: ${error}`);
|
||||
}
|
||||
|
||||
this._sdp = sdp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal sdp object.
|
||||
* @return {Object}
|
||||
* @private
|
||||
*/
|
||||
get sdpObject() {
|
||||
return this._sdpObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type field.
|
||||
* @return {String}
|
||||
*/
|
||||
get type() {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with type and sdp fields.
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
sdp: this._sdp,
|
||||
type: this._type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* Create a class inheriting from Error.
|
||||
*/
|
||||
function createErrorClass(name) {
|
||||
const klass = class extends Error {
|
||||
/**
|
||||
* Custom error class constructor.
|
||||
* @param {string} message
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
|
||||
// Override `name` property value and make it non enumerable.
|
||||
Object.defineProperty(this, 'name', { value: name });
|
||||
}
|
||||
};
|
||||
|
||||
return klass;
|
||||
}
|
||||
|
||||
export const InvalidStateError = createErrorClass('InvalidStateError');
|
||||
|
|
@ -1,458 +0,0 @@
|
|||
/* global RTCRtpReceiver */
|
||||
|
||||
import sdpTransform from 'sdp-transform';
|
||||
|
||||
/**
|
||||
* Extract RTP capabilities from remote description.
|
||||
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
|
||||
* @return {RTCRtpCapabilities}
|
||||
*/
|
||||
export function extractCapabilities(sdpObject) {
|
||||
// Map of RtpCodecParameters indexed by payload type.
|
||||
const codecsMap = new Map();
|
||||
|
||||
// Array of RtpHeaderExtensions.
|
||||
const headerExtensions = [];
|
||||
|
||||
for (const m of sdpObject.media) {
|
||||
// Media kind.
|
||||
const kind = m.type;
|
||||
|
||||
if (kind !== 'audio' && kind !== 'video') {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
// Get codecs.
|
||||
for (const rtp of m.rtp) {
|
||||
const codec = {
|
||||
clockRate: rtp.rate,
|
||||
kind,
|
||||
mimeType: `${kind}/${rtp.codec}`,
|
||||
name: rtp.codec,
|
||||
numChannels: rtp.encoding || 1,
|
||||
parameters: {},
|
||||
preferredPayloadType: rtp.payload,
|
||||
rtcpFeedback: []
|
||||
};
|
||||
|
||||
codecsMap.set(codec.preferredPayloadType, codec);
|
||||
}
|
||||
|
||||
// Get codec parameters.
|
||||
for (const fmtp of m.fmtp || []) {
|
||||
const parameters = sdpTransform.parseFmtpConfig(fmtp.config);
|
||||
const codec = codecsMap.get(fmtp.payload);
|
||||
|
||||
if (!codec) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
codec.parameters = parameters;
|
||||
}
|
||||
|
||||
// Get RTCP feedback for each codec.
|
||||
for (const fb of m.rtcpFb || []) {
|
||||
const codec = codecsMap.get(fb.payload);
|
||||
|
||||
if (!codec) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
codec.rtcpFeedback.push({
|
||||
parameter: fb.subtype || '',
|
||||
type: fb.type
|
||||
});
|
||||
}
|
||||
|
||||
// Get RTP header extensions.
|
||||
for (const ext of m.ext || []) {
|
||||
const preferredId = ext.value;
|
||||
const uri = ext.uri;
|
||||
const headerExtension = {
|
||||
kind,
|
||||
uri,
|
||||
preferredId
|
||||
};
|
||||
|
||||
// Check if already present.
|
||||
const duplicated = headerExtensions.find(savedHeaderExtension =>
|
||||
headerExtension.kind === savedHeaderExtension.kind
|
||||
&& headerExtension.uri === savedHeaderExtension.uri
|
||||
);
|
||||
|
||||
if (!duplicated) {
|
||||
headerExtensions.push(headerExtension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
codecs: Array.from(codecsMap.values()),
|
||||
fecMechanisms: [], // TODO
|
||||
headerExtensions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract DTLS parameters from remote description.
|
||||
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
|
||||
* @return {RTCDtlsParameters}
|
||||
*/
|
||||
export function extractDtlsParameters(sdpObject) {
|
||||
const media = getFirstActiveMediaSection(sdpObject);
|
||||
const fingerprint = media.fingerprint || sdpObject.fingerprint;
|
||||
let role;
|
||||
|
||||
switch (media.setup) {
|
||||
case 'active':
|
||||
role = 'client';
|
||||
break;
|
||||
case 'passive':
|
||||
role = 'server';
|
||||
break;
|
||||
case 'actpass':
|
||||
role = 'auto';
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
role,
|
||||
fingerprints: [
|
||||
{
|
||||
algorithm: fingerprint.type,
|
||||
value: fingerprint.hash
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ICE candidates from remote description.
|
||||
* NOTE: This implementation assumes a single BUNDLEd transport and rtcp-mux.
|
||||
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
|
||||
* @return {sequence<RTCIceCandidate>}
|
||||
*/
|
||||
export function extractIceCandidates(sdpObject) {
|
||||
const media = getFirstActiveMediaSection(sdpObject);
|
||||
const candidates = [];
|
||||
|
||||
for (const c of media.candidates) {
|
||||
// Ignore RTCP candidates (we assume rtcp-mux).
|
||||
if (c.component !== 1) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
const candidate = {
|
||||
foundation: c.foundation,
|
||||
ip: c.ip,
|
||||
port: c.port,
|
||||
priority: c.priority,
|
||||
protocol: c.transport.toLowerCase(),
|
||||
type: c.type
|
||||
};
|
||||
|
||||
candidates.push(candidate);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ICE parameters from remote description.
|
||||
* NOTE: This implementation assumes a single BUNDLEd transport.
|
||||
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
|
||||
* @return {RTCIceParameters}
|
||||
*/
|
||||
export function extractIceParameters(sdpObject) {
|
||||
const media = getFirstActiveMediaSection(sdpObject);
|
||||
const usernameFragment = media.iceUfrag;
|
||||
const password = media.icePwd;
|
||||
const icelite = sdpObject.icelite === 'ice-lite';
|
||||
|
||||
return {
|
||||
icelite,
|
||||
password,
|
||||
usernameFragment
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract MID values from remote description.
|
||||
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
|
||||
* @return {map<String, String>} Ordered Map with MID as key and kind as value.
|
||||
*/
|
||||
export function extractMids(sdpObject) {
|
||||
const midToKind = new Map();
|
||||
|
||||
// Ignore disabled media sections.
|
||||
for (const m of sdpObject.media) {
|
||||
midToKind.set(m.mid, m.type);
|
||||
}
|
||||
|
||||
return midToKind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tracks information.
|
||||
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
|
||||
* @return {Map}
|
||||
*/
|
||||
export function extractTrackInfos(sdpObject) {
|
||||
// Map with info about receiving media.
|
||||
// - index: Media SSRC
|
||||
// - value: Object
|
||||
// - kind: 'audio' / 'video'
|
||||
// - ssrc: Media SSRC
|
||||
// - rtxSsrc: RTX SSRC (may be unset)
|
||||
// - streamId: MediaStream.jitsiRemoteId
|
||||
// - trackId: MediaStreamTrack.jitsiRemoteId
|
||||
// - cname: CNAME
|
||||
// @type {map<Number, Object>}
|
||||
const infos = new Map();
|
||||
|
||||
// Map with stream SSRC as index and associated RTX SSRC as value.
|
||||
// @type {map<Number, Number>}
|
||||
const rtxMap = new Map();
|
||||
|
||||
// Set of RTX SSRC values.
|
||||
const rtxSet = new Set();
|
||||
|
||||
for (const m of sdpObject.media) {
|
||||
const kind = m.type;
|
||||
|
||||
if (kind !== 'audio' && kind !== 'video') {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
// Get RTX information.
|
||||
for (const ssrcGroup of m.ssrcGroups || []) {
|
||||
// Just consider FID.
|
||||
if (ssrcGroup.semantics !== 'FID') {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
const ssrcs
|
||||
= ssrcGroup.ssrcs.split(' ').map(ssrc => Number(ssrc));
|
||||
const ssrc = ssrcs[0];
|
||||
const rtxSsrc = ssrcs[1];
|
||||
|
||||
rtxMap.set(ssrc, rtxSsrc);
|
||||
rtxSet.add(rtxSsrc);
|
||||
}
|
||||
|
||||
for (const ssrcObject of m.ssrcs || []) {
|
||||
const ssrc = ssrcObject.id;
|
||||
|
||||
// Ignore RTX.
|
||||
if (rtxSet.has(ssrc)) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
let info = infos.get(ssrc);
|
||||
|
||||
if (!info) {
|
||||
info = {
|
||||
kind,
|
||||
rtxSsrc: rtxMap.get(ssrc),
|
||||
ssrc
|
||||
};
|
||||
|
||||
infos.set(ssrc, info);
|
||||
}
|
||||
|
||||
switch (ssrcObject.attribute) {
|
||||
case 'cname': {
|
||||
info.cname = ssrcObject.value;
|
||||
break;
|
||||
}
|
||||
case 'msid': {
|
||||
const values = ssrcObject.value.split(' ');
|
||||
const streamId = values[0];
|
||||
const trackId = values[1];
|
||||
|
||||
info.streamId = streamId;
|
||||
info.trackId = trackId;
|
||||
break;
|
||||
}
|
||||
case 'mslabel': {
|
||||
const streamId = ssrcObject.value;
|
||||
|
||||
info.streamId = streamId;
|
||||
break;
|
||||
}
|
||||
case 'label': {
|
||||
const trackId = ssrcObject.value;
|
||||
|
||||
info.trackId = trackId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local ORTC RTP capabilities filtered and adapted to the given remote RTP
|
||||
* capabilities.
|
||||
* @param {RTCRtpCapabilities} filterWithCapabilities - RTP capabilities to
|
||||
* filter with.
|
||||
* @return {RTCRtpCapabilities}
|
||||
*/
|
||||
export function getLocalCapabilities(filterWithCapabilities) {
|
||||
const localFullCapabilities = RTCRtpReceiver.getCapabilities();
|
||||
const localCapabilities = {
|
||||
codecs: [],
|
||||
fecMechanisms: [],
|
||||
headerExtensions: []
|
||||
};
|
||||
|
||||
// Map of RTX and codec payloads.
|
||||
// - index: Codec payloadType
|
||||
// - value: Associated RTX payloadType
|
||||
// @type {map<Number, Number>}
|
||||
const remoteRtxMap = new Map();
|
||||
|
||||
// Set codecs.
|
||||
for (const remoteCodec of filterWithCapabilities.codecs) {
|
||||
const remoteCodecName = remoteCodec.name.toLowerCase();
|
||||
|
||||
if (remoteCodecName === 'rtx') {
|
||||
remoteRtxMap.set(
|
||||
remoteCodec.parameters.apt, remoteCodec.preferredPayloadType);
|
||||
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
const localCodec = localFullCapabilities.codecs.find(codec =>
|
||||
codec.name.toLowerCase() === remoteCodecName
|
||||
&& codec.kind === remoteCodec.kind
|
||||
&& codec.clockRate === remoteCodec.clockRate
|
||||
);
|
||||
|
||||
if (!localCodec) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
const codec = {
|
||||
clockRate: localCodec.clockRate,
|
||||
kind: localCodec.kind,
|
||||
mimeType: `${localCodec.kind}/${localCodec.name}`,
|
||||
name: localCodec.name,
|
||||
numChannels: localCodec.numChannels || 1,
|
||||
parameters: {},
|
||||
preferredPayloadType: remoteCodec.preferredPayloadType,
|
||||
rtcpFeedback: []
|
||||
};
|
||||
|
||||
for (const remoteParamName of Object.keys(remoteCodec.parameters)) {
|
||||
const remoteParamValue
|
||||
= remoteCodec.parameters[remoteParamName];
|
||||
|
||||
for (const localParamName of Object.keys(localCodec.parameters)) {
|
||||
const localParamValue
|
||||
= localCodec.parameters[localParamName];
|
||||
|
||||
if (localParamName !== remoteParamName) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
// TODO: We should consider much more cases here, but Edge
|
||||
// does not support many codec parameters.
|
||||
if (localParamValue === remoteParamValue) {
|
||||
// Use this RTP parameter.
|
||||
codec.parameters[localParamName] = localParamValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const remoteFb of remoteCodec.rtcpFeedback) {
|
||||
const localFb = localCodec.rtcpFeedback.find(fb =>
|
||||
fb.type === remoteFb.type
|
||||
&& fb.parameter === remoteFb.parameter
|
||||
);
|
||||
|
||||
if (localFb) {
|
||||
// Use this RTCP feedback.
|
||||
codec.rtcpFeedback.push(localFb);
|
||||
}
|
||||
}
|
||||
|
||||
// Use this codec.
|
||||
localCapabilities.codecs.push(codec);
|
||||
}
|
||||
|
||||
// Add RTX for video codecs.
|
||||
for (const codec of localCapabilities.codecs) {
|
||||
const payloadType = codec.preferredPayloadType;
|
||||
|
||||
if (!remoteRtxMap.has(payloadType)) {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
|
||||
const rtxCodec = {
|
||||
clockRate: codec.clockRate,
|
||||
kind: codec.kind,
|
||||
mimeType: `${codec.kind}/rtx`,
|
||||
name: 'rtx',
|
||||
parameters: {
|
||||
apt: payloadType
|
||||
},
|
||||
preferredPayloadType: remoteRtxMap.get(payloadType),
|
||||
rtcpFeedback: []
|
||||
};
|
||||
|
||||
// Add RTX codec.
|
||||
localCapabilities.codecs.push(rtxCodec);
|
||||
}
|
||||
|
||||
// Add RTP header extensions.
|
||||
for (const remoteExtension of filterWithCapabilities.headerExtensions) {
|
||||
const localExtension
|
||||
= localFullCapabilities.headerExtensions.find(extension =>
|
||||
extension.kind === remoteExtension.kind
|
||||
&& extension.uri === remoteExtension.uri
|
||||
);
|
||||
|
||||
if (localExtension) {
|
||||
const extension = {
|
||||
kind: localExtension.kind,
|
||||
preferredEncrypt: Boolean(remoteExtension.preferredEncrypt),
|
||||
preferredId: remoteExtension.preferredId,
|
||||
uri: localExtension.uri
|
||||
};
|
||||
|
||||
// Use this RTP header extension.
|
||||
localCapabilities.headerExtensions.push(extension);
|
||||
}
|
||||
}
|
||||
|
||||
// Add FEC mechanisms.
|
||||
// NOTE: We don't support FEC yet and, in fact, neither does Edge.
|
||||
for (const remoteFecMechanism of filterWithCapabilities.fecMechanisms) {
|
||||
const localFecMechanism
|
||||
= localFullCapabilities.fecMechanisms.find(fec =>
|
||||
fec === remoteFecMechanism
|
||||
);
|
||||
|
||||
if (localFecMechanism) {
|
||||
// Use this FEC mechanism.
|
||||
localCapabilities.fecMechanisms.push(localFecMechanism);
|
||||
}
|
||||
}
|
||||
|
||||
return localCapabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first acive media section.
|
||||
* @param {Object} sdpObject - SDP object generated by sdp-transform.
|
||||
* @return {Object} SDP media section as parsed by sdp-transform.
|
||||
*/
|
||||
function getFirstActiveMediaSection(sdpObject) {
|
||||
return sdpObject.media.find(m =>
|
||||
m.iceUfrag && m.port !== 0
|
||||
);
|
||||
}
|
||||
|
|
@ -1,74 +1,183 @@
|
|||
'use strict';
|
||||
|
||||
import browser from 'bowser';
|
||||
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 { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import {
|
||||
applyMiddleware as applyReduxMiddleware,
|
||||
createStore as createReduxStore
|
||||
} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { createLogger as createReduxLogger } from 'redux-logger';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import randomString from 'random-string';
|
||||
import randomName from 'node-random-name';
|
||||
import Logger from './Logger';
|
||||
import * as utils from './utils';
|
||||
import edgeRTCPeerConnection from './edge/RTCPeerConnection';
|
||||
import edgeRTCSessionDescription from './edge/RTCSessionDescription';
|
||||
import App from './components/App';
|
||||
import * as cookiesManager from './cookiesManager';
|
||||
import * as requestActions from './redux/requestActions';
|
||||
import * as stateActions from './redux/stateActions';
|
||||
import reducers from './redux/reducers';
|
||||
import roomClientMiddleware from './redux/roomClientMiddleware';
|
||||
import Room from './components/Room';
|
||||
|
||||
const REGEXP_FRAGMENT_ROOM_ID = new RegExp('^#room-id=([0-9a-zA-Z_-]+)$');
|
||||
const logger = new Logger();
|
||||
const reduxMiddlewares =
|
||||
[
|
||||
thunk,
|
||||
roomClientMiddleware
|
||||
];
|
||||
|
||||
injectTapEventPlugin();
|
||||
|
||||
logger.debug('detected browser [name:"%s", version:%s]', browser.name, browser.version);
|
||||
|
||||
// If Edge, use the Jitsi RTCPeerConnection shim.
|
||||
if (browser.msedge)
|
||||
if (process.env.NODE_ENV === 'development')
|
||||
{
|
||||
logger.debug('Edge detected, overriding RTCPeerConnection and RTCSessionDescription');
|
||||
const reduxLogger = createReduxLogger(
|
||||
{
|
||||
duration : true,
|
||||
timestamp : false,
|
||||
level : 'log',
|
||||
logErrors : true
|
||||
});
|
||||
|
||||
window.RTCPeerConnection = edgeRTCPeerConnection;
|
||||
window.RTCSessionDescription = edgeRTCSessionDescription;
|
||||
}
|
||||
// Otherwise, do almost anything.
|
||||
else
|
||||
{
|
||||
window.RTCPeerConnection =
|
||||
window.webkitRTCPeerConnection ||
|
||||
window.mozRTCPeerConnection ||
|
||||
window.RTCPeerConnection;
|
||||
reduxMiddlewares.push(reduxLogger);
|
||||
}
|
||||
|
||||
const store = createReduxStore(
|
||||
reducers,
|
||||
undefined,
|
||||
applyReduxMiddleware(...reduxMiddlewares)
|
||||
);
|
||||
|
||||
domready(() =>
|
||||
{
|
||||
logger.debug('DOM ready');
|
||||
|
||||
// Load stuff and run
|
||||
utils.initialize()
|
||||
.then(run)
|
||||
.catch((error) =>
|
||||
{
|
||||
console.error(error);
|
||||
});
|
||||
.then(run);
|
||||
});
|
||||
|
||||
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;
|
||||
const peerName = randomString({ length: 8 }).toLowerCase();
|
||||
const urlParser = new UrlParse(window.location.href, true);
|
||||
let roomId = urlParser.query.roomId;
|
||||
const produce = urlParser.query.produce !== 'false';
|
||||
let displayName = urlParser.query.displayName;
|
||||
const isSipEndpoint = urlParser.query.sipEndpoint === 'true';
|
||||
const useSimulcast = urlParser.query.simulcast !== 'false';
|
||||
|
||||
if (match)
|
||||
if (!roomId)
|
||||
{
|
||||
roomId = match[1];
|
||||
roomId = randomString({ length: 8 }).toLowerCase();
|
||||
|
||||
urlParser.query.roomId = roomId;
|
||||
window.history.pushState('', '', urlParser.toString());
|
||||
}
|
||||
|
||||
// Get the effective/shareable Room URL.
|
||||
const roomUrlParser = new UrlParse(window.location.href, true);
|
||||
|
||||
for (const key of Object.keys(roomUrlParser.query))
|
||||
{
|
||||
// Don't keep some custom params.
|
||||
switch (key)
|
||||
{
|
||||
case 'roomId':
|
||||
case 'simulcast':
|
||||
break;
|
||||
default:
|
||||
delete roomUrlParser.query[key];
|
||||
}
|
||||
}
|
||||
delete roomUrlParser.hash;
|
||||
|
||||
const roomUrl = roomUrlParser.toString();
|
||||
|
||||
// Get displayName from cookie (if not already given as param).
|
||||
const userCookie = cookiesManager.getUser() || {};
|
||||
let displayNameSet;
|
||||
|
||||
if (!displayName)
|
||||
displayName = userCookie.displayName;
|
||||
|
||||
if (displayName)
|
||||
{
|
||||
displayNameSet = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
roomId = randomString({ length: 8 }).toLowerCase();
|
||||
window.location = `#room-id=${roomId}`;
|
||||
displayName = randomName();
|
||||
displayNameSet = false;
|
||||
}
|
||||
|
||||
ReactDOM.render(<App peerId={peerId} roomId={roomId}/>, container);
|
||||
// Get current device.
|
||||
const device = getDeviceInfo();
|
||||
|
||||
// If a SIP endpoint mangle device info.
|
||||
if (isSipEndpoint)
|
||||
{
|
||||
device.flag = 'sipendpoint';
|
||||
device.name = 'SIP Endpoint';
|
||||
device.version = undefined;
|
||||
}
|
||||
|
||||
// NOTE: I don't like this.
|
||||
store.dispatch(
|
||||
stateActions.setRoomUrl(roomUrl));
|
||||
|
||||
// NOTE: I don't like this.
|
||||
store.dispatch(
|
||||
stateActions.setMe({ peerName, displayName, displayNameSet, device }));
|
||||
|
||||
// NOTE: I don't like this.
|
||||
store.dispatch(
|
||||
requestActions.joinRoom(
|
||||
{ roomId, peerName, displayName, device, useSimulcast, produce }));
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Room />
|
||||
</Provider>,
|
||||
document.getElementById('mediasoup-demo-app-container')
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Debugging stuff.
|
||||
|
||||
setInterval(() =>
|
||||
{
|
||||
if (!global.CLIENT._room.peers[0])
|
||||
{
|
||||
delete global.CONSUMER;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const peer = global.CLIENT._room.peers[0];
|
||||
|
||||
global.CONSUMER = peer.consumers[peer.consumers.length - 1];
|
||||
}, 2000);
|
||||
|
||||
global.sendSdp = function()
|
||||
{
|
||||
logger.debug('---------- SEND_TRANSPORT LOCAL SDP OFFER:');
|
||||
logger.debug(
|
||||
global.CLIENT._sendTransport._handler._pc.localDescription.sdp);
|
||||
|
||||
logger.debug('---------- SEND_TRANSPORT REMOTE SDP ANSWER:');
|
||||
logger.debug(
|
||||
global.CLIENT._sendTransport._handler._pc.remoteDescription.sdp);
|
||||
};
|
||||
|
||||
global.recvSdp = function()
|
||||
{
|
||||
logger.debug('---------- RECV_TRANSPORT REMOTE SDP OFFER:');
|
||||
logger.debug(
|
||||
global.CLIENT._recvTransport._handler._pc.remoteDescription.sdp);
|
||||
|
||||
logger.debug('---------- RECV_TRANSPORT LOCAL SDP ANSWER:');
|
||||
logger.debug(
|
||||
global.CLIENT._recvTransport._handler._pc.localDescription.sdp);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
# APP STATE
|
||||
|
||||
```js
|
||||
{
|
||||
room :
|
||||
{
|
||||
url : 'https://example.io/?&roomId=d0el8y34',
|
||||
state : 'connected', // new/connecting/connected/closed
|
||||
activeSpeakerName : 'alice'
|
||||
},
|
||||
me :
|
||||
{
|
||||
name : 'bob',
|
||||
displayName : 'Bob McFLower',
|
||||
displayNameSet : false, // true if got from cookie or manually set.
|
||||
device : { flag: 'firefox', name: 'Firefox', version: '61' },
|
||||
canSendMic : true,
|
||||
canSendWebcam : true,
|
||||
canChangeWebcam : false,
|
||||
webcamInProgress : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
restartIceInProgress : false
|
||||
},
|
||||
producers :
|
||||
{
|
||||
1111 :
|
||||
{
|
||||
id : 1111,
|
||||
source : 'mic', // mic/webcam,
|
||||
locallyPaused : true,
|
||||
remotelyPaused : false,
|
||||
track : MediaStreamTrack,
|
||||
codec : 'opus'
|
||||
},
|
||||
1112 :
|
||||
{
|
||||
id : 1112,
|
||||
source : 'webcam', // mic/webcam
|
||||
deviceLabel : 'Macbook Webcam',
|
||||
type : 'front', // front/back
|
||||
locallyPaused : false,
|
||||
remotelyPaused : false,
|
||||
track : MediaStreamTrack,
|
||||
codec : 'vp8',
|
||||
}
|
||||
},
|
||||
peers :
|
||||
{
|
||||
'alice' :
|
||||
{
|
||||
name : 'alice',
|
||||
displayName : 'Alice Thomsom',
|
||||
device : { flag: 'chrome', name: 'Chrome', version: '58' },
|
||||
consumers : [ 5551, 5552 ]
|
||||
}
|
||||
},
|
||||
consumers :
|
||||
{
|
||||
5551 :
|
||||
{
|
||||
id : 5551,
|
||||
peerName : 'alice',
|
||||
source : 'mic', // mic/webcam
|
||||
supported : true,
|
||||
locallyPaused : false,
|
||||
remotelyPaused : false,
|
||||
profile : 'default',
|
||||
track : MediaStreamTrack,
|
||||
codec : 'opus'
|
||||
},
|
||||
5552 :
|
||||
{
|
||||
id : 5552,
|
||||
peerName : 'alice',
|
||||
source : 'webcam',
|
||||
supported : false,
|
||||
locallyPaused : false,
|
||||
remotelyPaused : true,
|
||||
profile : 'medium',
|
||||
track : null,
|
||||
codec : 'h264'
|
||||
}
|
||||
},
|
||||
notifications :
|
||||
[
|
||||
{
|
||||
id : 'qweasdw43we',
|
||||
type : 'info' // info/error
|
||||
text : 'You joined the room'
|
||||
},
|
||||
{
|
||||
id : 'j7sdhkjjkcc',
|
||||
type : 'error'
|
||||
text : 'Could not add webcam'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
const initialState = {};
|
||||
|
||||
const consumers = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const { consumer } = action.payload;
|
||||
|
||||
return { ...state, [consumer.id]: consumer };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const { consumerId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
delete newState[consumerId];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_PAUSED':
|
||||
{
|
||||
const { consumerId, originator } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
let newConsumer;
|
||||
|
||||
if (originator === 'local')
|
||||
newConsumer = { ...consumer, locallyPaused: true };
|
||||
else
|
||||
newConsumer = { ...consumer, remotelyPaused: true };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_RESUMED':
|
||||
{
|
||||
const { consumerId, originator } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
let newConsumer;
|
||||
|
||||
if (originator === 'local')
|
||||
newConsumer = { ...consumer, locallyPaused: false };
|
||||
else
|
||||
newConsumer = { ...consumer, remotelyPaused: false };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_EFFECTIVE_PROFILE':
|
||||
{
|
||||
const { consumerId, profile } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, profile };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_TRACK':
|
||||
{
|
||||
const { consumerId, track } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, track };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default consumers;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import room from './room';
|
||||
import me from './me';
|
||||
import producers from './producers';
|
||||
import peers from './peers';
|
||||
import consumers from './consumers';
|
||||
import notifications from './notifications';
|
||||
|
||||
const reducers = combineReducers(
|
||||
{
|
||||
room,
|
||||
me,
|
||||
producers,
|
||||
peers,
|
||||
consumers,
|
||||
notifications
|
||||
});
|
||||
|
||||
export default reducers;
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
const initialState =
|
||||
{
|
||||
name : null,
|
||||
displayName : null,
|
||||
displayNameSet : false,
|
||||
device : null,
|
||||
canSendMic : false,
|
||||
canSendWebcam : false,
|
||||
canChangeWebcam : false,
|
||||
webcamInProgress : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
restartIceInProgress : false
|
||||
};
|
||||
|
||||
const me = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'SET_ME':
|
||||
{
|
||||
const { peerName, displayName, displayNameSet, device } = action.payload;
|
||||
|
||||
return { ...state, name: peerName, displayName, displayNameSet, device };
|
||||
}
|
||||
|
||||
case 'SET_MEDIA_CAPABILITIES':
|
||||
{
|
||||
const { canSendMic, canSendWebcam } = action.payload;
|
||||
|
||||
return { ...state, canSendMic, canSendWebcam };
|
||||
}
|
||||
|
||||
case 'SET_CAN_CHANGE_WEBCAM':
|
||||
{
|
||||
const canChangeWebcam = action.payload;
|
||||
|
||||
return { ...state, canChangeWebcam };
|
||||
}
|
||||
|
||||
case 'SET_WEBCAM_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, webcamInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_DISPLAY_NAME':
|
||||
{
|
||||
let { displayName } = action.payload;
|
||||
|
||||
// Be ready for undefined displayName (so keep previous one).
|
||||
if (!displayName)
|
||||
displayName = state.displayName;
|
||||
|
||||
return { ...state, displayName, displayNameSet: true };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_ONLY_STATE':
|
||||
{
|
||||
const { enabled } = action.payload;
|
||||
|
||||
return { ...state, audioOnly: enabled };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_ONLY_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, audioOnlyInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_RESTART_ICE_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, restartIceInProgress: flag };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default me;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
const initialState = [];
|
||||
|
||||
const notifications = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_NOTIFICATION':
|
||||
{
|
||||
const { notification } = action.payload;
|
||||
|
||||
return [ ...state, notification ];
|
||||
}
|
||||
|
||||
case 'REMOVE_NOTIFICATION':
|
||||
{
|
||||
const { notificationId } = action.payload;
|
||||
|
||||
return state.filter((notification) => notification.id !== notificationId);
|
||||
}
|
||||
|
||||
case 'REMOVE_ALL_NOTIFICATIONS':
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default notifications;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
const initialState = {};
|
||||
|
||||
const peers = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_PEER':
|
||||
{
|
||||
const { peer } = action.payload;
|
||||
|
||||
return { ...state, [peer.name]: peer };
|
||||
}
|
||||
|
||||
case 'REMOVE_PEER':
|
||||
{
|
||||
const { peerName } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
delete newState[peerName];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_PEER_DISPLAY_NAME':
|
||||
{
|
||||
const { displayName, peerName } = action.payload;
|
||||
const peer = state[peerName];
|
||||
|
||||
if (!peer)
|
||||
throw new Error('no Peer found');
|
||||
|
||||
const newPeer = { ...peer, displayName };
|
||||
|
||||
return { ...state, [newPeer.name]: newPeer };
|
||||
}
|
||||
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const { consumer, peerName } = action.payload;
|
||||
const peer = state[peerName];
|
||||
|
||||
if (!peer)
|
||||
throw new Error('no Peer found for new Consumer');
|
||||
|
||||
const newConsumers = [ ...peer.consumers, consumer.id ];
|
||||
const newPeer = { ...peer, consumers: newConsumers };
|
||||
|
||||
return { ...state, [newPeer.name]: newPeer };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const { consumerId, peerName } = action.payload;
|
||||
const peer = state[peerName];
|
||||
|
||||
// NOTE: This means that the Peer was closed before, so it's ok.
|
||||
if (!peer)
|
||||
return state;
|
||||
|
||||
const idx = peer.consumers.indexOf(consumerId);
|
||||
|
||||
if (idx === -1)
|
||||
throw new Error('Consumer not found');
|
||||
|
||||
const newConsumers = peer.consumers.slice();
|
||||
|
||||
newConsumers.splice(idx, 1);
|
||||
|
||||
const newPeer = { ...peer, consumers: newConsumers };
|
||||
|
||||
return { ...state, [newPeer.name]: newPeer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default peers;
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
const initialState = {};
|
||||
|
||||
const producers = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_PRODUCER':
|
||||
{
|
||||
const { producer } = action.payload;
|
||||
|
||||
return { ...state, [producer.id]: producer };
|
||||
}
|
||||
|
||||
case 'REMOVE_PRODUCER':
|
||||
{
|
||||
const { producerId } = action.payload;
|
||||
const newState = { ...state };
|
||||
|
||||
delete newState[producerId];
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_PAUSED':
|
||||
{
|
||||
const { producerId, originator } = action.payload;
|
||||
const producer = state[producerId];
|
||||
let newProducer;
|
||||
|
||||
if (originator === 'local')
|
||||
newProducer = { ...producer, locallyPaused: true };
|
||||
else
|
||||
newProducer = { ...producer, remotelyPaused: true };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_RESUMED':
|
||||
{
|
||||
const { producerId, originator } = action.payload;
|
||||
const producer = state[producerId];
|
||||
let newProducer;
|
||||
|
||||
if (originator === 'local')
|
||||
newProducer = { ...producer, locallyPaused: false };
|
||||
else
|
||||
newProducer = { ...producer, remotelyPaused: false };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_TRACK':
|
||||
{
|
||||
const { producerId, track } = action.payload;
|
||||
const producer = state[producerId];
|
||||
const newProducer = { ...producer, track };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default producers;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
const initialState =
|
||||
{
|
||||
url : null,
|
||||
state : 'new', // new/connecting/connected/disconnected/closed,
|
||||
activeSpeakerName : null
|
||||
};
|
||||
|
||||
const room = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'SET_ROOM_URL':
|
||||
{
|
||||
const { url } = action.payload;
|
||||
|
||||
return { ...state, url };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_STATE':
|
||||
{
|
||||
const roomState = action.payload.state;
|
||||
|
||||
if (roomState == 'connected')
|
||||
return { ...state, state: roomState };
|
||||
else
|
||||
return { ...state, state: roomState, activeSpeakerName: null };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_ACTIVE_SPEAKER':
|
||||
{
|
||||
const { peerName } = action.payload;
|
||||
|
||||
return { ...state, activeSpeakerName: peerName };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default room;
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import randomString from 'random-string';
|
||||
import * as stateActions from './stateActions';
|
||||
|
||||
export const joinRoom = (
|
||||
{ roomId, peerName, displayName, device, useSimulcast, produce }) =>
|
||||
{
|
||||
return {
|
||||
type : 'JOIN_ROOM',
|
||||
payload : { roomId, peerName, displayName, device, useSimulcast, produce }
|
||||
};
|
||||
};
|
||||
|
||||
export const leaveRoom = () =>
|
||||
{
|
||||
return {
|
||||
type : 'LEAVE_ROOM'
|
||||
};
|
||||
};
|
||||
|
||||
export const changeDisplayName = (displayName) =>
|
||||
{
|
||||
return {
|
||||
type : 'CHANGE_DISPLAY_NAME',
|
||||
payload : { displayName }
|
||||
};
|
||||
};
|
||||
|
||||
export const muteMic = () =>
|
||||
{
|
||||
return {
|
||||
type : 'MUTE_MIC'
|
||||
};
|
||||
};
|
||||
|
||||
export const unmuteMic = () =>
|
||||
{
|
||||
return {
|
||||
type : 'UNMUTE_MIC'
|
||||
};
|
||||
};
|
||||
|
||||
export const enableWebcam = () =>
|
||||
{
|
||||
return {
|
||||
type : 'ENABLE_WEBCAM'
|
||||
};
|
||||
};
|
||||
|
||||
export const disableWebcam = () =>
|
||||
{
|
||||
return {
|
||||
type : 'DISABLE_WEBCAM'
|
||||
};
|
||||
};
|
||||
|
||||
export const changeWebcam = () =>
|
||||
{
|
||||
return {
|
||||
type : 'CHANGE_WEBCAM'
|
||||
};
|
||||
};
|
||||
|
||||
export const enableAudioOnly = () =>
|
||||
{
|
||||
return {
|
||||
type : 'ENABLE_AUDIO_ONLY'
|
||||
};
|
||||
};
|
||||
|
||||
export const disableAudioOnly = () =>
|
||||
{
|
||||
return {
|
||||
type : 'DISABLE_AUDIO_ONLY'
|
||||
};
|
||||
};
|
||||
|
||||
export const restartIce = () =>
|
||||
{
|
||||
return {
|
||||
type : 'RESTART_ICE'
|
||||
};
|
||||
};
|
||||
|
||||
// This returns a redux-thunk action (a function).
|
||||
export const notify = ({ type = 'info', text, timeout }) =>
|
||||
{
|
||||
if (!timeout)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case 'info':
|
||||
timeout = 3000;
|
||||
break;
|
||||
case 'error':
|
||||
timeout = 5000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const notification =
|
||||
{
|
||||
id : randomString({ length: 6 }).toLowerCase(),
|
||||
type : type,
|
||||
text : text,
|
||||
timeout : timeout
|
||||
};
|
||||
|
||||
return (dispatch) =>
|
||||
{
|
||||
dispatch(stateActions.addNotification(notification));
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
dispatch(stateActions.removeNotification(notification.id));
|
||||
}, timeout);
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import RoomClient from '../RoomClient';
|
||||
|
||||
export default ({ dispatch, getState }) => (next) =>
|
||||
{
|
||||
let client;
|
||||
|
||||
return (action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'JOIN_ROOM':
|
||||
{
|
||||
const {
|
||||
roomId,
|
||||
peerName,
|
||||
displayName,
|
||||
device,
|
||||
useSimulcast,
|
||||
produce
|
||||
} = action.payload;
|
||||
|
||||
client = new RoomClient(
|
||||
{
|
||||
roomId,
|
||||
peerName,
|
||||
displayName,
|
||||
device,
|
||||
useSimulcast,
|
||||
produce,
|
||||
dispatch,
|
||||
getState
|
||||
});
|
||||
|
||||
// TODO: TMP
|
||||
global.CLIENT = client;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'LEAVE_ROOM':
|
||||
{
|
||||
client.close();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'CHANGE_DISPLAY_NAME':
|
||||
{
|
||||
const { displayName } = action.payload;
|
||||
|
||||
client.changeDisplayName(displayName);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'MUTE_MIC':
|
||||
{
|
||||
client.muteMic();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'UNMUTE_MIC':
|
||||
{
|
||||
client.unmuteMic();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ENABLE_WEBCAM':
|
||||
{
|
||||
client.enableWebcam();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'DISABLE_WEBCAM':
|
||||
{
|
||||
client.disableWebcam();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'CHANGE_WEBCAM':
|
||||
{
|
||||
client.changeWebcam();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ENABLE_AUDIO_ONLY':
|
||||
{
|
||||
client.enableAudioOnly();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'DISABLE_AUDIO_ONLY':
|
||||
{
|
||||
client.disableAudioOnly();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'RESTART_ICE':
|
||||
{
|
||||
client.restartIce();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
export const setRoomUrl = (url) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_URL',
|
||||
payload : { url }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomState = (state) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_STATE',
|
||||
payload : { state }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomActiveSpeaker = (peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_ACTIVE_SPEAKER',
|
||||
payload : { peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMe = ({ peerName, displayName, displayNameSet, device }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ME',
|
||||
payload : { peerName, displayName, displayNameSet, device }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MEDIA_CAPABILITIES',
|
||||
payload : { canSendMic, canSendWebcam }
|
||||
};
|
||||
};
|
||||
|
||||
export const setCanChangeWebcam = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CAN_CHANGE_WEBCAM',
|
||||
payload : flag
|
||||
};
|
||||
};
|
||||
|
||||
export const setDisplayName = (displayName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_DISPLAY_NAME',
|
||||
payload : { displayName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioOnlyState = (enabled) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_ONLY_STATE',
|
||||
payload : { enabled }
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioOnlyInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_ONLY_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRestartIceInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_RESTART_ICE_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const addProducer = (producer) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_PRODUCER',
|
||||
payload : { producer }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeProducer = (producerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_PRODUCER',
|
||||
payload : { producerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerPaused = (producerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_PAUSED',
|
||||
payload : { producerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerResumed = (producerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_RESUMED',
|
||||
payload : { producerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerTrack = (producerId, track) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_TRACK',
|
||||
payload : { producerId, track }
|
||||
};
|
||||
};
|
||||
|
||||
export const setWebcamInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_WEBCAM_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const addPeer = (peer) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_PEER',
|
||||
payload : { peer }
|
||||
};
|
||||
};
|
||||
|
||||
export const removePeer = (peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_PEER',
|
||||
payload : { peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerDisplayName = (displayName, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_DISPLAY_NAME',
|
||||
payload : { displayName, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const addConsumer = (consumer, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_CONSUMER',
|
||||
payload : { consumer, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeConsumer = (consumerId, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_CONSUMER',
|
||||
payload : { consumerId, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerPaused = (consumerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_PAUSED',
|
||||
payload : { consumerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerResumed = (consumerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_RESUMED',
|
||||
payload : { consumerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerEffectiveProfile = (consumerId, profile) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_EFFECTIVE_PROFILE',
|
||||
payload : { consumerId, profile }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerTrack = (consumerId, track) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_TRACK',
|
||||
payload : { consumerId, track }
|
||||
};
|
||||
};
|
||||
|
||||
export const addNotification = (notification) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NOTIFICATION',
|
||||
payload : { notification }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeNotification = (notificationId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_NOTIFICATION',
|
||||
payload : { notificationId }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeAllNotifications = () =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_ALL_NOTIFICATIONS'
|
||||
};
|
||||
};
|
||||
|
|
@ -1,12 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
export function getProtooUrl(peerId, roomId)
|
||||
export function getProtooUrl(peerName, roomId)
|
||||
{
|
||||
let hostname = window.location.hostname;
|
||||
let port = config.protoo.listenPort;
|
||||
let url = `wss://${hostname}:${port}/?peer-id=${peerId}&room-id=${roomId}`;
|
||||
const hostname = window.location.hostname;
|
||||
const url = `wss://${hostname}:3443/?peerName=${peerName}&roomId=${roomId}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,75 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
import browser from 'bowser';
|
||||
import randomNumberLib from 'random-number';
|
||||
import Logger from './Logger';
|
||||
|
||||
global.BROWSER = browser;
|
||||
|
||||
const logger = new Logger('utils');
|
||||
const randomNumberGenerator = randomNumberLib.generator(
|
||||
{
|
||||
min : 10000000,
|
||||
max : 99999999,
|
||||
integer : true
|
||||
});
|
||||
|
||||
let mediaQueryDetectorElem;
|
||||
|
||||
export function initialize()
|
||||
{
|
||||
logger.debug('initialize()');
|
||||
|
||||
// Media query detector stuff
|
||||
mediaQueryDetectorElem = document.getElementById('mediasoup-demo-app-media-query-detector');
|
||||
// Media query detector stuff.
|
||||
mediaQueryDetectorElem =
|
||||
document.getElementById('mediasoup-demo-app-media-query-detector');
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export function isDesktop()
|
||||
{
|
||||
return !!mediaQueryDetectorElem.offsetParent;
|
||||
return Boolean(mediaQueryDetectorElem.offsetParent);
|
||||
}
|
||||
|
||||
export function isMobile()
|
||||
{
|
||||
return !mediaQueryDetectorElem.offsetParent;
|
||||
}
|
||||
|
||||
export function isPlanB()
|
||||
{
|
||||
if (browser.chrome || browser.chromium || browser.opera || browser.safari || browser.msedge)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfortunately Edge produces rtpSender.send() to fail when receiving media
|
||||
* from others and removing/adding a local track.
|
||||
*/
|
||||
export function canChangeResolution()
|
||||
{
|
||||
if (browser.msedge)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function randomNumber()
|
||||
{
|
||||
return randomNumberGenerator();
|
||||
}
|
||||
|
||||
export function closeMediaStream(stream)
|
||||
{
|
||||
if (!stream)
|
||||
return;
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
|
||||
for (let i=0, len=tracks.length; i < len; i++)
|
||||
{
|
||||
tracks[i].stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,61 @@
|
|||
{
|
||||
"name": "mediasoup-demo-app",
|
||||
"version": "1.2.0",
|
||||
"version": "2.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.7.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"classnames": "^2.2.5",
|
||||
"debug": "^2.6.8",
|
||||
"debug": "^3.1.0",
|
||||
"domready": "^1.0.8",
|
||||
"hark": "github:ibc/hark#main-with-raf",
|
||||
"material-ui": "^0.18.4",
|
||||
"prop-types": "^15.5.10",
|
||||
"protoo-client": "^1.1.4",
|
||||
"random-number": "0.0.7",
|
||||
"hark": "^1.1.6",
|
||||
"js-cookie": "^2.2.0",
|
||||
"mediasoup-client": "^2.0.1",
|
||||
"node-random-name": "^1.0.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"protoo-client": "^2.0.5",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^15.6.1",
|
||||
"react": "^16.0.0",
|
||||
"react-clipboard.js": "^1.1.2",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-notification-system": "github:ibc/react-notification-system#master",
|
||||
"react-tap-event-plugin": "^2.0.1",
|
||||
"react-transition-group": "^1.2.0",
|
||||
"sdp-transform": "^2.3.0",
|
||||
"url-parse": "^1.1.9",
|
||||
"yaeti": "^1.0.1"
|
||||
"react-dom": "^16.0.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-spinner": "^0.2.7",
|
||||
"react-tooltip": "^3.4.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"redux": "^3.7.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"riek": "^1.1.0",
|
||||
"supports-color": "^5.0.0",
|
||||
"url-parse": "^1.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-plugin-transform-object-assign": "^6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.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.12",
|
||||
"browserify": "^14.4.0",
|
||||
"babelify": "^8.0.0",
|
||||
"browser-sync": "^2.18.13",
|
||||
"browserify": "^14.5.0",
|
||||
"del": "^3.0.0",
|
||||
"envify": "^4.0.0",
|
||||
"eslint": "^4.1.1",
|
||||
"eslint-plugin-import": "^2.6.0",
|
||||
"eslint-plugin-react": "^7.1.0",
|
||||
"envify": "^4.1.0",
|
||||
"eslint": "^4.10.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"gulp": "git://github.com/gulpjs/gulp.git#4.0",
|
||||
"gulp-css-base64": "^1.3.4",
|
||||
"gulp-eslint": "^4.0.0",
|
||||
"gulp-header": "^1.8.8",
|
||||
"gulp-header": "^1.8.9",
|
||||
"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-touch-cmd": "0.0.1",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-util": "^3.0.8",
|
||||
"mkdirp": "^0.5.1",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 458 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 458 KiB |
|
After Width: | Height: | Size: 861 B |
|
After Width: | Height: | Size: 365 B |
|
After Width: | Height: | Size: 946 B |
|
After Width: | Height: | Size: 632 B |
|
After Width: | Height: | Size: 917 B |
|
|
@ -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="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 3h-1v5h1V3zm-2 2h-2V4h2V3h-3v3h2v1h-2v1h3V5zm3-2v5h1V6h2V3h-3zm2 2h-1V4h1v1zm0 10.5c-1.25 0-2.45-.2-3.57-.57-.35-.11-.74-.03-1.01.24l-2.2 2.2c-2.83-1.44-5.15-3.75-6.59-6.59l2.2-2.21c.27-.26.35-.65.24-1C8.7 6.45 8.5 5.25 8.5 4c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1 0 9.39 7.61 17 17 17 .55 0 1-.45 1-1v-3.5c0-.55-.45-1-1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 487 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.65" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 372 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 390 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 410 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.85" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 554 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 553 B |
|
|
@ -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="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 214 B |
|
|
@ -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="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 320 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 553 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.65" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 352 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#000000" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 265 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg fill="#FFFFFF" fill-opacity="0.5" height="36" viewBox="0 0 24 24" width="36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/>
|
||||
<path d="M21 6.5l-4 4V7c0-.55-.45-1-1-1H9.82L21 17.18V6.5zM3.27 2L2 3.27 4.73 6H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.21 0 .39-.08.54-.18L19.73 21 21 19.73 3.27 2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
|
After Width: | Height: | Size: 2.2 MiB |
|
|
@ -1,5 +0,0 @@
|
|||
[data-component='App'] {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
[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 .5s infinite linear;
|
||||
}
|
||||
|
||||
&.state-connected,
|
||||
&.state-completed {
|
||||
border-color: rgba(#49ce3e, 0.9);
|
||||
}
|
||||
|
||||
&.state-failed,
|
||||
&.state-disconnected,
|
||||
&.state-closed {
|
||||
border-color: rgba(#ff2000, 0.75);
|
||||
}
|
||||
|
||||
&.active-speaker {
|
||||
border-color: rgba(#fff, 0.9);
|
||||
}
|
||||
|
||||
> .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,98 @@
|
|||
[data-component='Me'] {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
> .controls {
|
||||
position: absolute;
|
||||
z-index: 10
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction:; row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
> .button {
|
||||
flex: 0 0 auto;
|
||||
margin: 4px;
|
||||
margin-left: 0;
|
||||
border-radius: 2px;
|
||||
background-position: center;
|
||||
background-size: 75%;
|
||||
background-repeat: no-repeat;
|
||||
background-color: rgba(#000, 0.5);
|
||||
cursor: pointer;
|
||||
transition-property: opacity, background-color;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
+desktop() {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
&.unsupported {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.on {
|
||||
background-color: rgba(#fff, 0.7);
|
||||
}
|
||||
|
||||
&.mic {
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon_mic_black_on.svg');
|
||||
}
|
||||
|
||||
&.off {
|
||||
background-image: url('/resources/images/icon_mic_white_off.svg');
|
||||
background-color: rgba(#d42241, 0.7);
|
||||
}
|
||||
|
||||
&.unsupported {
|
||||
background-image: url('/resources/images/icon_mic_white_unsupported.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.webcam {
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon_webcam_black_on.svg');
|
||||
}
|
||||
|
||||
&.off {
|
||||
background-image: url('/resources/images/icon_webcam_white_on.svg');
|
||||
}
|
||||
|
||||
&.unsupported {
|
||||
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.change-webcam {
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon_change_webcam_black.svg');
|
||||
}
|
||||
|
||||
&.unsupported {
|
||||
background-image: url('/resources/images/icon_change_webcam_white_unsupported.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
[data-component='Notifications'] {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
|
||||
+desktop() {
|
||||
padding: 10px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
padding: 4px;
|
||||
width: 65%;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
> .notification {
|
||||
pointer-events: auto;
|
||||
margin-top: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.Appear-appear {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.15s ease-in-out 0s, visibility 0s linear 0.25s;
|
||||
transform: translateX(200px);
|
||||
}
|
||||
|
||||
&.Appear-appear.Appear-appear-active {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
transform: translateY(0%);
|
||||
transition-delay: 0s, 0s;
|
||||
}
|
||||
|
||||
+desktop() {
|
||||
padding: 16px 24px 16px 12px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
padding: 6px 16px 6px 12px;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
flex: 0 0 auto;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 12px;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
> .text {
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
|
||||
+desktop() {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: rgba(#0a1d26, 0.75);
|
||||
color: rgba(#fff, 0.65);
|
||||
|
||||
>.icon {
|
||||
opacity: 0.65;
|
||||
background-image: url('/resources/images/icon_notification_info_white.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: rgba(#ff1914, 0.65);
|
||||
color: rgba(#fff, 0.85);
|
||||
|
||||
>.icon {
|
||||
opacity: 0.85;
|
||||
background-image: url('/resources/images/icon_notification_error_white.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
[data-component='Peer'] {
|
||||
flex: 100 100 auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
+mobile() {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
> .indicators {
|
||||
position: absolute;
|
||||
z-index: 10
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction:; row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
> .icon {
|
||||
flex: 0 0 auto;
|
||||
margin: 4px;
|
||||
margin-left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-position: center;
|
||||
background-size: 75%;
|
||||
background-repeat: no-repeat;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
+desktop() {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
&.mic-off {
|
||||
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
|
||||
}
|
||||
|
||||
&.webcam-off {
|
||||
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.incompatible-video {
|
||||
position: absolute;
|
||||
z-index: 2
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> p {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
font-size: 15px;
|
||||
color: rgba(#fff, 0.55);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
[data-component='PeerView'] {
|
||||
position: relative;
|
||||
flex: 100 100 auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: rgba(#2a4b58, 0.9);
|
||||
background-image: url('/resources/images/buddy.svg');
|
||||
background-position: bottom;
|
||||
background-size: auto 85%;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
> .info {
|
||||
$backgroundTint = #000;
|
||||
|
||||
position: absolute;
|
||||
z-index: 5
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(to bottom,
|
||||
rgba($backgroundTint, 0) 0%,
|
||||
rgba($backgroundTint, 0) 60%,
|
||||
rgba($backgroundTint, 0.1) 70%,
|
||||
rgba($backgroundTint, 0.8) 100%);
|
||||
|
||||
> .media {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
> .box {
|
||||
margin: 4px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(#000, 0.25);
|
||||
|
||||
> p {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
margin-bottom: 2px;
|
||||
color: rgba(#fff, 0.7);
|
||||
font-size: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .peer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
+desktop() {
|
||||
&.is-me {
|
||||
padding: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&:not(.is-me) {
|
||||
padding: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
&.is-me {
|
||||
padding: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&:not(.is-me) {
|
||||
padding: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
> .display-name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(#fff, 0.85);
|
||||
}
|
||||
|
||||
> span.display-name {
|
||||
user-select: none;
|
||||
cursor: text;
|
||||
|
||||
&:not(.editable) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.editable {
|
||||
+desktop() {
|
||||
&:hover {
|
||||
background-color: rgba(#aeff00, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
> input.display-name {
|
||||
border: none;
|
||||
border-bottom: 1px solid #aeff00;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
> .row {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
|
||||
> .device-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-right: 3px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('/resources/images/devices/unknown.svg');
|
||||
|
||||
&.chrome {
|
||||
background-image: url('/resources/images/devices/chrome_16x16.png');
|
||||
}
|
||||
|
||||
&.firefox {
|
||||
background-image: url('/resources/images/devices/firefox_16x16.png');
|
||||
}
|
||||
|
||||
&.safari {
|
||||
background-image: url('/resources/images/devices/safari_16x16.png');
|
||||
}
|
||||
|
||||
&.msedge {
|
||||
background-image: url('/resources/images/devices/edge_16x16.png');
|
||||
}
|
||||
|
||||
&.opera {
|
||||
background-image: url('/resources/images/devices/opera_16x16.png');
|
||||
}
|
||||
|
||||
&.sipendpoint {
|
||||
background-image: url('/resources/images/devices/sip_endpoint.svg');
|
||||
}
|
||||
}
|
||||
|
||||
> .device-version {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
font-size: 11px;
|
||||
color: rgba(#fff, 0.55);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> video {
|
||||
flex: 100 100 auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
user-select: none;
|
||||
transition-property: opacity;
|
||||
transition-duration: .15s;
|
||||
background-color: rgba(#000, 0.75);
|
||||
|
||||
&.is-me {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
filter: blur(5px);
|
||||
}
|
||||
}
|
||||
|
||||
> .volume-container {
|
||||
position: absolute;
|
||||
top: 0
|
||||
bottom: 0;
|
||||
right: 2px;
|
||||
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); }
|
||||
}
|
||||
}
|
||||
|
||||
> .spinner-container {
|
||||
position: absolute;
|
||||
top: 0
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(#000, 0.75);
|
||||
|
||||
.react-spinner {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
.react-spinner_bar {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
height: 7.8%;
|
||||
top: -3.9%;
|
||||
left: -10%;
|
||||
animation: PeerView-spinner 1.2s linear infinite;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(#fff, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes PeerView-spinner {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0.15; }
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
[data-component='Peers'] {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
|
||||
+desktop() {
|
||||
width: 100%;
|
||||
padding: 40px 0 140px 0;
|
||||
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;
|
||||
}
|
||||
|
||||
> .peer-container {
|
||||
overflow: hidden;
|
||||
|
||||
AppearFadeIn(1000ms);
|
||||
|
||||
+desktop() {
|
||||
flex: 0 0 auto;
|
||||
height: 382px;
|
||||
width: 450px;
|
||||
margin: 6px;
|
||||
border: 1px solid rgba(#fff, 0.15);
|
||||
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
|
||||
transition-property: border-color;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
&.active-speaker {
|
||||
border-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
flex: 100 100 auto;
|
||||
order: 2;
|
||||
min-height: 25vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.active-speaker {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
&.active-speaker {
|
||||
border-color: rgba(#fff, 0.9);
|
||||
}
|
||||
|
||||
> .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,91 @@
|
|||
[data-component='Room'] {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
AppearFadeIn(300ms);
|
||||
|
||||
> .state {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 25px;
|
||||
background-color: rgba(#fff, 0.2);
|
||||
|
||||
+desktop() {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 124px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 100%;
|
||||
|
||||
+desktop() {
|
||||
margin: 5px;
|
||||
margin-right: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
margin: 4px;
|
||||
margin-right: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.new, &.closed {
|
||||
background-color: rgba(#aaa, 0.5);
|
||||
}
|
||||
|
||||
&.connecting {
|
||||
animation: Room-info-state-connecting .75s infinite linear;
|
||||
}
|
||||
|
||||
&.connected {
|
||||
background-color: rgba(#30bd18, 0.75);
|
||||
|
||||
+mobile() {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .text {
|
||||
flex: 100 0 auto;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 400;
|
||||
color: rgba(#fff, 0.75);
|
||||
|
||||
+desktop() {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&.connected {
|
||||
+mobile() {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .room-link-wrapper {
|
||||
|
|
@ -24,7 +101,7 @@
|
|||
|
||||
> .room-link {
|
||||
width: auto;
|
||||
background-color: rgba(#fff, 0.8);
|
||||
background-color: rgba(#fff, 0.75);
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
box-shadow: 0px 3px 12px 2px rgba(#111, 0.4);
|
||||
|
|
@ -33,15 +110,24 @@
|
|||
display: block;;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
padding: 10px 20px;
|
||||
color: #104758;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.25s;
|
||||
opacity: 0.8;
|
||||
|
||||
+desktop() {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
|
|
@ -50,65 +136,105 @@
|
|||
}
|
||||
}
|
||||
|
||||
> .remote-videos {
|
||||
> .me-container {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
|
||||
transition-property: border-color;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
&.active-speaker {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
+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;
|
||||
height: 200px;
|
||||
width: 235px;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
border: 1px solid rgba(#fff, 0.15);
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
height: 175px;
|
||||
width: 150px;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
border: 1px solid rgba(#fff, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
> .sidebar {
|
||||
position: fixed;
|
||||
z-index: 101;
|
||||
top: calc(50% - 60px);
|
||||
height: 120px;
|
||||
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;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
> .show-stats {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: -40px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-image: url('/resources/images/stats.svg');
|
||||
> .button {
|
||||
flex: 0 0 auto;
|
||||
margin: 4px 0;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-size: 75%;
|
||||
background-repeat: no-repeat;
|
||||
background-color: rgba(#000, 0.25);
|
||||
border-radius: 4px;
|
||||
background-color: rgba(#fff, 0.15);
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
transition-duration: 0.25s;
|
||||
transition-property: opacity, background-color;
|
||||
transition-duration: 0.15s;
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
+desktop() {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
&.on {
|
||||
background-color: rgba(#fff, 0.7);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.audio-only {
|
||||
background-image: url('/resources/images/icon_audio_only_white.svg');
|
||||
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon_audio_only_black.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.restart-ice {
|
||||
background-image: url('/resources/images/icon_restart_ice_white.svg');
|
||||
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon_restart_ice__black.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes Room-info-state-connecting {
|
||||
50% { background-color: rgba(orange, 0.75); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
[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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
[data-component='Video'] {
|
||||
position: relative;
|
||||
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;
|
||||
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: 2px;
|
||||
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;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,18 @@ global-reset();
|
|||
|
||||
@import './mixins';
|
||||
@import './fonts';
|
||||
@import './reset';
|
||||
|
||||
html {
|
||||
font-family: 'Roboto';
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
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;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 300;
|
||||
|
||||
+desktop() {
|
||||
font-size: 16px;
|
||||
|
|
@ -26,25 +27,20 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#mediasoup-demo-app-container {
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
// Components
|
||||
@import './components/App';
|
||||
@import './components/Room';
|
||||
@import './components/LocalVideo';
|
||||
@import './components/RemoteVideo';
|
||||
@import './components/Video';
|
||||
@import './components/Stats';
|
||||
@import './components/Me';
|
||||
@import './components/Peers';
|
||||
@import './components/Peer';
|
||||
@import './components/PeerView';
|
||||
@import './components/Notifications';
|
||||
}
|
||||
|
||||
// Hack to detect in JS the current media query
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ desktop()
|
|||
@media (min-device-width: 721px)
|
||||
{block}
|
||||
|
||||
TransitionAppear($duration = 1s, $appearOpacity = 0, $activeOpacity = 1)
|
||||
AppearFadeIn($duration = 1s, $enterOpacity = 0, $activeOpacity = 1)
|
||||
will-change: opacity;
|
||||
|
||||
&.transition-appear
|
||||
opacity: $appearOpacity;
|
||||
&.Appear-appear
|
||||
opacity: $enterOpacity;
|
||||
|
||||
&.transition-appear.transition-appear-active
|
||||
&.Appear-appear.Appear-appear-active
|
||||
transition-property: opacity;
|
||||
transition-duration: $duration;
|
||||
opacity: $activeOpacity;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background: none;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
/* eslint-disable key-spacing */
|
||||
|
||||
exports.ROOM_OPTIONS =
|
||||
{
|
||||
requestTimeout: 10000,
|
||||
transportOptions:
|
||||
{
|
||||
tcp: false
|
||||
},
|
||||
__turnServers:
|
||||
[
|
||||
{
|
||||
urls: [ 'turn:worker2.versatica.com:3478?transport=udp' ],
|
||||
username: 'testuser1',
|
||||
credential: 'testpasswd1'
|
||||
}
|
||||
],
|
||||
hidden: false
|
||||
};
|
||||
|
||||
exports.ROOM_RTP_CAPABILITIES =
|
||||
{
|
||||
codecs:
|
||||
[
|
||||
{
|
||||
name: 'PCMA',
|
||||
mimeType: 'audio/PCMA',
|
||||
kind: 'audio',
|
||||
clockRate: 8000,
|
||||
preferredPayloadType: 8,
|
||||
rtcpFeedback: [],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
name: 'opus',
|
||||
mimeType: 'audio/opus',
|
||||
kind: 'audio',
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
preferredPayloadType: 96,
|
||||
rtcpFeedback: [],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
name: 'SILK',
|
||||
mimeType: 'audio/SILK',
|
||||
kind: 'audio',
|
||||
clockRate: 16000,
|
||||
preferredPayloadType: 97,
|
||||
rtcpFeedback: [],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
name: 'VP9',
|
||||
mimeType: 'video/VP9',
|
||||
kind: 'video',
|
||||
clockRate: 90000,
|
||||
preferredPayloadType: 102,
|
||||
rtcpFeedback:
|
||||
[
|
||||
{
|
||||
parameter: '',
|
||||
type: 'nack'
|
||||
},
|
||||
{
|
||||
parameter: 'pli',
|
||||
type: 'nack'
|
||||
},
|
||||
{
|
||||
parameter: '',
|
||||
type: 'goog-remb'
|
||||
},
|
||||
{
|
||||
parameter: 'bar',
|
||||
type: 'foo'
|
||||
}
|
||||
],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
name: 'rtx',
|
||||
mimeType: 'video/rtx',
|
||||
kind: 'video',
|
||||
clockRate: 90000,
|
||||
preferredPayloadType: 103,
|
||||
rtcpFeedback: [],
|
||||
parameters: {
|
||||
apt: 102
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'VP8',
|
||||
mimeType: 'video/VP8',
|
||||
kind: 'video',
|
||||
clockRate: 90000,
|
||||
preferredPayloadType: 100,
|
||||
rtcpFeedback:
|
||||
[
|
||||
{
|
||||
parameter: '',
|
||||
type: 'nack'
|
||||
},
|
||||
{
|
||||
parameter: 'pli',
|
||||
type: 'nack'
|
||||
},
|
||||
{
|
||||
parameter: '',
|
||||
type: 'goog-remb'
|
||||
},
|
||||
{
|
||||
parameter: 'bar',
|
||||
type: 'foo'
|
||||
}
|
||||
],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
name: 'rtx',
|
||||
mimeType: 'video/rtx',
|
||||
kind: 'video',
|
||||
clockRate: 90000,
|
||||
preferredPayloadType: 101,
|
||||
rtcpFeedback: [],
|
||||
parameters: {
|
||||
apt: 100
|
||||
}
|
||||
}
|
||||
],
|
||||
headerExtensions: [
|
||||
{
|
||||
kind: 'audio',
|
||||
uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level',
|
||||
preferredId: 10
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time',
|
||||
preferredId: 11
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
uri: 'http://foo.bar',
|
||||
preferredId: 12
|
||||
}
|
||||
],
|
||||
fecMechanisms: []
|
||||
};
|
||||
|
||||
exports.QUERY_ROOM_RESPONSE =
|
||||
{
|
||||
rtpCapabilities: exports.ROOM_RTP_CAPABILITIES
|
||||
};
|
||||
|
||||
exports.JOIN_ROOM_RESPONSE =
|
||||
{
|
||||
peers:
|
||||
[
|
||||
{
|
||||
name: 'alice',
|
||||
appData: 'Alice iPad Pro',
|
||||
consumers:
|
||||
[
|
||||
{
|
||||
id: 3333,
|
||||
kind: 'audio',
|
||||
paused: false,
|
||||
appData: 'ALICE_MIC',
|
||||
rtpParameters:
|
||||
{
|
||||
muxId: null,
|
||||
codecs:
|
||||
[
|
||||
{
|
||||
name: 'PCMA',
|
||||
mimeType: 'audio/PCMA',
|
||||
clockRate: 8000,
|
||||
payloadType: 8,
|
||||
rtcpFeedback: [],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
headerExtensions:
|
||||
[
|
||||
{
|
||||
uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level',
|
||||
id: 1
|
||||
}
|
||||
],
|
||||
encodings:
|
||||
[
|
||||
{
|
||||
ssrc: 33333333
|
||||
}
|
||||
],
|
||||
rtcp:
|
||||
{
|
||||
cname: 'ALICECNAME',
|
||||
reducedSize: true,
|
||||
mux: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'bob',
|
||||
appData: 'Bob HP Laptop',
|
||||
consumers:
|
||||
[
|
||||
{
|
||||
id: 6666,
|
||||
kind: 'audio',
|
||||
paused: false,
|
||||
appData: 'BOB_MIC',
|
||||
rtpParameters:
|
||||
{
|
||||
muxId: null,
|
||||
codecs:
|
||||
[
|
||||
{
|
||||
name: 'opus',
|
||||
mimeType: 'audio/opus',
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
payloadType: 96,
|
||||
rtcpFeedback: [],
|
||||
parameters: {}
|
||||
}
|
||||
],
|
||||
headerExtensions:
|
||||
[
|
||||
{
|
||||
uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level',
|
||||
id: 1
|
||||
}
|
||||
],
|
||||
encodings:
|
||||
[
|
||||
{
|
||||
ssrc: 66666666
|
||||
}
|
||||
],
|
||||
rtcp:
|
||||
{
|
||||
cname: 'BOBCNAME',
|
||||
reducedSize: true,
|
||||
mux: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
exports.CREATE_TRANSPORT_1_RESPONSE =
|
||||
{
|
||||
iceParameters:
|
||||
{
|
||||
usernameFragment: 'server-usernamefragment-12345678',
|
||||
password: 'server-password-xxxxxxxx',
|
||||
iceLite: true
|
||||
},
|
||||
iceCandidates:
|
||||
[
|
||||
{
|
||||
foundation: 'F1',
|
||||
priority: 1234,
|
||||
ip: '1.2.3.4',
|
||||
protocol: 'udp',
|
||||
port: 9999,
|
||||
type: 'host'
|
||||
}
|
||||
],
|
||||
dtlsParameters:
|
||||
{
|
||||
fingerprints:
|
||||
[
|
||||
{
|
||||
algorithm: 'sha-256',
|
||||
value: 'FF:FF:39:66:A4:E2:66:60:30:18:A7:59:B3:AF:A5:33:58:5E:7F:69:A4:62:A6:D4:EB:9F:B7:42:05:35:FF:FF'
|
||||
}
|
||||
],
|
||||
role: 'client'
|
||||
}
|
||||
};
|
||||
|
||||
exports.CREATE_TRANSPORT_2_RESPONSE =
|
||||
{
|
||||
iceParameters:
|
||||
{
|
||||
usernameFragment: 'server-usernamefragment-12345678',
|
||||
password: 'server-password-xxxxxxxx',
|
||||
iceLite: true
|
||||
},
|
||||
iceCandidates:
|
||||
[
|
||||
{
|
||||
foundation: 'F1',
|
||||
priority: 1234,
|
||||
ip: '1.2.3.4',
|
||||
protocol: 'udp',
|
||||
port: 9999,
|
||||
type: 'host'
|
||||
}
|
||||
],
|
||||
dtlsParameters:
|
||||
{
|
||||
fingerprints:
|
||||
[
|
||||
{
|
||||
algorithm: 'sha-256',
|
||||
value: 'FF:FF:39:66:A4:E2:66:60:30:18:A7:59:B3:AF:A5:33:58:5E:7F:69:A4:62:A6:D4:EB:9F:B7:42:05:35:FF:FF'
|
||||
}
|
||||
],
|
||||
role: 'auto'
|
||||
}
|
||||
};
|
||||
|
||||
exports.ALICE_WEBCAM_NEW_CONSUMER_NOTIFICATION =
|
||||
{
|
||||
method: 'newConsumer',
|
||||
notification: true,
|
||||
id: 4444,
|
||||
peerName: 'alice',
|
||||
kind: 'video',
|
||||
paused: true,
|
||||
appData: 'ALICE_WEBCAM',
|
||||
rtpParameters:
|
||||
{
|
||||
muxId: null,
|
||||
codecs:
|
||||
[
|
||||
{
|
||||
name: 'VP8',
|
||||
mimeType: 'video/VP8',
|
||||
clockRate: 90000,
|
||||
payloadType: 100,
|
||||
rtcpFeedback:
|
||||
[
|
||||
{
|
||||
parameter: '',
|
||||
type: 'nack'
|
||||
},
|
||||
{
|
||||
parameter: 'pli',
|
||||
type: 'nack'
|
||||
},
|
||||
{
|
||||
parameter: '',
|
||||
type: 'goog-remb'
|
||||
},
|
||||
{
|
||||
parameter: 'bar',
|
||||
type: 'foo'
|
||||
}
|
||||
],
|
||||
parameters: {}
|
||||
},
|
||||
{
|
||||
name: 'rtx',
|
||||
mimeType: 'video/rtx',
|
||||
clockRate: 90000,
|
||||
payloadType: 101,
|
||||
rtcpFeedback: [],
|
||||
parameters: {
|
||||
apt: 100
|
||||
}
|
||||
}
|
||||
],
|
||||
headerExtensions:
|
||||
[
|
||||
{
|
||||
kind: 'video',
|
||||
uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time',
|
||||
id: 11
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
uri: 'http://foo.bar',
|
||||
id: 12
|
||||
}
|
||||
],
|
||||
encodings:
|
||||
[
|
||||
{
|
||||
ssrc: 444444441,
|
||||
rtx: {
|
||||
ssrc: 444444442
|
||||
}
|
||||
}
|
||||
],
|
||||
rtcp:
|
||||
{
|
||||
cname: 'ALICECNAME',
|
||||
reducedSize: true,
|
||||
mux: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const gutil = require('gulp-util');
|
||||
const plumber = require('gulp-plumber');
|
||||
const rename = require('gulp-rename');
|
||||
const browserify = require('browserify');
|
||||
const watchify = require('watchify');
|
||||
const envify = require('envify/custom');
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const eslint = require('gulp-eslint');
|
||||
const browserSync = require('browser-sync');
|
||||
|
||||
const OUTPUT_DIR = 'output';
|
||||
const APP_NAME = 'mediasoup-client-test';
|
||||
|
||||
// Node environment.
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
function logError(error)
|
||||
{
|
||||
gutil.log(gutil.colors.red(error.stack));
|
||||
}
|
||||
|
||||
gulp.task('lint', () =>
|
||||
{
|
||||
const src =
|
||||
[
|
||||
'gulpfile.js',
|
||||
'**/*.js',
|
||||
'**/*.jsx'
|
||||
];
|
||||
|
||||
return gulp.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(eslint())
|
||||
.pipe(eslint.format());
|
||||
});
|
||||
|
||||
gulp.task('html', () =>
|
||||
{
|
||||
return gulp.src('index.html')
|
||||
.pipe(gulp.dest(OUTPUT_DIR));
|
||||
});
|
||||
|
||||
gulp.task('bundle', () =>
|
||||
{
|
||||
const watch = true;
|
||||
|
||||
let bundler = browserify(
|
||||
{
|
||||
entries : 'index.jsx',
|
||||
extensions : [ '.js', '.jsx' ],
|
||||
// required for sourcemaps (must be false otherwise).
|
||||
debug : process.env.NODE_ENV === 'development',
|
||||
// required for watchify.
|
||||
cache : {},
|
||||
// required for watchify.
|
||||
packageCache : {},
|
||||
// required to be true only for watchify.
|
||||
fullPaths : watch
|
||||
})
|
||||
.transform('babelify',
|
||||
{
|
||||
presets : [ 'es2015', 'es2017', 'react' ],
|
||||
plugins :
|
||||
[
|
||||
'transform-runtime',
|
||||
'transform-object-assign',
|
||||
'transform-object-rest-spread'
|
||||
]
|
||||
})
|
||||
.transform(envify(
|
||||
{
|
||||
NODE_ENV : process.env.NODE_ENV,
|
||||
_ : 'purge'
|
||||
}));
|
||||
|
||||
if (watch)
|
||||
{
|
||||
bundler = watchify(bundler);
|
||||
|
||||
bundler.on('update', () =>
|
||||
{
|
||||
const start = Date.now();
|
||||
|
||||
gutil.log('bundling...');
|
||||
rebundle();
|
||||
gutil.log('bundle took %sms', (Date.now() - start));
|
||||
});
|
||||
}
|
||||
|
||||
function rebundle()
|
||||
{
|
||||
return bundler.bundle()
|
||||
.on('error', logError)
|
||||
.pipe(plumber())
|
||||
.pipe(source(`${APP_NAME}.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(rename(`${APP_NAME}.js`))
|
||||
.pipe(gulp.dest(OUTPUT_DIR));
|
||||
}
|
||||
|
||||
return rebundle();
|
||||
});
|
||||
|
||||
gulp.task('livebrowser', (done) =>
|
||||
{
|
||||
browserSync(
|
||||
{
|
||||
server :
|
||||
{
|
||||
baseDir : OUTPUT_DIR
|
||||
},
|
||||
ghostMode : false,
|
||||
files : path.join(OUTPUT_DIR, '**', '*')
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('watch', (done) =>
|
||||
{
|
||||
// Watch changes in HTML.
|
||||
gulp.watch([ 'index.html' ], gulp.series(
|
||||
'html'
|
||||
));
|
||||
|
||||
// Watch changes in JS files.
|
||||
gulp.watch([ 'gulpfile.js', '**/*.js', '**/*.jsx' ], gulp.series(
|
||||
'lint'
|
||||
));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('live', gulp.series(
|
||||
'lint',
|
||||
'html',
|
||||
'bundle',
|
||||
'watch',
|
||||
'livebrowser'
|
||||
));
|
||||
|
||||
gulp.task('default', gulp.series('live'));
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>mediasoup-client test</title>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
|
||||
<meta name='description' content='mediasoup-client test'>
|
||||
|
||||
<script async src='/mediasoup-client-test.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>mediasoup-client test</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,692 @@
|
|||
import * as mediasoupClient from 'mediasoup-client';
|
||||
import domready from 'domready';
|
||||
import Logger from '../lib/Logger';
|
||||
const DATA = require('./DATA');
|
||||
|
||||
window.mediasoupClient = mediasoupClient;
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
|
||||
const SEND = true;
|
||||
const SEND_AUDIO = true;
|
||||
const SEND_VIDEO = false;
|
||||
const RECV = true;
|
||||
|
||||
|
||||
domready(() =>
|
||||
{
|
||||
logger.debug('DOM ready');
|
||||
|
||||
run();
|
||||
});
|
||||
|
||||
function run()
|
||||
{
|
||||
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
|
||||
|
||||
let transport1;
|
||||
let transport2;
|
||||
let audioTrack;
|
||||
let videoTrack;
|
||||
let audioProducer1;
|
||||
let audioProducer2;
|
||||
let videoProducer;
|
||||
|
||||
logger.debug('calling room = new mediasoupClient.Room()');
|
||||
|
||||
// const room = new mediasoupClient.Room();
|
||||
const room = new mediasoupClient.Room(DATA.ROOM_OPTIONS);
|
||||
|
||||
window.room = room;
|
||||
|
||||
room.on('closed', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'room "closed" event [originator:%s, appData:%o]', originator, appData);
|
||||
});
|
||||
|
||||
room.on('request', (request, callback, errback) =>
|
||||
{
|
||||
logger.warn('sending request [method:%s]:%o', request.method, request);
|
||||
|
||||
switch (request.method)
|
||||
{
|
||||
case 'queryRoom':
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
callback(DATA.QUERY_ROOM_RESPONSE);
|
||||
errback('upppps');
|
||||
}, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'joinRoom':
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
callback(DATA.JOIN_ROOM_RESPONSE);
|
||||
// errback('upppps');
|
||||
}, 200);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'createTransport':
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
switch (request.appData)
|
||||
{
|
||||
case 'TRANSPORT_1':
|
||||
callback(DATA.CREATE_TRANSPORT_1_RESPONSE);
|
||||
break;
|
||||
case 'TRANSPORT_2':
|
||||
callback(DATA.CREATE_TRANSPORT_2_RESPONSE);
|
||||
break;
|
||||
default:
|
||||
errback('upppps');
|
||||
}
|
||||
}, 250);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'createProducer':
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
callback();
|
||||
}, 250);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'enableConsumer':
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
callback();
|
||||
}, 500);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
errback(`NO IDEA ABOUT REQUEST METHOD "${request.method}"`);
|
||||
}
|
||||
});
|
||||
|
||||
room.on('notify', (notification) =>
|
||||
{
|
||||
logger.warn(
|
||||
'sending notification [method:%s]:%o', notification.method, notification);
|
||||
|
||||
switch (notification.method)
|
||||
{
|
||||
case 'leaveRoom':
|
||||
case 'updateTransport':
|
||||
case 'closeTransport':
|
||||
case 'closeProducer':
|
||||
case 'pauseProducer':
|
||||
case 'resumeProducer':
|
||||
case 'pauseConsumer':
|
||||
case 'resumeConsumer':
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.error(`NO IDEA ABOUT NOTIFICATION METHOD "${notification.method}"`);
|
||||
}
|
||||
});
|
||||
|
||||
room.on('newpeer', (peer) =>
|
||||
{
|
||||
logger.warn('room "newpeer" event [name:"%s", peer:%o]', peer.name, peer);
|
||||
|
||||
handlePeer(peer);
|
||||
});
|
||||
|
||||
Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
logger.debug('calling room.join()');
|
||||
|
||||
const deviceInfo = mediasoupClient.getDeviceInfo();
|
||||
const appData =
|
||||
{
|
||||
device : `${deviceInfo.name} ${deviceInfo.version}`
|
||||
};
|
||||
|
||||
return room.join(null, appData);
|
||||
// return room.join(DATA.ROOM_RTP_CAPABILITIES, appData);
|
||||
})
|
||||
.then((peers) =>
|
||||
{
|
||||
if (!RECV)
|
||||
return;
|
||||
|
||||
logger.debug('room.join() succeeded');
|
||||
|
||||
logger.debug('calling transport2 = room.createTransport("recv")');
|
||||
|
||||
transport2 = room.createTransport('recv', 'TRANSPORT_2');
|
||||
window.transport2 = transport2;
|
||||
window.pc2 = transport2._handler._pc;
|
||||
|
||||
handleTransport(transport2);
|
||||
|
||||
for (const peer of peers)
|
||||
{
|
||||
handlePeer(peer);
|
||||
}
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
if (!SEND)
|
||||
return;
|
||||
|
||||
if (room.canSend('audio'))
|
||||
logger.debug('can send audio');
|
||||
else
|
||||
logger.warn('cannot send audio');
|
||||
|
||||
if (room.canSend('video'))
|
||||
logger.debug('can send video');
|
||||
else
|
||||
logger.warn('cannot send video');
|
||||
|
||||
logger.debug('calling transport1 = room.createTransport("send")');
|
||||
|
||||
transport1 = room.createTransport('send', 'TRANSPORT_1');
|
||||
window.transport1 = transport1;
|
||||
window.pc1 = transport1._handler._pc;
|
||||
|
||||
handleTransport(transport1);
|
||||
|
||||
logger.debug('calling getUserMedia()');
|
||||
|
||||
return navigator.mediaDevices
|
||||
.getUserMedia({ audio: SEND_AUDIO, video: SEND_VIDEO });
|
||||
})
|
||||
.then((stream) =>
|
||||
{
|
||||
if (!SEND)
|
||||
return;
|
||||
|
||||
audioTrack = stream.getAudioTracks()[0];
|
||||
videoTrack = stream.getVideoTracks()[0];
|
||||
window.audioTrack = audioTrack;
|
||||
window.videoTrack = videoTrack;
|
||||
})
|
||||
// Add Producers.
|
||||
.then(() =>
|
||||
{
|
||||
if (audioTrack)
|
||||
{
|
||||
const deviceId = audioTrack.getSettings().deviceId;
|
||||
|
||||
logger.debug('calling audioProducer1 = room.createProducer(audioTrack)');
|
||||
|
||||
try
|
||||
{
|
||||
audioProducer1 = room.createProducer(audioTrack, `${deviceId}-1`);
|
||||
window.audioProducer1 = audioProducer1;
|
||||
|
||||
handleProducer(audioProducer1);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
logger.debug('calling audioProducer2 = room.createProducer(audioTrack)');
|
||||
|
||||
try
|
||||
{
|
||||
audioProducer2 = room.createProducer(audioTrack, `${deviceId}-2`);
|
||||
window.audioProducer2 = audioProducer2;
|
||||
|
||||
handleProducer(audioProducer2);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (videoTrack)
|
||||
{
|
||||
const deviceId = videoTrack.getSettings().deviceId;
|
||||
|
||||
logger.debug('calling videoProducer = room.createProducer(videoTrack)');
|
||||
|
||||
try
|
||||
{
|
||||
videoProducer = room.createProducer(videoTrack, `${deviceId}-1`);
|
||||
window.videoProducer = videoProducer;
|
||||
|
||||
handleProducer(videoProducer);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
})
|
||||
// Receive notifications.
|
||||
.then(() =>
|
||||
{
|
||||
if (!RECV)
|
||||
return;
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
room.receiveNotification(DATA.ALICE_WEBCAM_NEW_CONSUMER_NOTIFICATION);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function handleTransport(transport)
|
||||
{
|
||||
logger.warn(
|
||||
'handleTransport() [direction:%s, appData:"%s", transport:%o]',
|
||||
transport.direction, transport.appData, transport);
|
||||
|
||||
transport.on('closed', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'transport "closed" event [originator:%s, appData:%o, transport:%o]',
|
||||
originator, appData, transport);
|
||||
});
|
||||
|
||||
transport.on('connectionstatechange', (state) =>
|
||||
{
|
||||
logger.warn(
|
||||
'transport "connectionstatechange" event [direction:%s, state:%s, transport:%o]',
|
||||
transport.direction, state, transport);
|
||||
});
|
||||
|
||||
setInterval(() =>
|
||||
{
|
||||
const queue = transport._commandQueue._queue;
|
||||
|
||||
if (queue.length !== 0)
|
||||
logger.error('queue not empty [transport:%o, queue:%o]', transport, queue);
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
function handlePeer(peer)
|
||||
{
|
||||
logger.warn('handlePeer() [name:"%s", peer:%o]', peer.name, peer);
|
||||
|
||||
switch (peer.name)
|
||||
{
|
||||
case 'alice':
|
||||
window.alice = peer;
|
||||
break;
|
||||
case 'bob':
|
||||
window.bob = peer;
|
||||
break;
|
||||
}
|
||||
|
||||
for (const consumer of peer.consumers)
|
||||
{
|
||||
handleConsumer(consumer);
|
||||
}
|
||||
|
||||
peer.on('closed', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'peer "closed" event [name:"%s", originator:%s, appData:%o]',
|
||||
peer.name, originator, appData);
|
||||
});
|
||||
|
||||
peer.on('newconsumer', (consumer) =>
|
||||
{
|
||||
logger.warn(
|
||||
'peer "newconsumer" event [name:"%s", id:%s, consumer:%o]',
|
||||
peer.name, consumer.id, consumer);
|
||||
|
||||
handleConsumer(consumer);
|
||||
});
|
||||
}
|
||||
|
||||
function handleProducer(producer)
|
||||
{
|
||||
const transport1 = window.transport1;
|
||||
|
||||
logger.debug(
|
||||
'handleProducer() [id:"%s", appData:%o, producer:%o]',
|
||||
producer.id, producer.appData, producer);
|
||||
|
||||
logger.debug('handleProducer() | calling transport1.send(producer)');
|
||||
|
||||
transport1.send(producer)
|
||||
.then(() =>
|
||||
{
|
||||
logger.debug('transport1.send(producer) succeeded');
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('transport1.send(producer) failed: %o', error);
|
||||
});
|
||||
|
||||
producer.on('closed', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'producer "closed" event [id:%s, originator:%s, appData:%o, producer:%o]',
|
||||
producer.id, originator, appData, producer);
|
||||
});
|
||||
|
||||
producer.on('paused', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'producer "paused" event [id:%s, originator:%s, appData:%o, producer:%o]',
|
||||
producer.id, originator, appData, producer);
|
||||
});
|
||||
|
||||
producer.on('resumed', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'producer "resumed" event [id:%s, originator:%s, appData:%o, producer:%o]',
|
||||
producer.id, originator, appData, producer);
|
||||
});
|
||||
|
||||
producer.on('unhandled', () =>
|
||||
{
|
||||
logger.warn(
|
||||
'producer "unhandled" event [id:%s, producer:%o]', producer.id, producer);
|
||||
});
|
||||
}
|
||||
|
||||
function handleConsumer(consumer)
|
||||
{
|
||||
const transport2 = window.transport2;
|
||||
|
||||
logger.debug(
|
||||
'handleConsumer() [id:"%s", appData:%o, consumer:%o]',
|
||||
consumer.id, consumer.appData, consumer);
|
||||
|
||||
switch (consumer.appData)
|
||||
{
|
||||
case 'ALICE_MIC':
|
||||
window.aliceAudioConsumer = consumer;
|
||||
break;
|
||||
case 'ALICE_WEBCAM':
|
||||
window.aliceVideoConsumer = consumer;
|
||||
break;
|
||||
case 'BOB_MIC':
|
||||
window.bobAudioConsumer = consumer;
|
||||
break;
|
||||
}
|
||||
|
||||
logger.debug('handleConsumer() calling transport2.receive(consumer)');
|
||||
|
||||
transport2.receive(consumer)
|
||||
.then((track) =>
|
||||
{
|
||||
logger.warn(
|
||||
'transport2.receive(consumer) succeeded [track:%o]', track);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('transport2.receive() failed:%o', error);
|
||||
});
|
||||
|
||||
consumer.on('closed', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'consumer "closed" event [id:%s, originator:%s, appData:%o, consumer:%o]',
|
||||
consumer.id, originator, appData, consumer);
|
||||
});
|
||||
|
||||
consumer.on('paused', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'consumer "paused" event [id:%s, originator:%s, appData:%o, consumer:%o]',
|
||||
consumer.id, originator, appData, consumer);
|
||||
});
|
||||
|
||||
consumer.on('resumed', (originator, appData) =>
|
||||
{
|
||||
logger.warn(
|
||||
'consumer "resumed" event [id:%s, originator:%s, appData:%o, consumer:%o]',
|
||||
consumer.id, originator, appData, consumer);
|
||||
});
|
||||
|
||||
consumer.on('unhandled', () =>
|
||||
{
|
||||
logger.warn(
|
||||
'consumer "unhandled" event [id:%s, consumer:%o]', consumer.id, consumer);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// NOTE: Trigger server notifications.
|
||||
|
||||
window.notifyRoomClosed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'roomClosed',
|
||||
notification : true,
|
||||
appData : 'ha cascao la room remota!!!'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyTransportClosed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'transportClosed',
|
||||
notification : true,
|
||||
id : room.transports[0].id,
|
||||
appData : 'admin closed your transport'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAudioProducer1Closed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'producerClosed',
|
||||
notification : true,
|
||||
id : window.audioProducer1.id,
|
||||
appData : 'te paro el micro por la fuerza'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAudioProducer1Paused = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'producerPaused',
|
||||
notification : true,
|
||||
id : window.audioProducer1.id,
|
||||
appData : 'te pause el micro por la fuerza'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAudioProducer1Resumed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'producerResumed',
|
||||
notification : true,
|
||||
id : window.audioProducer1.id,
|
||||
appData : 'te resumo el micro'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAlicePeerClosed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'peerClosed',
|
||||
notification : true,
|
||||
name : 'alice',
|
||||
appData : 'peer left'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAliceAudioConsumerClosed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'consumerClosed',
|
||||
notification : true,
|
||||
peerName : 'alice',
|
||||
id : 3333,
|
||||
appData : 'mic broken'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAliceVideoConsumerClosed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'consumerClosed',
|
||||
notification : true,
|
||||
peerName : 'alice',
|
||||
id : 4444,
|
||||
appData : 'webcam broken'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAliceVideoConsumerPaused = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'consumerPaused',
|
||||
notification : true,
|
||||
peerName : 'alice',
|
||||
id : 4444,
|
||||
appData : 'webcam paused'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
window.notifyAliceVideoConsumerResumed = function()
|
||||
{
|
||||
const room = window.room;
|
||||
const notification =
|
||||
{
|
||||
method : 'consumerResumed',
|
||||
notification : true,
|
||||
peerName : 'alice',
|
||||
id : 4444,
|
||||
appData : 'webcam resumed'
|
||||
};
|
||||
|
||||
room.receiveNotification(notification);
|
||||
};
|
||||
|
||||
|
||||
// NOTE: Test pause/resume.
|
||||
|
||||
window.testPauseResume = function()
|
||||
{
|
||||
logger.debug('testPauseResume() with audioProducer1');
|
||||
|
||||
const producer = window.audioProducer1;
|
||||
|
||||
// producer.once('paused', () =>
|
||||
// {
|
||||
// producer.resume('I RESUME TO FUACK!!!');
|
||||
// });
|
||||
|
||||
logger.debug('testPauseResume() | (1) calling producer.pause()');
|
||||
|
||||
if (producer.pause('I PAUSE (1)'))
|
||||
{
|
||||
logger.warn(
|
||||
'testPauseResume() | (1) producer.pause() succeeded [locallyPaused:%s]',
|
||||
producer.locallyPaused);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error(
|
||||
'testPauseResume() | (1) producer.pause() failed [locallyPaused:%s]',
|
||||
producer.locallyPaused);
|
||||
}
|
||||
|
||||
logger.debug('testPauseResume() | (2) calling producer.pause()');
|
||||
|
||||
if (producer.pause('I PAUSE (2)'))
|
||||
{
|
||||
logger.warn(
|
||||
'testPauseResume() | (2) producer.pause() succeeded [locallyPaused:%s]',
|
||||
producer.locallyPaused);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error(
|
||||
'testPauseResume() | (2) producer.pause() failed [locallyPaused:%s]',
|
||||
producer.locallyPaused);
|
||||
}
|
||||
|
||||
logger.debug('testPauseResume() | (3) calling producer.resume()');
|
||||
|
||||
if (producer.resume('I RESUME (3)'))
|
||||
{
|
||||
logger.warn(
|
||||
'testPauseResume() | (3) producer.resume() succeeded [locallyPaused:%s]',
|
||||
producer.locallyPaused);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error(
|
||||
'testPauseResume() | (3) producer.resume() failed [locallyPaused:%s]',
|
||||
producer.locallyPaused);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// NOTE: For debugging.
|
||||
|
||||
window.dump1 = function()
|
||||
{
|
||||
const transport1 = window.transport1;
|
||||
const pc1 = transport1._handler._pc;
|
||||
|
||||
if (pc1 && pc1.localDescription)
|
||||
logger.warn('PC1 SEND LOCAL OFFER:\n%s', pc1.localDescription.sdp);
|
||||
|
||||
if (pc1 && pc1.remoteDescription)
|
||||
logger.warn('PC1 SEND REMOTE ANSWER:\n%s', pc1.remoteDescription.sdp);
|
||||
};
|
||||
|
||||
window.dump2 = function()
|
||||
{
|
||||
const transport2 = window.transport2;
|
||||
const pc2 = transport2._handler._pc;
|
||||
|
||||
if (pc2 && pc2.remoteDescription)
|
||||
logger.warn('PC2 RECV REMOTE OFFER:\n%s', pc2.remoteDescription.sdp);
|
||||
|
||||
if (pc2 && pc2.localDescription)
|
||||
logger.warn('PC2 RECV LOCAL ANSWER:\n%s', pc2.localDescription.sdp);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>mediasoup-client test</title>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
|
||||
<meta name='description' content='mediasoup-client test'>
|
||||
|
||||
<script async src='/mediasoup-client-test.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>mediasoup-client test</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
module.exports =
|
||||
{
|
||||
env:
|
||||
{
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends:
|
||||
[
|
||||
'eslint:recommended'
|
||||
],
|
||||
settings: {},
|
||||
parserOptions:
|
||||
{
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures:
|
||||
{
|
||||
impliedStrict: true
|
||||
}
|
||||
},
|
||||
rules:
|
||||
{
|
||||
'array-bracket-spacing': [ 2, 'always',
|
||||
{
|
||||
objectsInArrays: true,
|
||||
arraysInArrays: true
|
||||
}],
|
||||
'arrow-parens': [ 2, 'always' ],
|
||||
'arrow-spacing': 2,
|
||||
'block-spacing': [ 2, 'always' ],
|
||||
'brace-style': [ 2, 'allman', { allowSingleLine: true } ],
|
||||
'camelcase': 2,
|
||||
'comma-dangle': 2,
|
||||
'comma-spacing': [ 2, { before: false, after: true } ],
|
||||
'comma-style': 2,
|
||||
'computed-property-spacing': 2,
|
||||
'constructor-super': 2,
|
||||
'func-call-spacing': 2,
|
||||
'generator-star-spacing': 2,
|
||||
'guard-for-in': 2,
|
||||
'indent': [ 2, 'tab', { 'SwitchCase': 1 } ],
|
||||
'key-spacing': [ 2,
|
||||
{
|
||||
singleLine:
|
||||
{
|
||||
beforeColon: false,
|
||||
afterColon: true
|
||||
},
|
||||
multiLine:
|
||||
{
|
||||
beforeColon: true,
|
||||
afterColon: true,
|
||||
align: 'colon'
|
||||
}
|
||||
}],
|
||||
'keyword-spacing': 2,
|
||||
'linebreak-style': [ 2, 'unix' ],
|
||||
'lines-around-comment': [ 2,
|
||||
{
|
||||
allowBlockStart: true,
|
||||
allowObjectStart: true,
|
||||
beforeBlockComment: true,
|
||||
beforeLineComment: false
|
||||
}],
|
||||
'max-len': [ 2, 90,
|
||||
{
|
||||
tabWidth: 2,
|
||||
comments: 110,
|
||||
ignoreUrls: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreRegExpLiterals: true
|
||||
}],
|
||||
'newline-after-var': 2,
|
||||
'newline-before-return': 2,
|
||||
'newline-per-chained-call': 2,
|
||||
'no-alert': 2,
|
||||
'no-caller': 2,
|
||||
'no-case-declarations': 2,
|
||||
'no-catch-shadow': 2,
|
||||
'no-class-assign': 2,
|
||||
'no-confusing-arrow': 2,
|
||||
'no-console': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-debugger': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-div-regex': 2,
|
||||
'no-empty': [ 2, { allowEmptyCatch: true } ],
|
||||
'no-empty-pattern': 2,
|
||||
'no-else-return': 0,
|
||||
'no-eval': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-ex-assign': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-extra-boolean-cast': 2,
|
||||
'no-extra-label': 2,
|
||||
'no-extra-semi': 2,
|
||||
'no-fallthrough': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-global-assign': 2,
|
||||
'no-implicit-coercion': 2,
|
||||
'no-implicit-globals': 2,
|
||||
'no-inner-declarations': 2,
|
||||
'no-invalid-regexp': 2,
|
||||
'no-invalid-this': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-lonely-if': 2,
|
||||
'no-mixed-operators': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multi-spaces': 2,
|
||||
'no-multi-str': 2,
|
||||
'no-multiple-empty-lines': [ 2, { max: 1, maxEOF: 0, maxBOF: 0 } ],
|
||||
'no-native-reassign': 2,
|
||||
'no-negated-in-lhs': 2,
|
||||
'no-new': 2,
|
||||
'no-new-func': 2,
|
||||
'no-new-wrappers': 2,
|
||||
'no-obj-calls': 2,
|
||||
'no-proto': 2,
|
||||
'no-prototype-builtins': 0,
|
||||
'no-redeclare': 2,
|
||||
'no-regex-spaces': 2,
|
||||
'no-restricted-imports': 2,
|
||||
'no-return-assign': 2,
|
||||
'no-self-assign': 2,
|
||||
'no-self-compare': 2,
|
||||
'no-sequences': 2,
|
||||
'no-shadow': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'no-spaced-func': 2,
|
||||
'no-sparse-arrays': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-throw-literal': 2,
|
||||
'no-undef': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unmodified-loop-condition': 2,
|
||||
'no-unreachable': 2,
|
||||
'no-unused-vars': [ 1, { vars: 'all', args: 'after-used' }],
|
||||
'no-use-before-define': [ 2, { functions: false } ],
|
||||
'no-useless-call': 2,
|
||||
'no-useless-computed-key': 2,
|
||||
'no-useless-concat': 2,
|
||||
'no-useless-rename': 2,
|
||||
'no-var': 2,
|
||||
'no-whitespace-before-property': 2,
|
||||
'object-curly-newline': 0,
|
||||
'object-curly-spacing': [ 2, 'always' ],
|
||||
'object-property-newline': [ 2, { allowMultiplePropertiesPerLine: true } ],
|
||||
'prefer-const': 2,
|
||||
'prefer-rest-params': 2,
|
||||
'prefer-spread': 2,
|
||||
'prefer-template': 2,
|
||||
'quotes': [ 2, 'single', { avoidEscape: true } ],
|
||||
'semi': [ 2, 'always' ],
|
||||
'semi-spacing': 2,
|
||||
'space-before-blocks': 2,
|
||||
'space-before-function-paren': [ 2, 'never' ],
|
||||
'space-in-parens': [ 2, 'never' ],
|
||||
'spaced-comment': [ 2, 'always' ],
|
||||
'strict': 0,
|
||||
'valid-typeof': 2,
|
||||
'yoda': 2
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
module.exports =
|
||||
{
|
||||
// DEBUG env variable For the NPM debug module.
|
||||
debug : '*LOG* *WARN* *ERROR* *mediasoup-worker*',
|
||||
debug : '*INFO* *WARN* *ERROR* *mediasoup-worker*',
|
||||
// Listening hostname for `gulp live|open`.
|
||||
domain : 'localhost',
|
||||
tls :
|
||||
|
|
@ -9,24 +9,19 @@ module.exports =
|
|||
cert : `${__dirname}/certs/mediasoup-demo.localhost.cert.pem`,
|
||||
key : `${__dirname}/certs/mediasoup-demo.localhost.key.pem`
|
||||
},
|
||||
protoo :
|
||||
{
|
||||
listenIp : '0.0.0.0',
|
||||
listenPort : 3443
|
||||
},
|
||||
mediasoup :
|
||||
{
|
||||
// mediasoup Server settings.
|
||||
logLevel : 'debug',
|
||||
logLevel : 'warn',
|
||||
logTags :
|
||||
[
|
||||
'info',
|
||||
// 'ice',
|
||||
// 'dlts',
|
||||
'ice',
|
||||
'dlts'
|
||||
'rtp',
|
||||
// 'srtp',
|
||||
'srtp',
|
||||
'rtcp',
|
||||
// 'rbe',
|
||||
'rbe',
|
||||
'rtx'
|
||||
],
|
||||
rtcIPv4 : true,
|
||||
|
|
@ -35,32 +30,35 @@ module.exports =
|
|||
rtcAnnouncedIPv6 : null,
|
||||
rtcMinPort : 40000,
|
||||
rtcMaxPort : 49999,
|
||||
// mediasoup Room settings.
|
||||
roomCodecs :
|
||||
// mediasoup Room codecs.
|
||||
mediaCodecs :
|
||||
[
|
||||
{
|
||||
kind : 'audio',
|
||||
name : 'audio/opus',
|
||||
name : 'opus',
|
||||
clockRate : 48000,
|
||||
channels : 2,
|
||||
parameters :
|
||||
{
|
||||
useInbandFec : 1,
|
||||
minptime : 10
|
||||
useinbandfec : 1
|
||||
}
|
||||
},
|
||||
{
|
||||
kind : 'video',
|
||||
name : 'video/vp8',
|
||||
name : 'VP8',
|
||||
clockRate : 90000
|
||||
}
|
||||
// {
|
||||
// kind : 'video',
|
||||
// name : 'H264',
|
||||
// clockRate : 90000,
|
||||
// parameters :
|
||||
// {
|
||||
// 'packetization-mode' : 1
|
||||
// }
|
||||
// }
|
||||
],
|
||||
// mediasoup per Peer Transport settings.
|
||||
peerTransport :
|
||||
{
|
||||
udp : true,
|
||||
tcp : true
|
||||
},
|
||||
// mediasoup per Peer max sending bitrate (in kpbs).
|
||||
// mediasoup per Peer max sending bitrate (in bps).
|
||||
maxBitrate : 500000
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Tasks:
|
||||
*
|
||||
|
|
@ -19,63 +17,18 @@ const eslint = require('gulp-eslint');
|
|||
|
||||
gulp.task('lint', () =>
|
||||
{
|
||||
let src =
|
||||
const src =
|
||||
[
|
||||
'gulpfile.js',
|
||||
'server.js',
|
||||
'config.example.js',
|
||||
'config.js',
|
||||
'lib/**/*.js'
|
||||
];
|
||||
|
||||
return gulp.src(src)
|
||||
.pipe(plumber())
|
||||
.pipe(eslint(
|
||||
{
|
||||
extends : [ 'eslint:recommended' ],
|
||||
parserOptions :
|
||||
{
|
||||
ecmaVersion : 6,
|
||||
sourceType : 'module',
|
||||
ecmaFeatures :
|
||||
{
|
||||
impliedStrict : true
|
||||
}
|
||||
},
|
||||
envs :
|
||||
[
|
||||
'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' ],
|
||||
}
|
||||
}))
|
||||
.pipe(eslint())
|
||||
.pipe(eslint.format());
|
||||
});
|
||||
|
||||
gulp.task('watch', (done) =>
|
||||
{
|
||||
let src = [ 'gulpfile.js', 'server.js', 'config.js', 'lib/**/*.js' ];
|
||||
|
||||
gulp.watch(src, gulp.series(
|
||||
'lint'
|
||||
));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.series('lint', 'watch'));
|
||||
gulp.task('default', gulp.series('lint'));
|
||||
|
|
|
|||
|
|
@ -2,123 +2,53 @@
|
|||
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const protooServer = require('protoo-server');
|
||||
const webrtc = require('mediasoup').webrtc;
|
||||
const logger = require('./logger')('Room');
|
||||
const Logger = require('./Logger');
|
||||
const config = require('../config');
|
||||
|
||||
const MAX_BITRATE = config.mediasoup.maxBitrate || 3000000;
|
||||
const MIN_BITRATE = Math.min(50000 || MAX_BITRATE);
|
||||
const MAX_BITRATE = config.mediasoup.maxBitrate || 1000000;
|
||||
const MIN_BITRATE = Math.min(50000, MAX_BITRATE);
|
||||
const BITRATE_FACTOR = 0.75;
|
||||
const MIN_AUDIO_LEVEL = -50;
|
||||
|
||||
const logger = new Logger('Room');
|
||||
|
||||
class Room extends EventEmitter
|
||||
{
|
||||
constructor(roomId, mediaServer)
|
||||
{
|
||||
logger.log('constructor() [roomId:"%s"]', roomId);
|
||||
logger.info('constructor() [roomId:"%s"]', roomId);
|
||||
|
||||
super();
|
||||
this.setMaxListeners(Infinity);
|
||||
|
||||
// Room ID.
|
||||
this._roomId = roomId;
|
||||
|
||||
// Closed flag.
|
||||
this._closed = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Protoo Room instance.
|
||||
this._protooRoom = new protooServer.Room();
|
||||
|
||||
// mediasoup Room instance.
|
||||
this._mediaRoom = null;
|
||||
// Pending peers (this is because at the time we get the first peer, the
|
||||
// mediasoup room does not yet exist).
|
||||
this._pendingProtooPeers = [];
|
||||
this._mediaRoom = mediaServer.Room(config.mediasoup.mediaCodecs);
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
this.close();
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Current max bitrate for all the participants.
|
||||
this._maxBitrate = MAX_BITRATE;
|
||||
// Current active speaker mediasoup Peer.
|
||||
this._activeSpeaker = null;
|
||||
|
||||
// Create a mediasoup room.
|
||||
mediaServer.createRoom(
|
||||
{
|
||||
mediaCodecs : config.mediasoup.roomCodecs
|
||||
})
|
||||
.then((room) =>
|
||||
{
|
||||
logger.debug('mediasoup room created');
|
||||
// Current active speaker.
|
||||
// @type {mediasoup.Peer}
|
||||
this._currentActiveSpeaker = null;
|
||||
|
||||
this._mediaRoom = room;
|
||||
|
||||
process.nextTick(() =>
|
||||
{
|
||||
this._mediaRoom.on('newpeer', (peer) =>
|
||||
{
|
||||
this._updateMaxBitrate();
|
||||
|
||||
peer.on('close', () =>
|
||||
{
|
||||
this._updateMaxBitrate();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: FIX?
|
||||
this._mediaRoom.on('audiolevels', (entries) =>
|
||||
{
|
||||
logger.debug('room "audiolevels" event');
|
||||
|
||||
for (let entry of entries)
|
||||
{
|
||||
logger.debug('- [peer name:%s, rtpReceiver.id:%s, audio level:%s]',
|
||||
entry.peer.name, entry.rtpReceiver.id, entry.audioLevel);
|
||||
}
|
||||
|
||||
let activeSpeaker;
|
||||
let activeLevel;
|
||||
|
||||
if (entries.length > 0)
|
||||
{
|
||||
activeSpeaker = entries[0].peer;
|
||||
activeLevel = entries[0].audioLevel;
|
||||
|
||||
if (activeLevel < MIN_AUDIO_LEVEL)
|
||||
{
|
||||
activeSpeaker = null;
|
||||
activeLevel = undefined;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
activeSpeaker = null;
|
||||
}
|
||||
|
||||
if (this._activeSpeaker !== activeSpeaker)
|
||||
{
|
||||
let data = {};
|
||||
|
||||
if (activeSpeaker)
|
||||
{
|
||||
logger.debug('active speaker [peer:"%s", volume:%s]',
|
||||
activeSpeaker.name, activeLevel);
|
||||
|
||||
data.peer = { id: activeSpeaker.name };
|
||||
data.level = activeLevel;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug('no current speaker');
|
||||
|
||||
data.peer = null;
|
||||
}
|
||||
|
||||
this._protooRoom.spread('activespeaker', data);
|
||||
}
|
||||
|
||||
this._activeSpeaker = activeSpeaker;
|
||||
});
|
||||
});
|
||||
|
||||
// Run all the pending join requests.
|
||||
for (let protooPeer of this._pendingProtooPeers)
|
||||
{
|
||||
this._handleProtooPeer(protooPeer);
|
||||
}
|
||||
});
|
||||
this._handleMediaRoom();
|
||||
}
|
||||
|
||||
get id()
|
||||
|
|
@ -130,7 +60,10 @@ class Room extends EventEmitter
|
|||
{
|
||||
logger.debug('close()');
|
||||
|
||||
this._closed = true;
|
||||
|
||||
// Close the protoo Room.
|
||||
if (this._protooRoom)
|
||||
this._protooRoom.close();
|
||||
|
||||
// Close the mediasoup Room.
|
||||
|
|
@ -146,367 +79,409 @@ class Room extends EventEmitter
|
|||
if (!this._mediaRoom)
|
||||
return;
|
||||
|
||||
logger.log(
|
||||
logger.info(
|
||||
'logStatus() [room id:"%s", protoo peers:%s, mediasoup peers:%s]',
|
||||
this._roomId,
|
||||
this._protooRoom.peers.length,
|
||||
this._mediaRoom.peers.length);
|
||||
}
|
||||
|
||||
createProtooPeer(peerId, transport)
|
||||
handleConnection(peerName, transport)
|
||||
{
|
||||
logger.log('createProtooPeer() [peerId:"%s"]', peerId);
|
||||
logger.info('handleConnection() [peerName:"%s"]', peerName);
|
||||
|
||||
if (this._protooRoom.hasPeer(peerId))
|
||||
if (this._protooRoom.hasPeer(peerName))
|
||||
{
|
||||
logger.warn('createProtooPeer() | there is already a peer with same peerId, closing the previous one [peerId:"%s"]', peerId);
|
||||
logger.warn(
|
||||
'handleConnection() | there is already a peer with same peerName, ' +
|
||||
'closing the previous one [peerName:"%s"]',
|
||||
peerName);
|
||||
|
||||
let protooPeer = this._protooRoom.getPeer(peerId);
|
||||
const protooPeer = this._protooRoom.getPeer(peerName);
|
||||
|
||||
protooPeer.close();
|
||||
}
|
||||
|
||||
return this._protooRoom.createPeer(peerId, transport)
|
||||
.then((protooPeer) =>
|
||||
{
|
||||
if (this._mediaRoom)
|
||||
const protooPeer = this._protooRoom.createPeer(peerName, transport);
|
||||
|
||||
this._handleProtooPeer(protooPeer);
|
||||
}
|
||||
|
||||
_handleMediaRoom()
|
||||
{
|
||||
logger.debug('_handleMediaRoom()');
|
||||
|
||||
const activeSpeakerDetector = this._mediaRoom.createActiveSpeakerDetector();
|
||||
|
||||
activeSpeakerDetector.on('activespeakerchange', (activePeer) =>
|
||||
{
|
||||
if (activePeer)
|
||||
{
|
||||
logger.info('new active speaker [peerName:"%s"]', activePeer.name);
|
||||
|
||||
this._currentActiveSpeaker = activePeer;
|
||||
|
||||
const activeVideoProducer = activePeer.producers
|
||||
.find((producer) => producer.kind === 'video');
|
||||
|
||||
for (const peer of this._mediaRoom.peers)
|
||||
{
|
||||
for (const consumer of peer.consumers)
|
||||
{
|
||||
if (consumer.kind !== 'video')
|
||||
continue;
|
||||
|
||||
if (consumer.source === activeVideoProducer)
|
||||
{
|
||||
consumer.setPreferredProfile('high');
|
||||
}
|
||||
else
|
||||
this._pendingProtooPeers.push(protooPeer);
|
||||
{
|
||||
consumer.setPreferredProfile('low');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.info('no active speaker');
|
||||
|
||||
this._currentActiveSpeaker = null;
|
||||
|
||||
for (const peer of this._mediaRoom.peers)
|
||||
{
|
||||
for (const consumer of peer.consumers)
|
||||
{
|
||||
if (consumer.kind !== 'video')
|
||||
continue;
|
||||
|
||||
consumer.setPreferredProfile('low');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spread to others via protoo.
|
||||
this._protooRoom.spread(
|
||||
'active-speaker',
|
||||
{
|
||||
peerName : activePeer ? activePeer.name : null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_handleProtooPeer(protooPeer)
|
||||
{
|
||||
logger.debug('_handleProtooPeer() [peerId:"%s"]', protooPeer.id);
|
||||
logger.debug('_handleProtooPeer() [peer:"%s"]', protooPeer.id);
|
||||
|
||||
let mediaPeer = this._mediaRoom.Peer(protooPeer.id);
|
||||
let peerconnection;
|
||||
|
||||
protooPeer.data.msids = [];
|
||||
|
||||
protooPeer.on('close', () =>
|
||||
{
|
||||
logger.debug('protoo Peer "close" event [peerId:"%s"]', protooPeer.id);
|
||||
|
||||
this._protooRoom.spread(
|
||||
'removepeer',
|
||||
{
|
||||
peer :
|
||||
{
|
||||
id : protooPeer.id,
|
||||
msids : protooPeer.data.msids
|
||||
}
|
||||
});
|
||||
|
||||
// Close the media stuff.
|
||||
if (peerconnection)
|
||||
peerconnection.close();
|
||||
else
|
||||
mediaPeer.close();
|
||||
|
||||
// If this is the latest peer in the room, close the room.
|
||||
// However, wait a bit (for reconnections).
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (this._mediaRoom && this._mediaRoom.closed)
|
||||
return;
|
||||
|
||||
if (this._protooRoom.peers.length === 0)
|
||||
{
|
||||
logger.log(
|
||||
'last peer in the room left, closing the room [roomId:"%s"]',
|
||||
this._roomId);
|
||||
|
||||
this.close();
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
Promise.resolve()
|
||||
// Send 'join' request to the new peer.
|
||||
.then(() =>
|
||||
{
|
||||
return protooPeer.send(
|
||||
'joinme',
|
||||
{
|
||||
peerId : protooPeer.id,
|
||||
roomId : this.id
|
||||
});
|
||||
})
|
||||
// Create a RTCPeerConnection instance and set media capabilities.
|
||||
.then((data) =>
|
||||
{
|
||||
peerconnection = new webrtc.RTCPeerConnection(
|
||||
{
|
||||
peer : mediaPeer,
|
||||
usePlanB : !!data.usePlanB,
|
||||
transportOptions : config.mediasoup.peerTransport,
|
||||
maxBitrate : this._maxBitrate
|
||||
});
|
||||
|
||||
// Store the RTCPeerConnection instance within the protoo Peer.
|
||||
protooPeer.data.peerconnection = peerconnection;
|
||||
|
||||
mediaPeer.on('newtransport', (transport) =>
|
||||
{
|
||||
transport.on('iceselectedtuplechange', (data) =>
|
||||
{
|
||||
logger.log('"iceselectedtuplechange" event [peerId:"%s", protocol:%s, remoteIP:%s, remotePort:%s]',
|
||||
protooPeer.id, data.protocol, data.remoteIP, data.remotePort);
|
||||
});
|
||||
});
|
||||
|
||||
// Set RTCPeerConnection capabilities.
|
||||
return peerconnection.setCapabilities(data.capabilities);
|
||||
})
|
||||
// Send 'peers' request for the new peer to know about the existing peers.
|
||||
.then(() =>
|
||||
{
|
||||
return protooPeer.send(
|
||||
'peers',
|
||||
{
|
||||
peers : this._protooRoom.peers
|
||||
// Filter this protoo Peer.
|
||||
.filter((peer) =>
|
||||
{
|
||||
return peer !== protooPeer;
|
||||
})
|
||||
.map((peer) =>
|
||||
{
|
||||
return {
|
||||
id : peer.id,
|
||||
msids : peer.data.msids
|
||||
};
|
||||
})
|
||||
});
|
||||
})
|
||||
// Tell all the other peers about the new peer.
|
||||
.then(() =>
|
||||
{
|
||||
this._protooRoom.spread(
|
||||
'addpeer',
|
||||
{
|
||||
peer :
|
||||
{
|
||||
id : protooPeer.id,
|
||||
msids : protooPeer.data.msids
|
||||
}
|
||||
},
|
||||
[ protooPeer ]);
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
// Send initial SDP offer.
|
||||
return this._sendOffer(protooPeer,
|
||||
{
|
||||
offerToReceiveAudio : 1,
|
||||
offerToReceiveVideo : 1
|
||||
});
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
// Handle PeerConnection events.
|
||||
peerconnection.on('negotiationneeded', () =>
|
||||
{
|
||||
logger.debug('"negotiationneeded" event [peerId:"%s"]', protooPeer.id);
|
||||
|
||||
// Send SDP re-offer.
|
||||
this._sendOffer(protooPeer);
|
||||
});
|
||||
|
||||
peerconnection.on('signalingstatechange', () =>
|
||||
{
|
||||
logger.debug('"signalingstatechange" event [peerId:"%s", signalingState:%s]',
|
||||
protooPeer.id, peerconnection.signalingState);
|
||||
});
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
protooPeer.on('request', (request, accept, reject) =>
|
||||
{
|
||||
logger.debug('protoo Peer "request" event [method:%s]', request.method);
|
||||
logger.debug(
|
||||
'protoo "request" event [method:%s, peer:"%s"]',
|
||||
request.method, protooPeer.id);
|
||||
|
||||
switch(request.method)
|
||||
switch (request.method)
|
||||
{
|
||||
case 'reofferme':
|
||||
case 'mediasoup-request':
|
||||
{
|
||||
accept();
|
||||
this._sendOffer(protooPeer);
|
||||
const mediasoupRequest = request.data;
|
||||
|
||||
this._handleMediasoupClientRequest(
|
||||
protooPeer, mediasoupRequest, accept, reject);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'restartice':
|
||||
{
|
||||
peerconnection.restartIce()
|
||||
.then(() =>
|
||||
case 'mediasoup-notification':
|
||||
{
|
||||
accept();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('"restartice" request failed: %s', error);
|
||||
logger.error('stack:\n' + error.stack);
|
||||
|
||||
reject(500, `"restartice" failed: ${error.message}`);
|
||||
});
|
||||
const mediasoupNotification = request.data;
|
||||
|
||||
this._handleMediasoupClientNotification(
|
||||
protooPeer, mediasoupNotification);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'disableremotevideo':
|
||||
case 'change-display-name':
|
||||
{
|
||||
let videoMsid = request.data.msid;
|
||||
let disable = request.data.disable;
|
||||
let videoRtpSender;
|
||||
|
||||
for (let rtpSender of mediaPeer.rtpSenders)
|
||||
{
|
||||
if (rtpSender.kind !== 'video')
|
||||
continue;
|
||||
|
||||
let msid = rtpSender.rtpParameters.userParameters.msid.split(/\s/)[0];
|
||||
|
||||
if (msid === videoMsid)
|
||||
{
|
||||
videoRtpSender = rtpSender;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (videoRtpSender)
|
||||
{
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
if (disable)
|
||||
return videoRtpSender.disable({ emit: false });
|
||||
else
|
||||
return videoRtpSender.enable({ emit: false });
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
logger.log('"disableremotevideo" request succeed [disable:%s]',
|
||||
!!disable);
|
||||
|
||||
accept();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('"disableremotevideo" request failed: %s', error);
|
||||
logger.error('stack:\n' + error.stack);
|
||||
|
||||
reject(500, `"disableremotevideo" failed: ${error.message}`);
|
||||
});
|
||||
}
|
||||
else
|
||||
const { displayName } = request.data;
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
const oldDisplayName = mediaPeer.appData.displayName;
|
||||
|
||||
mediaPeer.appData.displayName = displayName;
|
||||
|
||||
// Spread to others via protoo.
|
||||
this._protooRoom.spread(
|
||||
'display-name-changed',
|
||||
{
|
||||
reject(404, 'msid not found');
|
||||
}
|
||||
peerName : protooPeer.id,
|
||||
displayName : displayName,
|
||||
oldDisplayName : oldDisplayName
|
||||
},
|
||||
[ protooPeer ]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
logger.error('unknown method');
|
||||
logger.error('unknown request.method "%s"', request.method);
|
||||
|
||||
reject(404, 'unknown method');
|
||||
reject(400, `unknown request.method "${request.method}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
protooPeer.on('close', () =>
|
||||
{
|
||||
logger.debug('protoo Peer "close" event [peer:"%s"]', protooPeer.id);
|
||||
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
|
||||
if (mediaPeer && !mediaPeer.closed)
|
||||
mediaPeer.close();
|
||||
|
||||
// If this is the latest peer in the room, close the room.
|
||||
// However wait a bit (for reconnections).
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (this._mediaRoom && this._mediaRoom.closed)
|
||||
return;
|
||||
|
||||
if (this._mediaRoom.peers.length === 0)
|
||||
{
|
||||
logger.info(
|
||||
'last peer in the room left, closing the room [roomId:"%s"]',
|
||||
this._roomId);
|
||||
|
||||
this.close();
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
_handleMediaPeer(protooPeer, mediaPeer)
|
||||
{
|
||||
mediaPeer.on('notify', (notification) =>
|
||||
{
|
||||
protooPeer.send('mediasoup-notification', notification)
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
mediaPeer.on('newtransport', (transport) =>
|
||||
{
|
||||
logger.info(
|
||||
'mediaPeer "newtransport" event [id:%s, direction:%s]',
|
||||
transport.id, transport.direction);
|
||||
|
||||
// Update peers max sending bitrate.
|
||||
if (transport.direction === 'send')
|
||||
{
|
||||
this._updateMaxBitrate();
|
||||
|
||||
transport.on('close', () =>
|
||||
{
|
||||
this._updateMaxBitrate();
|
||||
});
|
||||
}
|
||||
|
||||
this._handleMediaTransport(transport);
|
||||
});
|
||||
|
||||
mediaPeer.on('newproducer', (producer) =>
|
||||
{
|
||||
logger.info('mediaPeer "newproducer" event [id:%s]', producer.id);
|
||||
|
||||
this._handleMediaProducer(producer);
|
||||
});
|
||||
|
||||
mediaPeer.on('newconsumer', (consumer) =>
|
||||
{
|
||||
logger.info('mediaPeer "newconsumer" event [id:%s]', consumer.id);
|
||||
|
||||
this._handleMediaConsumer(consumer);
|
||||
});
|
||||
|
||||
// Also handle already existing Consumers.
|
||||
for (const consumer of mediaPeer.consumers)
|
||||
{
|
||||
logger.info('mediaPeer existing "consumer" [id:%s]', consumer.id);
|
||||
|
||||
this._handleMediaConsumer(consumer);
|
||||
}
|
||||
|
||||
// Notify about the existing active speaker.
|
||||
if (this._currentActiveSpeaker)
|
||||
{
|
||||
protooPeer.send(
|
||||
'active-speaker',
|
||||
{
|
||||
peerName : this._currentActiveSpeaker.name
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
_handleMediaTransport(transport)
|
||||
{
|
||||
transport.on('close', (originator) =>
|
||||
{
|
||||
logger.info(
|
||||
'Transport "close" event [originator:%s]', originator);
|
||||
});
|
||||
}
|
||||
|
||||
_handleMediaProducer(producer)
|
||||
{
|
||||
producer.on('close', (originator) =>
|
||||
{
|
||||
logger.info(
|
||||
'Producer "close" event [originator:%s]', originator);
|
||||
});
|
||||
|
||||
producer.on('pause', (originator) =>
|
||||
{
|
||||
logger.info(
|
||||
'Producer "pause" event [originator:%s]', originator);
|
||||
});
|
||||
|
||||
producer.on('resume', (originator) =>
|
||||
{
|
||||
logger.info(
|
||||
'Producer "resume" event [originator:%s]', originator);
|
||||
});
|
||||
}
|
||||
|
||||
_handleMediaConsumer(consumer)
|
||||
{
|
||||
consumer.on('close', (originator) =>
|
||||
{
|
||||
logger.info(
|
||||
'Consumer "close" event [originator:%s]', originator);
|
||||
});
|
||||
|
||||
consumer.on('pause', (originator) =>
|
||||
{
|
||||
logger.info(
|
||||
'Consumer "pause" event [originator:%s]', originator);
|
||||
});
|
||||
|
||||
consumer.on('resume', (originator) =>
|
||||
{
|
||||
logger.info(
|
||||
'Consumer "resume" event [originator:%s]', originator);
|
||||
});
|
||||
|
||||
consumer.on('effectiveprofilechange', (profile) =>
|
||||
{
|
||||
logger.info(
|
||||
'Consumer "effectiveprofilechange" event [profile:%s]', profile);
|
||||
});
|
||||
|
||||
// If video, initially make it 'low' profile unless this is for the current
|
||||
// active speaker.
|
||||
if (consumer.kind === 'video' && consumer.peer !== this._currentActiveSpeaker)
|
||||
consumer.setPreferredProfile('low');
|
||||
}
|
||||
|
||||
_handleMediasoupClientRequest(protooPeer, request, accept, reject)
|
||||
{
|
||||
logger.debug(
|
||||
'mediasoup-client request [method:%s, peer:"%s"]',
|
||||
request.method, protooPeer.id);
|
||||
|
||||
switch (request.method)
|
||||
{
|
||||
case 'queryRoom':
|
||||
{
|
||||
this._mediaRoom.receiveRequest(request)
|
||||
.then((response) => accept(response))
|
||||
.catch((error) => reject(500, error.toString()));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'join':
|
||||
{
|
||||
// TODO: Handle appData. Yes?
|
||||
const { peerName } = request;
|
||||
|
||||
if (peerName !== protooPeer.id)
|
||||
{
|
||||
reject(403, 'that is not your corresponding mediasoup Peer name');
|
||||
|
||||
break;
|
||||
}
|
||||
else if (protooPeer.data.mediaPeer)
|
||||
{
|
||||
reject(500, 'already have a mediasoup Peer');
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this._mediaRoom.receiveRequest(request)
|
||||
.then((response) =>
|
||||
{
|
||||
accept(response);
|
||||
|
||||
// Get the newly created mediasoup Peer.
|
||||
const mediaPeer = this._mediaRoom.getPeerByName(peerName);
|
||||
|
||||
protooPeer.data.mediaPeer = mediaPeer;
|
||||
|
||||
this._handleMediaPeer(protooPeer, mediaPeer);
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('_handleProtooPeer() failed: %s', error.message);
|
||||
logger.error('stack:\n' + error.stack);
|
||||
|
||||
protooPeer.close();
|
||||
});
|
||||
}
|
||||
|
||||
_sendOffer(protooPeer, options)
|
||||
{
|
||||
logger.debug('_sendOffer() [peerId:"%s"]', protooPeer.id);
|
||||
|
||||
let peerconnection = protooPeer.data.peerconnection;
|
||||
let mediaPeer = peerconnection.peer;
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
{
|
||||
return peerconnection.createOffer(options);
|
||||
})
|
||||
.then((desc) =>
|
||||
{
|
||||
return peerconnection.setLocalDescription(desc);
|
||||
})
|
||||
// Send the SDP offer to the peer.
|
||||
.then(() =>
|
||||
{
|
||||
return protooPeer.send(
|
||||
'offer',
|
||||
{
|
||||
offer : peerconnection.localDescription.serialize()
|
||||
});
|
||||
})
|
||||
// Process the SDP answer from the peer.
|
||||
.then((data) =>
|
||||
{
|
||||
let answer = data.answer;
|
||||
|
||||
return peerconnection.setRemoteDescription(answer);
|
||||
})
|
||||
.then(() =>
|
||||
{
|
||||
let oldMsids = protooPeer.data.msids;
|
||||
|
||||
// Reset peer's msids.
|
||||
protooPeer.data.msids = [];
|
||||
|
||||
let setMsids = new Set();
|
||||
|
||||
// Update peer's msids information.
|
||||
for (let rtpReceiver of mediaPeer.rtpReceivers)
|
||||
{
|
||||
let msid = rtpReceiver.rtpParameters.userParameters.msid.split(/\s/)[0];
|
||||
|
||||
setMsids.add(msid);
|
||||
}
|
||||
|
||||
protooPeer.data.msids = Array.from(setMsids);
|
||||
|
||||
// If msids changed, notify.
|
||||
let sameValues = (
|
||||
oldMsids.length == protooPeer.data.msids.length) &&
|
||||
oldMsids.every((element, index) =>
|
||||
{
|
||||
return element === protooPeer.data.msids[index];
|
||||
reject(500, error.toString());
|
||||
});
|
||||
|
||||
if (!sameValues)
|
||||
{
|
||||
this._protooRoom.spread(
|
||||
'updatepeer',
|
||||
{
|
||||
peer :
|
||||
{
|
||||
id : protooPeer.id,
|
||||
msids : protooPeer.data.msids
|
||||
break;
|
||||
}
|
||||
},
|
||||
[ protooPeer ]);
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('_sendOffer() failed: %s', error);
|
||||
logger.error('stack:\n' + error.stack);
|
||||
|
||||
logger.warn('resetting peerconnection');
|
||||
peerconnection.reset();
|
||||
});
|
||||
default:
|
||||
{
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
|
||||
if (!mediaPeer)
|
||||
{
|
||||
logger.error(
|
||||
'cannot handle mediasoup request, no mediasoup Peer [method:"%s"]',
|
||||
request.method);
|
||||
|
||||
reject(400, 'no mediasoup Peer');
|
||||
}
|
||||
|
||||
mediaPeer.receiveRequest(request)
|
||||
.then((response) => accept(response))
|
||||
.catch((error) => reject(500, error.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleMediasoupClientNotification(protooPeer, notification)
|
||||
{
|
||||
logger.debug(
|
||||
'mediasoup-client notification [method:%s, peer:"%s"]',
|
||||
notification.method, protooPeer.id);
|
||||
|
||||
// NOTE: mediasoup-client just sends notifications with target 'peer',
|
||||
// so first of all, get the mediasoup Peer.
|
||||
const { mediaPeer } = protooPeer.data;
|
||||
|
||||
if (!mediaPeer)
|
||||
{
|
||||
logger.error(
|
||||
'cannot handle mediasoup notification, no mediasoup Peer [method:"%s"]',
|
||||
notification.method);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
mediaPeer.receiveNotification(notification);
|
||||
}
|
||||
|
||||
_updateMaxBitrate()
|
||||
|
|
@ -514,8 +489,8 @@ class Room extends EventEmitter
|
|||
if (this._mediaRoom.closed)
|
||||
return;
|
||||
|
||||
let numPeers = this._mediaRoom.peers.length;
|
||||
let previousMaxBitrate = this._maxBitrate;
|
||||
const numPeers = this._mediaRoom.peers.length;
|
||||
const previousMaxBitrate = this._maxBitrate;
|
||||
let newMaxBitrate;
|
||||
|
||||
if (numPeers <= 2)
|
||||
|
|
@ -530,29 +505,28 @@ class Room extends EventEmitter
|
|||
newMaxBitrate = MIN_BITRATE;
|
||||
}
|
||||
|
||||
if (newMaxBitrate === previousMaxBitrate)
|
||||
return;
|
||||
this._maxBitrate = newMaxBitrate;
|
||||
|
||||
for (let peer of this._mediaRoom.peers)
|
||||
for (const peer of this._mediaRoom.peers)
|
||||
{
|
||||
if (!peer.capabilities || peer.closed)
|
||||
continue;
|
||||
|
||||
for (let transport of peer.transports)
|
||||
for (const transport of peer.transports)
|
||||
{
|
||||
if (transport.closed)
|
||||
continue;
|
||||
|
||||
transport.setMaxBitrate(newMaxBitrate);
|
||||
if (transport.direction === 'send')
|
||||
{
|
||||
transport.setMaxBitrate(newMaxBitrate)
|
||||
.catch((error) =>
|
||||
{
|
||||
logger.error('transport.setMaxBitrate() failed: %s', String(error));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('_updateMaxBitrate() [num peers:%s, before:%skbps, now:%skbps]',
|
||||
logger.info(
|
||||
'_updateMaxBitrate() [num peers:%s, before:%skbps, now:%skbps]',
|
||||
numPeers,
|
||||
Math.round(previousMaxBitrate / 1000),
|
||||
Math.round(newMaxBitrate / 1000));
|
||||
|
||||
this._maxBitrate = newMaxBitrate;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const debug = require('debug');
|
||||
|
||||
const NAMESPACE = 'mediasoup-demo-server';
|
||||
const APP_NAME = 'mediasoup-demo-server';
|
||||
|
||||
class Logger
|
||||
{
|
||||
|
|
@ -10,23 +10,25 @@ class Logger
|
|||
{
|
||||
if (prefix)
|
||||
{
|
||||
this._debug = debug(NAMESPACE + ':' + prefix);
|
||||
this._log = debug(NAMESPACE + ':LOG:' + prefix);
|
||||
this._warn = debug(NAMESPACE + ':WARN:' + prefix);
|
||||
this._error = debug(NAMESPACE + ':ERROR:' + prefix);
|
||||
this._debug = debug(`${APP_NAME}:${prefix}`);
|
||||
this._info = debug(`${APP_NAME}:INFO:${prefix}`);
|
||||
this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
|
||||
this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._debug = debug(NAMESPACE);
|
||||
this._log = debug(NAMESPACE + ':LOG');
|
||||
this._warn = debug(NAMESPACE + ':WARN');
|
||||
this._error = debug(NAMESPACE + ':ERROR');
|
||||
this._debug = debug(APP_NAME);
|
||||
this._info = debug(`${APP_NAME}:INFO`);
|
||||
this._warn = debug(`${APP_NAME}:WARN`);
|
||||
this._error = debug(`${APP_NAME}:ERROR`);
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
this._debug.log = console.info.bind(console);
|
||||
this._log.log = console.info.bind(console);
|
||||
this._info.log = console.info.bind(console);
|
||||
this._warn.log = console.warn.bind(console);
|
||||
this._error.log = console.error.bind(console);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
get debug()
|
||||
|
|
@ -34,9 +36,9 @@ class Logger
|
|||
return this._debug;
|
||||
}
|
||||
|
||||
get log()
|
||||
get info()
|
||||
{
|
||||
return this._log;
|
||||
return this._info;
|
||||
}
|
||||
|
||||
get warn()
|
||||
|
|
@ -50,7 +52,4 @@ class Logger
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = function(prefix)
|
||||
{
|
||||
return new Logger(prefix);
|
||||
};
|
||||
module.exports = Logger;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mediasoup-demo-server",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "mediasoup demo server",
|
||||
"author": "Iñaki Baz Castillo <ibc@aliax.net>",
|
||||
|
|
@ -8,16 +8,12 @@
|
|||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"debug": "^2.6.8",
|
||||
"express": "^4.15.3",
|
||||
"mediasoup": "^1.2.5",
|
||||
"protoo-server": "^1.1.4"
|
||||
"debug": "^3.1.0",
|
||||
"express": "^4.16.2",
|
||||
"mediasoup": "^2.0.0",
|
||||
"protoo-server": "^2.0.5"
|
||||
},
|
||||
"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",
|
||||
"gulp": "git://github.com/gulpjs/gulp.git#4.0",
|
||||
"gulp-eslint": "^4.0.0",
|
||||
"gulp-plumber": "^1.1.0"
|
||||
|
|
|
|||
236
server/server.js
|
|
@ -6,11 +6,13 @@ process.title = 'mediasoup-demo-server';
|
|||
|
||||
const config = require('./config');
|
||||
|
||||
process.env.DEBUG = config.debug || '*LOG* *WARN* *ERROR*';
|
||||
process.env.DEBUG = config.debug || '*INFO* *WARN* *ERROR*';
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log('- process.env.DEBUG:', process.env.DEBUG);
|
||||
console.log('- config.mediasoup.logLevel:', config.mediasoup.logLevel);
|
||||
console.log('- config.mediasoup.logTags:', config.mediasoup.logTags);
|
||||
/* eslint-enable no-console */
|
||||
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
|
@ -20,14 +22,16 @@ const mediasoup = require('mediasoup');
|
|||
const readline = require('readline');
|
||||
const colors = require('colors/safe');
|
||||
const repl = require('repl');
|
||||
const logger = require('./lib/logger')();
|
||||
const Logger = require('./lib/Logger');
|
||||
const Room = require('./lib/Room');
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
// Map of Room instances indexed by roomId.
|
||||
let rooms = new Map();
|
||||
const rooms = new Map();
|
||||
|
||||
// mediasoup server.
|
||||
let mediaServer = mediasoup.Server(
|
||||
const mediaServer = mediasoup.Server(
|
||||
{
|
||||
numWorkers : 1,
|
||||
logLevel : config.mediasoup.logLevel,
|
||||
|
|
@ -41,30 +45,55 @@ let mediaServer = mediasoup.Server(
|
|||
});
|
||||
|
||||
global.SERVER = mediaServer;
|
||||
|
||||
mediaServer.on('newroom', (room) =>
|
||||
{
|
||||
global.ROOM = room;
|
||||
|
||||
room.on('newpeer', (peer) =>
|
||||
{
|
||||
global.PEER = peer;
|
||||
|
||||
if (peer.consumers.length > 0)
|
||||
global.CONSUMER = peer.consumers[peer.consumers.length - 1];
|
||||
|
||||
peer.on('newtransport', (transport) =>
|
||||
{
|
||||
global.TRANSPORT = transport;
|
||||
});
|
||||
|
||||
peer.on('newproducer', (producer) =>
|
||||
{
|
||||
global.PRODUCER = producer;
|
||||
});
|
||||
|
||||
peer.on('newconsumer', (consumer) =>
|
||||
{
|
||||
global.CONSUMER = consumer;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// HTTPS server for the protoo WebSocjet server.
|
||||
let tls =
|
||||
const tls =
|
||||
{
|
||||
cert : fs.readFileSync(config.tls.cert),
|
||||
key : fs.readFileSync(config.tls.key)
|
||||
};
|
||||
let httpsServer = https.createServer(tls, (req, res) =>
|
||||
{
|
||||
|
||||
const httpsServer = https.createServer(tls, (req, res) =>
|
||||
{
|
||||
res.writeHead(404, 'Not Here');
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
|
||||
httpsServer.listen(config.protoo.listenPort, config.protoo.listenIp, () =>
|
||||
httpsServer.listen(3443, '0.0.0.0', () =>
|
||||
{
|
||||
logger.log('protoo WebSocket server running');
|
||||
logger.info('protoo WebSocket server running');
|
||||
});
|
||||
|
||||
// Protoo WebSocket server.
|
||||
let webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
||||
const webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
||||
{
|
||||
maxReceivedFrameSize : 960000, // 960 KBytes.
|
||||
maxReceivedMessageSize : 960000,
|
||||
|
|
@ -76,30 +105,48 @@ let webSocketServer = new protooServer.WebSocketServer(httpsServer,
|
|||
webSocketServer.on('connectionrequest', (info, accept, reject) =>
|
||||
{
|
||||
// The client indicates the roomId and peerId in the URL query.
|
||||
let u = url.parse(info.request.url, true);
|
||||
let roomId = u.query['room-id'];
|
||||
let peerId = u.query['peer-id'];
|
||||
const u = url.parse(info.request.url, true);
|
||||
const roomId = u.query['roomId'];
|
||||
const peerName = u.query['peerName'];
|
||||
|
||||
if (!roomId || !peerId)
|
||||
if (!roomId || !peerName)
|
||||
{
|
||||
logger.warn('connection request without roomId and/or peerId');
|
||||
logger.warn('connection request without roomId and/or peerName');
|
||||
|
||||
reject(400, 'Connection request without roomId and/or peerName');
|
||||
|
||||
reject(400, 'Connection request without roomId and/or peerId');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('connection request [roomId:"%s", peerId:"%s"]', roomId, peerId);
|
||||
logger.info(
|
||||
'connection request [roomId:"%s", peerName:"%s"]', roomId, peerName);
|
||||
|
||||
let room;
|
||||
|
||||
// If an unknown roomId, create a new Room.
|
||||
if (!rooms.has(roomId))
|
||||
{
|
||||
logger.debug('creating a new Room [roomId:"%s"]', roomId);
|
||||
logger.info('creating a new Room [roomId:"%s"]', roomId);
|
||||
|
||||
let room = new Room(roomId, mediaServer);
|
||||
let logStatusTimer = setInterval(() =>
|
||||
try
|
||||
{
|
||||
room = new Room(roomId, mediaServer);
|
||||
|
||||
global.APP_ROOM = room;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
logger.error('error creating a new Room: %s', error);
|
||||
|
||||
reject(error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const logStatusTimer = setInterval(() =>
|
||||
{
|
||||
room.logStatus();
|
||||
}, 10000);
|
||||
}, 30000);
|
||||
|
||||
rooms.set(roomId, room);
|
||||
|
||||
|
|
@ -109,15 +156,14 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
|
|||
clearInterval(logStatusTimer);
|
||||
});
|
||||
}
|
||||
|
||||
let room = rooms.get(roomId);
|
||||
let transport = accept();
|
||||
|
||||
room.createProtooPeer(peerId, transport)
|
||||
.catch((error) =>
|
||||
else
|
||||
{
|
||||
logger.error('error creating a protoo peer: %s', error);
|
||||
});
|
||||
room = rooms.get(roomId);
|
||||
}
|
||||
|
||||
const transport = accept();
|
||||
|
||||
room.handleConnection(peerName, transport);
|
||||
});
|
||||
|
||||
// Listen for keyboard input.
|
||||
|
|
@ -125,6 +171,8 @@ webSocketServer.on('connectionrequest', (info, accept, reject) =>
|
|||
let cmd;
|
||||
let terminal;
|
||||
|
||||
openCommandConsole();
|
||||
|
||||
function openCommandConsole()
|
||||
{
|
||||
stdinLog('[opening Readline Command Console...]');
|
||||
|
|
@ -165,6 +213,10 @@ function openCommandConsole()
|
|||
stdinLog('- h, help : show this message');
|
||||
stdinLog('- sd, serverdump : execute server.dump()');
|
||||
stdinLog('- rd, roomdump : execute room.dump() for the latest created mediasoup Room');
|
||||
stdinLog('- pd, peerdump : execute peer.dump() for the latest created mediasoup Peer');
|
||||
stdinLog('- td, transportdump : execute transport.dump() for the latest created mediasoup Transport');
|
||||
stdinLog('- prd, producerdump : execute producer.dump() for the latest created mediasoup Producer');
|
||||
stdinLog('- cd, consumerdump : execute consumer.dump() for the latest created mediasoup Consumer');
|
||||
stdinLog('- t, terminal : open REPL Terminal');
|
||||
stdinLog('');
|
||||
readStdin();
|
||||
|
|
@ -178,7 +230,7 @@ function openCommandConsole()
|
|||
mediaServer.dump()
|
||||
.then((data) =>
|
||||
{
|
||||
stdinLog(`mediaServer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||
stdinLog(`server.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||
readStdin();
|
||||
})
|
||||
.catch((error) =>
|
||||
|
|
@ -202,14 +254,108 @@ function openCommandConsole()
|
|||
global.ROOM.dump()
|
||||
.then((data) =>
|
||||
{
|
||||
stdinLog('global.ROOM.dump() succeeded');
|
||||
stdinLog(`- peers:\n${JSON.stringify(data.peers, null, ' ')}`);
|
||||
stdinLog(`- num peers: ${data.peers.length}`);
|
||||
stdinLog(`room.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||
readStdin();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
stdinError(`global.ROOM.dump() failed: ${error}`);
|
||||
stdinError(`room.dump() failed: ${error}`);
|
||||
readStdin();
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'pd':
|
||||
case 'peerdump':
|
||||
{
|
||||
if (!global.PEER)
|
||||
{
|
||||
readStdin();
|
||||
break;
|
||||
}
|
||||
|
||||
global.PEER.dump()
|
||||
.then((data) =>
|
||||
{
|
||||
stdinLog(`peer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||
readStdin();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
stdinError(`peer.dump() failed: ${error}`);
|
||||
readStdin();
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'td':
|
||||
case 'transportdump':
|
||||
{
|
||||
if (!global.TRANSPORT)
|
||||
{
|
||||
readStdin();
|
||||
break;
|
||||
}
|
||||
|
||||
global.TRANSPORT.dump()
|
||||
.then((data) =>
|
||||
{
|
||||
stdinLog(`transport.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||
readStdin();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
stdinError(`transport.dump() failed: ${error}`);
|
||||
readStdin();
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'prd':
|
||||
case 'producerdump':
|
||||
{
|
||||
if (!global.PRODUCER)
|
||||
{
|
||||
readStdin();
|
||||
break;
|
||||
}
|
||||
|
||||
global.PRODUCER.dump()
|
||||
.then((data) =>
|
||||
{
|
||||
stdinLog(`producer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||
readStdin();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
stdinError(`producer.dump() failed: ${error}`);
|
||||
readStdin();
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'cd':
|
||||
case 'consumerdump':
|
||||
{
|
||||
if (!global.CONSUMER)
|
||||
{
|
||||
readStdin();
|
||||
break;
|
||||
}
|
||||
|
||||
global.CONSUMER.dump()
|
||||
.then((data) =>
|
||||
{
|
||||
stdinLog(`consumer.dump() succeeded:\n${JSON.stringify(data, null, ' ')}`);
|
||||
readStdin();
|
||||
})
|
||||
.catch((error) =>
|
||||
{
|
||||
stdinError(`consumer.dump() failed: ${error}`);
|
||||
readStdin();
|
||||
});
|
||||
|
||||
|
|
@ -248,13 +394,10 @@ function openTerminal()
|
|||
prompt : 'terminal> ',
|
||||
useColors : true,
|
||||
useGlobal : true,
|
||||
ignoreUndefined : true
|
||||
ignoreUndefined : false
|
||||
});
|
||||
|
||||
terminal.on('exit', () =>
|
||||
{
|
||||
process.exit();
|
||||
});
|
||||
terminal.on('exit', () => openCommandConsole());
|
||||
}
|
||||
|
||||
function closeCommandConsole()
|
||||
|
|
@ -276,23 +419,14 @@ function closeTerminal()
|
|||
}
|
||||
}
|
||||
|
||||
openCommandConsole();
|
||||
|
||||
// Export openCommandConsole function by typing 'c'.
|
||||
Object.defineProperty(global, 'c',
|
||||
{
|
||||
get : function()
|
||||
{
|
||||
openCommandConsole();
|
||||
}
|
||||
});
|
||||
|
||||
function stdinLog(msg)
|
||||
{
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(colors.green(msg));
|
||||
}
|
||||
|
||||
function stdinError(msg)
|
||||
{
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(colors.red.bold('ERROR: ') + colors.red(msg));
|
||||
}
|
||||
|
|
|
|||