main
鲁誉程 3 months ago
parent 6f79c39b80
commit 67317d8918

9
.gitignore vendored

@ -0,0 +1,9 @@
node_modules/
/server/config.*
!/server/config.example.js
/server/public/
/server/certs/
/server/mediasoup_valgrind_*
/.vscode/

@ -0,0 +1,29 @@
# CHANGELOG
This is the changelog of the mediasoup demo application. To check the changes in **mediasoup** and **mediasoup-client** libraries check their corresponding projects.
## 2019-03-17
* Moving app and server to mediasoup v3.
## 2019-02-11
* Remove transport max bitrate artificial limitation.
## 2019-01-25
* Update mediasoup to 2.6.8. It fixes a [crash](https://github.com/versatica/mediasoup/issues/258) in the C++ `TcpConnection` class.
* Update mediasoup-client to 2.4.9.
## 2019-01-16
* Update mediasoup to 2.6.7. It fixes a [wrong destruction](https://github.com/versatica/mediasoup/commit/2b76b620b92c15e41fbb5677a326a90f0f365c7e) of Transport C++ instances producing a loop and CPU usage at 100%.
## 2019-01-15
* Update mediasoup to 2.6.6. It fixes an important [port leak bug](https://github.com/versatica/mediasoup/issues/259).

@ -0,0 +1,21 @@
MIT License
Copyright © 2015, Iñaki Baz Castillo <ibc@aliax.net>, José Luis Millán <jmillan@aliax.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,2 +1,160 @@
# zzxy_chat_online
# mediasoup-demo v3
A demo application of [mediasoup](https://mediasoup.org) **v3**.
Try it online at https://v3demo.mediasoup.org
## Resources
* mediasoup website and documentation: [mediasoup.org](https://mediasoup.org)
* mediasoup support forum: [mediasoup.discourse.group](https://mediasoup.discourse.group)
## Configuration via query parameters
By adding query parameters into the URL you can set certain settings of the application:
| Parameter | Type | Description | Default Value |
| ------------------ | ------- | -------------------- | ------------- |
| `roomId` | String | Id of the room | Autogenerated |
| `displayName` | String | Display name of your participant | Autogenerated |
| `handlerName` | String | Handler name of the mediasoup-client `Device` instance | Autodetected |
| `forceTcp` | Boolean | Force RTC (audio/video/datachannel) over TCP instead of UDP | `false` |
| `produce` | Boolean | Enable sending of audio/video | `true` |
| `consume` | Boolean | Enable reception of audio/video | `true` |
| `datachannel` | Boolean | Enable DataChannels | `true` |
| `forceVP8` | Boolean | Force VP8 codec for webcam and screen sharing | `false` |
| `forceH264` | Boolean | Force H264 codec for webcam and screen sharing | `false` |
| `forceVP9` | Boolean | Force VP9 codec for webcam and screen sharing | `false` |
| `enableWebcamLayers` | Boolean | Enable simulcast or SVC for webcam | `true` |
| `enableSharingLayers` | Boolean | Enable simulcast or SVC for screen sharing | `true` |
| `webcamScalabilityMode` | String | `scalabilityMode` for webcam | 'L1T3' for VP8/H264 (in each simulcast encoding), 'L3T3_KEY' for VP9 |
| `sharingScalabilityMode` | String | `scalabilityMode` for screen sharing | 'L1T3' for VP8/H264 (in each simulcast encoding), 'L3T3' for VP9 |
| `numSimulcastStreams` | Number | Number of streams for simulcast in webcam and screen sharing | 3 |
| `info` | Boolean | Display detailed information about media transmission | `false` |
| `faceDetection` | Boolean | Run face detection | `false` |
| `externalVideo` | Boolean | Send an external video instead of local webcam | `false` |
| `e2eKey` | String | Key for media E2E encryption/decryption (just works with some OPUS and VP8 codecs) | |
| `consumerReplicas` | Number | Create artificial replicas of yourself and receive their audio and video (not displayed in the UI) | 0 |
## Installation
* Clone the project:
```bash
$ git clone https://github.com/versatica/mediasoup-demo.git
$ cd mediasoup-demo
$ git checkout v3
```
* Ensure you have installed the [dependencies](https://mediasoup.org/documentation/v3/mediasoup/installation/#requirements) required by mediasoup to build.
* Set up the mediasoup-demo server:
```bash
$ cd server
$ npm install
```
* Copy `config.example.js` as `config.js` and customize it for your scenario:
```bash
$ cp config.example.js config.js
```
**NOTE:** To be perfectly clear, "customize it for your scenario" is not something "optional". If you don't set proper values in `config.js` the application **won't work**. You must configure a tls certificate.
* Set up the mediasoup-demo browser app:
```bash
$ cd app
# For node 16
$ npm install
# For node 18, use legacy peer dependencies
$ npm install --legacy-peer-deps
```
## Run it locally
* Run the Node.js server application in a terminal:
```bash
$ cd server
$ npm start
```
* In a different terminal build and run the browser application:
```bash
$ cd app
$ npm start
```
If you configured a self-signed tls certificate, and receive wss: connection errors, open the wss: url with https: protocol to accept the cert and allow wss: connections in your browser.
* Enjoy.
## Deploy it in a server
* Globally install `gulp-cli` NPM module (may need `sudo`):
```bash
$ npm install -g gulp-cli
```
* Build the production ready browser application:
```bash
$ cd app
$ gulp dist
```
* Upload the entire `server` folder to your server and make your web server (Apache, Nginx, etc) expose the `server/public` folder.
* Edit your `server/config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc).
* Within your server, run the Node.js application by setting the `DEBUG` environment variable according to your needs ([more info](https://mediasoup.org/documentation/v3/mediasoup/debugging/)):
```bash
$ DEBUG="*mediasoup* *ERROR* *WARN*" node server.js
```
* If you wish to run it as daemon/service you can use [pm2](https://www.npmjs.com/package/pm2) process manager. Or you can dockerize it among other options.
* The Node.js application exposes an interactive terminal. When running as daemon (in background) the host administrator can connect to it by entering into the `server` folder and running:
```bash
$ npm run connect
```
## Run mediasoup server with Docker
* Required environment variables: [server/DOCKER.md](server/DOCKER.md).
* Build the Docker image: [server/docker/build.sh](server/docker/build.sh).
* Run the Docker image: [server/docker/run.sh](server/docker/run.sh).
```
$ cd server
$ docker/build.sh
$ MEDIASOUP_ANNOUNCED_IP=192.168.1.34 ./docker/run.sh
```
#### Considerations for [config.js](server/config.example.js)
* Make sure [https.listenIp](server/config.example.js#L20) is set to `0.0.0.0`.
* Make sure [TLS certificates](server/config.example.js#L24) reside in `server/certs` directory with names `fullchain.pem` and `privkey.pem`.
* The default mediasoup port range is just 2000-2020, which is not suitable for production. You should increase it, however you should then run the container in `network="host"` mode.
## Authors
* Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)]
* José Luis Millán Villegas [[github](https://github.com/jmillan/)]
## License
MIT

@ -0,0 +1,223 @@
const eslintConfig =
{
env :
{
es6 : true,
node : true
},
plugins : [],
settings : {},
parserOptions :
{
ecmaVersion : 2018,
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 : false,
beforeLineComment : false
}
],
'max-len' : [ 2, 90,
{
tabWidth : 2,
comments : 90,
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' : [ 1, { 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,
{
anonymous : 'never',
named : 'never',
asyncArrow : 'always'
}
],
'space-in-parens' : [ 2, 'never' ],
'spaced-comment' : [ 2, 'always' ],
'strict' : 2,
'valid-typeof' : 2,
'yoda' : 2
}
};
switch (process.env.MEDIASOUP_NODE_LANGUAGE)
{
case 'typescript':
{
eslintConfig.parser = '@typescript-eslint/parser';
eslintConfig.plugins =
[
...eslintConfig.plugins,
'@typescript-eslint'
];
eslintConfig.extends =
[
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'
];
eslintConfig.rules =
{
...eslintConfig.rules,
'no-unused-vars' : 0,
'@typescript-eslint/ban-ts-ignore' : 0,
'@typescript-eslint/member-delimiter-style' : [ 2,
{
multiline : { delimiter: 'semi', requireLast: true },
singleline : { delimiter: 'semi', requireLast: false }
}
],
'@typescript-eslint/no-explicit-any' : 0,
'@typescript-eslint/no-unused-vars' : [ 2,
{
vars : 'all',
args : 'after-used',
ignoreRestSiblings : false
}
],
'@typescript-eslint/no-use-before-define' : 0
};
break;
}
default:
{
throw new TypeError('wrong/missing MEDIASOUP_NODE_LANGUAGE env');
}
}
module.exports = eslintConfig;

12
aiortc/.gitignore vendored

@ -0,0 +1,12 @@
/lib
/node_modules/
# Vim temporal files.
*.swp
*.swo
# Mac Stuff.
.DS_Store
# Vistual Studio Code stuff.
/.vscode/

@ -0,0 +1 @@
package-lock=false

@ -0,0 +1,6 @@
## mediasoup-demo Node app using mediasoup-client-aiortc
See [index.ts](./src/index.ts).

@ -0,0 +1,2 @@
* aiortc RTCPeerConnection does not seem to return from `close()`.

@ -0,0 +1,67 @@
const process = require('process');
const fs = require('fs');
const { execSync } = require('child_process');
const { version } = require('./package.json');
const task = process.argv.slice(2).join(' ');
// eslint-disable-next-line no-console
console.log(`npm-scripts.js [INFO] running task "${task}"`);
switch (task)
{
case 'typescript:build':
{
execute('rm -rf lib');
execute('tsc');
break;
}
case 'typescript:watch':
{
const TscWatchClient = require('tsc-watch/client');
execute('rm -rf lib');
const watch = new TscWatchClient();
watch.start('--pretty');
break;
}
case 'lint':
{
execute('MEDIASOUP_NODE_LANGUAGE=typescript eslint -c .eslintrc.js --ext=ts src/');
break;
}
case 'lint:fix':
{
execute('MEDIASOUP_NODE_LANGUAGE=typescript eslint -c .eslintrc.js --fix --ext=ts src/');
break;
}
default:
{
throw new TypeError(`unknown task "${task}"`);
}
}
function execute(command)
{
// eslint-disable-next-line no-console
console.log(`npm-scripts.js [INFO] executing command: ${command}`);
try
{
execSync(command, { stdio: [ 'ignore', process.stdout, process.stderr ] });
}
catch (error)
{
process.exit(1);
}
}

@ -0,0 +1,44 @@
{
"name": "mediasoup-demo-aiortc",
"version": "3.0.0",
"private": true,
"description": "mediasoup demo aiortc app",
"contributors": [
"Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)",
"José Luis Millán <jmillan@aliax.net> (https://github.com/jmillan)"
],
"license": "All Rights Reserved",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"engines": {
"node": ">=8.6.0"
},
"scripts": {
"typescript:build": "node npm-scripts.js typescript:build",
"typescript:watch": "node npm-scripts.js typescript:watch",
"lint": "node npm-scripts.js lint",
"lint:fix": "node npm-scripts.js lint:fix",
"dev": "cross-env DEBUG=\"${DEBUG:='*'}\" PYTHON_LOG_TO_STDOUT=true ROOM_ID=devel node ./lib/index.js"
},
"dependencies": {
"debug": "^4.1.1",
"fake-mediastreamtrack": "^1.1.3",
"mediasoup-client-aiortc": "github:versatica/mediasoup-client-aiortc#v3",
"protoo-client": "^4.0.3",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"repl": "^0.1.3",
"tsc-watch": "^4.1.0",
"typescript": "^4.7.4",
"url-parse": "^1.4.7"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/node": "^13.7.5",
"@typescript-eslint/eslint-plugin": "^2.21.0",
"@typescript-eslint/parser": "^2.21.0",
"cross-env": "^7.0.1",
"eslint": "^6.8.0"
}
}

@ -0,0 +1,47 @@
import debug from 'debug';
const APP_NAME = 'mediasoup-demo-aiortc';
export class Logger
{
private readonly _debug: debug.Debugger;
private readonly _warn: debug.Debugger;
private readonly _error: debug.Debugger;
constructor(prefix?: string)
{
if (prefix)
{
this._debug = debug(`${APP_NAME}:${prefix}`);
this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
}
else
{
this._debug = debug(APP_NAME);
this._warn = debug(`${APP_NAME}:WARN`);
this._error = debug(`${APP_NAME}:ERROR`);
}
/* 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(): debug.Debugger
{
return this._debug;
}
get warn(): debug.Debugger
{
return this._warn;
}
get error(): debug.Debugger
{
return this._error;
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,89 @@
import * as repl from 'repl';
import {
applyMiddleware as applyReduxMiddleware,
createStore as createReduxStore
} from 'redux';
import thunk from 'redux-thunk';
import { Logger } from './Logger';
import { RoomClient } from './RoomClient';
import reducers from './redux/reducers';
const reduxMiddlewares = [ thunk ];
const logger = new Logger();
const store = createReduxStore(
reducers,
undefined,
applyReduxMiddleware(...reduxMiddlewares)
);
RoomClient.init({ store });
const roomId = process.env.ROOM_ID || 'test';
const peerId = process.env.PEER_ID || 'mediasoup-client-aiortc-id';
const displayName = process.env.DISPLAY_NAME || 'mediasoup-client-aiortc';
const forceTcp = process.env.FORCE_TCP === 'true' ? true : false;
const produce = process.env.PRODUCE === 'false' ? false : true;
const consume = process.env.CONSUME === 'false' ? false : true;
const forceH264 = process.env.FORCE_H264 === 'true' ? true : false;
const forceVP8 = process.env.FORCE_VP8 === 'true' ? true : false;
const datachannel = process.env.DATACHANNEL === 'false' ? false : true;
const externalAudio = process.env.EXTERNAL_AUDIO || '';
const externalVideo = process.env.EXTERNAL_VIDEO || '';
const options =
{
roomId,
peerId,
displayName,
useSimulcast : false,
useSharingSimulcast : false,
forceTcp,
produce,
consume,
forceH264,
forceVP8,
datachannel,
externalAudio,
externalVideo
};
logger.debug(`starting mediasoup-demo-aiortc: ${JSON.stringify(options, undefined, 2)}`);
const roomClient = new RoomClient(options);
// For the interactive terminal.
(global as any).store = store;
(global as any).roomClient = roomClient;
// Run an interactive terminal.
const terminal = repl.start({
terminal : true,
prompt : 'terminal> ',
useColors : true,
useGlobal : true,
ignoreUndefined : false
});
terminal.on('exit', () => process.exit(0));
// Join!!
roomClient.join();
// NOTE: Debugging stuff.
(global as any).__sendSdps = function(): void
{
// eslint-disable-next-line no-console
console.warn('>>> send transport local SDP offer:');
// @ts-ignore
roomClient._sendTransport._handler._channel.request(
'handler.getLocalDescription',
// @ts-ignore
roomClient._sendTransport._handler._internal)
.then((desc: any) =>
{
logger.warn(desc.sdp);
});
};

@ -0,0 +1,148 @@
# APP STATE
```js
{
room :
{
url : 'https://demo.mediasoup.org/?roomId=d0el8y34',
state : 'connected', // new/connecting/connected/closed
activeSpeakerId : 'alice',
statsPeerId : null,
faceDetection : false
},
me :
{
id : '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,
shareInProgress : false,
audioOnly : false,
audioOnlyInProgress : false,
audioMuted : false,
restartIceInProgress : false
},
producers :
{
'1111-qwer' :
{
id : '1111-qwer',
paused : true,
track : MediaStreamTrack,
codec : 'opus',
rtpParameters : {},
score : [ { ssrc: 1111, score: 10 } ]
},
'1112-asdf' :
{
id : '1112-asdf',
deviceLabel : 'Macbook Webcam',
type : 'front', // front/back/share
paused : false,
track : MediaStreamTrack,
codec : 'vp8',
score : [ { ssrc: 2221, score: 10 }, { ssrc: 2222, score: 9 } ]
}
},
dataProducers :
{
'4444-4444' :
{
id : '4444-4444',
sctpStreamParameters : {},
label : 'chat',
protocol : ''
},
'4444-4445' :
{
id : '4444-4445',
sctpStreamParameters : {},
label : 'bot',
protocol : ''
}
},
peers :
{
'alice' :
{
id : 'alice',
displayName : 'Alice Thomsom',
device : { flag: 'chrome', name: 'Chrome', version: '58' },
consumers : [ '5551-qwer', '5552-zxzx' ],
dataConsumers : [ '6661-asdf' ]
}
},
consumers :
{
'5551-qwer' :
{
id : '5551-qwer',
type : 'simple',
locallyPaused : false,
remotelyPaused : false,
rtpParameters : {},
codec : 'opus',
spatialLayers : 1,
temporalLayers : 1,
currentSpatialLayer : undefined,
currentTemporalLayer : undefined,
preferredSpatialLayer : undefined,
preferredTemporalLayer : undefined,
priority : 1,
track : MediaStreamTrack,
score : [ { ssrc: 3331, score: 10 } ]
},
'5552-zxzx' :
{
id : '5552-zxzx',
type : 'simulcast',
locallyPaused : false,
remotelyPaused : true,
rtpParameters : {},
codec : 'h264',
spatialLayers : 1,
temporalLayers : 1,
currentSpatialLayer : 1,
currentTemporalLayer : 1,
preferredSpatialLayer : 2,
preferredTemporalLayer : 2,
priority : 2,
track : MediaStreamTrack,
score : [ { ssrc: 4441, score: 9 }, { ssrc: 4444, score: 8 } ]
}
},
dataConsumers :
{
'5551-qwer' :
{
id : '5551-qwer',
sctpStreamParameters : {},
label : 'chat',
protocol : 'something'
},
'5552-qwer' :
{
id : '5552-qwer',
sctpStreamParameters : {},
label : 'bot'
}
},
notifications :
[
{
id : 'qweasdw43we',
type : 'info' // info/error
text : 'You joined the room'
},
{
id : 'j7sdhkjjkcc',
type : 'error'
text : 'Could not add webcam'
}
]
}
```

@ -0,0 +1,136 @@
const initialState = {};
const consumers = (state = initialState, action: any): any =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_CONSUMER':
{
const { consumer } = action.payload;
return { ...state, [consumer.id]: consumer };
}
case 'REMOVE_CONSUMER':
{
const { consumerId } = action.payload;
const newState = { ...state };
// @ts-ignore
delete newState[consumerId];
return newState;
}
case 'SET_CONSUMER_PAUSED':
{
const { consumerId, originator } = action.payload;
// @ts-ignore
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;
// @ts-ignore
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_CURRENT_LAYERS':
{
const { consumerId, spatialLayer, temporalLayer } = action.payload;
// @ts-ignore
const consumer = state[consumerId];
const newConsumer =
{
...consumer,
currentSpatialLayer : spatialLayer,
currentTemporalLayer : temporalLayer
};
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_PREFERRED_LAYERS':
{
const { consumerId, spatialLayer, temporalLayer } = action.payload;
// @ts-ignore
const consumer = state[consumerId];
const newConsumer =
{
...consumer,
preferredSpatialLayer : spatialLayer,
preferredTemporalLayer : temporalLayer
};
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_PRIORITY':
{
const { consumerId, priority } = action.payload;
// @ts-ignore
const consumer = state[consumerId];
const newConsumer = { ...consumer, priority };
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_TRACK':
{
const { consumerId, track } = action.payload;
// @ts-ignore
const consumer = state[consumerId];
const newConsumer = { ...consumer, track };
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_SCORE':
{
const { consumerId, score } = action.payload;
// @ts-ignore
const consumer = state[consumerId];
if (!consumer)
return state;
const newConsumer = { ...consumer, score };
return { ...state, [consumerId]: newConsumer };
}
default:
{
return state;
}
}
};
export default consumers;

@ -0,0 +1,42 @@
const initialState = {};
const dataConsumers = (state = initialState, action: any): any =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_DATA_CONSUMER':
{
const { dataConsumer } = action.payload;
return { ...state, [dataConsumer.id]: dataConsumer };
}
case 'REMOVE_DATA_CONSUMER':
{
const { dataConsumerId } = action.payload;
const newState = { ...state };
// @ts-ignore
delete newState[dataConsumerId];
return newState;
}
default:
{
return state;
}
}
};
export default dataConsumers;

@ -0,0 +1,42 @@
const initialState = {};
const dataProducers = (state = initialState, action: any): any =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_DATA_PRODUCER':
{
const { dataProducer } = action.payload;
return { ...state, [dataProducer.id]: dataProducer };
}
case 'REMOVE_DATA_PRODUCER':
{
const { dataProducerId } = action.payload;
const newState = { ...state };
// @ts-ignore
delete newState[dataProducerId];
return newState;
}
default:
{
return state;
}
}
};
export default dataProducers;

@ -0,0 +1,21 @@
import { combineReducers } from 'redux';
import room from './room';
import me from './me';
import producers from './producers';
import dataProducers from './dataProducers';
import peers from './peers';
import consumers from './consumers';
import dataConsumers from './dataConsumers';
const reducers = combineReducers(
{
room,
me,
producers,
dataProducers,
peers,
consumers,
dataConsumers
});
export default reducers;

@ -0,0 +1,140 @@
type InitialState =
{
id: string | null;
displayName: string | null;
displayNameSet: boolean;
device: any | null;
canSendMic: boolean;
canSendWebcam: boolean;
canChangeWebcam: boolean;
webcamInProgress: boolean;
shareInProgress: boolean;
audioOnly: boolean;
audioOnlyInProgress: boolean;
audioMuted: boolean;
restartIceInProgress: boolean;
};
const initialState: InitialState =
{
id : null,
displayName : null,
displayNameSet : false,
device : null,
canSendMic : false,
canSendWebcam : false,
canChangeWebcam : false,
webcamInProgress : false,
shareInProgress : false,
audioOnly : false,
audioOnlyInProgress : false,
audioMuted : false,
restartIceInProgress : false
};
const me = (state = initialState, action: any): any =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
{
return {
...state,
webcamInProgress : false,
shareInProgress : false,
audioOnly : false,
audioOnlyInProgress : false,
audioMuted : false,
restartIceInProgress : false
};
}
else
{
return state;
}
}
case 'SET_ME':
{
const { peerId, displayName, displayNameSet, device } = action.payload;
return { ...state, id: peerId, 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_SHARE_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, shareInProgress: 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_AUDIO_MUTED_STATE':
{
const { enabled } = action.payload;
return { ...state, audioMuted: enabled };
}
case 'SET_RESTART_ICE_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, restartIceInProgress: flag };
}
default:
return state;
}
};
export default me;

@ -0,0 +1,144 @@
const initialState = {};
const peers = (state = initialState, action: any): any =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_PEER':
{
const { peer } = action.payload;
return { ...state, [peer.id]: peer };
}
case 'REMOVE_PEER':
{
const { peerId } = action.payload;
const newState = { ...state };
// @ts-ignore
delete newState[peerId];
return newState;
}
case 'SET_PEER_DISPLAY_NAME':
{
const { displayName, peerId } = action.payload;
// @ts-ignore
const peer = state[peerId];
if (!peer)
throw new Error('no Peer found');
const newPeer = { ...peer, displayName };
return { ...state, [newPeer.id]: newPeer };
}
case 'ADD_CONSUMER':
{
const { consumer, peerId } = action.payload;
// @ts-ignore
const peer = state[peerId];
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.id]: newPeer };
}
case 'REMOVE_CONSUMER':
{
const { consumerId, peerId } = action.payload;
// @ts-ignore
const peer = state[peerId];
// 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.id]: newPeer };
}
case 'ADD_DATA_CONSUMER':
{
const { dataConsumer, peerId } = action.payload;
// special case for bot DataConsumer.
if (!peerId)
return state;
// @ts-ignore
const peer = state[peerId];
if (!peer)
throw new Error('no Peer found for new DataConsumer');
const newDataConsumers = [ ...peer.dataConsumers, dataConsumer.id ];
const newPeer = { ...peer, dataConsumers: newDataConsumers };
return { ...state, [newPeer.id]: newPeer };
}
case 'REMOVE_DATA_CONSUMER':
{
const { dataConsumerId, peerId } = action.payload;
// special case for bot DataConsumer.
if (!peerId)
return state;
// @ts-ignore
const peer = state[peerId];
// NOTE: This means that the Peer was closed before, so it's ok.
if (!peer)
return state;
const idx = peer.dataConsumers.indexOf(dataConsumerId);
if (idx === -1)
throw new Error('DataConsumer not found');
const newDataConsumers = peer.dataConsumers.slice();
newDataConsumers.splice(idx, 1);
const newPeer = { ...peer, dataConsumers: newDataConsumers };
return { ...state, [newPeer.id]: newPeer };
}
default:
{
return state;
}
}
};
export default peers;

@ -0,0 +1,86 @@
const initialState = {};
const producers = (state = initialState, action: any): any =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_PRODUCER':
{
const { producer } = action.payload;
return { ...state, [producer.id]: producer };
}
case 'REMOVE_PRODUCER':
{
const { producerId } = action.payload;
const newState = { ...state };
// @ts-ignore
delete newState[producerId];
return newState;
}
case 'SET_PRODUCER_PAUSED':
{
const { producerId } = action.payload;
// @ts-ignore
const producer = state[producerId];
const newProducer = { ...producer, paused: true };
return { ...state, [producerId]: newProducer };
}
case 'SET_PRODUCER_RESUMED':
{
const { producerId } = action.payload;
// @ts-ignore
const producer = state[producerId];
const newProducer = { ...producer, paused: false };
return { ...state, [producerId]: newProducer };
}
case 'SET_PRODUCER_TRACK':
{
const { producerId, track } = action.payload;
// @ts-ignore
const producer = state[producerId];
const newProducer = { ...producer, track };
return { ...state, [producerId]: newProducer };
}
case 'SET_PRODUCER_SCORE':
{
const { producerId, score } = action.payload;
// @ts-ignore
const producer = state[producerId];
if (!producer)
return state;
const newProducer = { ...producer, score };
return { ...state, [producerId]: newProducer };
}
default:
{
return state;
}
}
};
export default producers;

@ -0,0 +1,83 @@
type InitialState =
{
url: string | null;
state: 'new' | 'connecting' | 'connected' | 'disconnected' | 'closed';
activeSpeakerId: string | null;
statsPeerId: string | null;
faceDetection: boolean;
};
const initialState: InitialState =
{
url : null,
state : 'new',
activeSpeakerId : null,
statsPeerId : null,
faceDetection : false
};
const room = (state = initialState, action: any): any =>
{
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, activeSpeakerId: null, statsPeerId: null };
}
case 'SET_ROOM_ACTIVE_SPEAKER':
{
const { peerId } = action.payload;
return { ...state, activeSpeakerId: peerId };
}
case 'SET_ROOM_STATS_PEER_ID':
{
const { peerId } = action.payload;
if (state.statsPeerId === peerId)
return { ...state, statsPeerId: null };
return { ...state, statsPeerId: peerId };
}
case 'SET_FACE_DETECTION':
{
const flag = action.payload;
return { ...state, faceDetection: flag };
}
case 'REMOVE_PEER':
{
const { peerId } = action.payload;
const newState = { ...state };
if (peerId && peerId === state.activeSpeakerId)
newState.activeSpeakerId = null;
if (peerId && peerId === state.statsPeerId)
newState.statsPeerId = null;
return newState;
}
default:
return state;
}
};
export default room;

@ -0,0 +1,326 @@
export const setRoomUrl = (url: string): any =>
{
return {
type : 'SET_ROOM_URL',
payload : { url }
};
};
export const setRoomState = (state: string): any =>
{
return {
type : 'SET_ROOM_STATE',
payload : { state }
};
};
export const setRoomActiveSpeaker = (peerId: string): any =>
{
return {
type : 'SET_ROOM_ACTIVE_SPEAKER',
payload : { peerId }
};
};
export const setRoomStatsPeerId = (peerId: string): any =>
{
return {
type : 'SET_ROOM_STATS_PEER_ID',
payload : { peerId }
};
};
export const setRoomFaceDetection = (flag: boolean): any =>
{
return {
type : 'SET_FACE_DETECTION',
payload : flag
};
};
export const setMe = (
{ peerId, displayName, displayNameSet, device }:
{ peerId: string; displayName: string; displayNameSet: boolean; device: any }
): any =>
{
return {
type : 'SET_ME',
payload : { peerId, displayName, displayNameSet, device }
};
};
export const setMediaCapabilities = (
{ canSendMic, canSendWebcam }:
{ canSendMic: boolean; canSendWebcam: boolean }
): any =>
{
return {
type : 'SET_MEDIA_CAPABILITIES',
payload : { canSendMic, canSendWebcam }
};
};
export const setCanChangeWebcam = (flag: boolean): any =>
{
return {
type : 'SET_CAN_CHANGE_WEBCAM',
payload : flag
};
};
export const setDisplayName = (displayName: string): any =>
{
return {
type : 'SET_DISPLAY_NAME',
payload : { displayName }
};
};
export const setAudioOnlyState = (enabled: boolean): any =>
{
return {
type : 'SET_AUDIO_ONLY_STATE',
payload : { enabled }
};
};
export const setAudioOnlyInProgress = (flag: boolean): any =>
{
return {
type : 'SET_AUDIO_ONLY_IN_PROGRESS',
payload : { flag }
};
};
export const setAudioMutedState = (enabled: boolean): any =>
{
return {
type : 'SET_AUDIO_MUTED_STATE',
payload : { enabled }
};
};
export const setRestartIceInProgress = (flag: boolean): any =>
{
return {
type : 'SET_RESTART_ICE_IN_PROGRESS',
payload : { flag }
};
};
export const addProducer = (producer: any): any =>
{
return {
type : 'ADD_PRODUCER',
payload : { producer }
};
};
export const removeProducer = (producerId: string): any =>
{
return {
type : 'REMOVE_PRODUCER',
payload : { producerId }
};
};
export const setProducerPaused = (producerId: string): any =>
{
return {
type : 'SET_PRODUCER_PAUSED',
payload : { producerId }
};
};
export const setProducerResumed = (producerId: string): any =>
{
return {
type : 'SET_PRODUCER_RESUMED',
payload : { producerId }
};
};
export const setProducerTrack = (producerId: string, track: any): any =>
{
return {
type : 'SET_PRODUCER_TRACK',
payload : { producerId, track }
};
};
export const setProducerScore = (producerId: any, score: number): any =>
{
return {
type : 'SET_PRODUCER_SCORE',
payload : { producerId, score }
};
};
export const addDataProducer = (dataProducer: any): any =>
{
return {
type : 'ADD_DATA_PRODUCER',
payload : { dataProducer }
};
};
export const removeDataProducer = (dataProducerId: string): any =>
{
return {
type : 'REMOVE_DATA_PRODUCER',
payload : { dataProducerId }
};
};
export const setWebcamInProgress = (flag: boolean): any =>
{
return {
type : 'SET_WEBCAM_IN_PROGRESS',
payload : { flag }
};
};
export const setShareInProgress = (flag: boolean): any =>
{
return {
type : 'SET_SHARE_IN_PROGRESS',
payload : { flag }
};
};
export const addPeer = (peer: any): any =>
{
return {
type : 'ADD_PEER',
payload : { peer }
};
};
export const removePeer = (peerId: string): any =>
{
return {
type : 'REMOVE_PEER',
payload : { peerId }
};
};
export const setPeerDisplayName = (displayName: string, peerId: string): any =>
{
return {
type : 'SET_PEER_DISPLAY_NAME',
payload : { displayName, peerId }
};
};
export const addConsumer = (consumer: any, peerId: string): any =>
{
return {
type : 'ADD_CONSUMER',
payload : { consumer, peerId }
};
};
export const removeConsumer = (consumerId: string, peerId: string): any =>
{
return {
type : 'REMOVE_CONSUMER',
payload : { consumerId, peerId }
};
};
export const setConsumerPaused = (consumerId: string, originator: string): any =>
{
return {
type : 'SET_CONSUMER_PAUSED',
payload : { consumerId, originator }
};
};
export const setConsumerResumed = (consumerId: string, originator: string): any =>
{
return {
type : 'SET_CONSUMER_RESUMED',
payload : { consumerId, originator }
};
};
export const setConsumerCurrentLayers =
(consumerId: string, spatialLayer: number, temporalLayer: number): any =>
{
return {
type : 'SET_CONSUMER_CURRENT_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerPreferredLayers =
(consumerId: string, spatialLayer: number, temporalLayer: number): any =>
{
return {
type : 'SET_CONSUMER_PREFERRED_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerPriority = (consumerId: string, priority: number): any =>
{
return {
type : 'SET_CONSUMER_PRIORITY',
payload : { consumerId, priority }
};
};
export const setConsumerTrack = (consumerId: string, track: any): any =>
{
return {
type : 'SET_CONSUMER_TRACK',
payload : { consumerId, track }
};
};
export const setConsumerScore = (consumerId: string, score: number): any =>
{
return {
type : 'SET_CONSUMER_SCORE',
payload : { consumerId, score }
};
};
export const addDataConsumer = (dataConsumer: any, peerId: string): any =>
{
return {
type : 'ADD_DATA_CONSUMER',
payload : { dataConsumer, peerId }
};
};
export const removeDataConsumer = (dataConsumerId: string, peerId: string): any =>
{
return {
type : 'REMOVE_DATA_CONSUMER',
payload : { dataConsumerId, peerId }
};
};
export const addNotification = (notification: any): any =>
{
return {
type : 'ADD_NOTIFICATION',
payload : { notification }
};
};
export const removeNotification = (notificationId: string): any =>
{
return {
type : 'REMOVE_NOTIFICATION',
payload : { notificationId }
};
};
export const removeAllNotifications = (): any =>
{
return {
type : 'REMOVE_ALL_NOTIFICATIONS'
};
};

@ -0,0 +1,14 @@
let protooPort = 4443;
const hostname = process.env.HOSTNAME || 'test.mediasoup.org';
if (hostname === 'test.mediasoup.org')
protooPort = 4444;
export function getProtooUrl(
{ roomId, peerId }:
{ roomId: string; peerId: string; }
): string
{
return `wss://${hostname}:${protooPort}/?roomId=${roomId}&peerId=${peerId}`;
}

@ -0,0 +1,23 @@
{
"compileOnSave": true,
"compilerOptions":
{
"lib": [ "es2018", "dom" ],
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"outDir": "lib",
"strict": true,
"strictNullChecks": false,
"noUnusedLocals": true,
"noUnusedParameters": false,
"noImplicitAny" : true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": false
},
"include": [ "src" ]
}

@ -0,0 +1,13 @@
{
"plugins":
[
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-object-rest-spread",
"jsx-control-statements"
],
"presets":
[
"@babel/env",
"@babel/react"
]
}

@ -0,0 +1,236 @@
module.exports =
{
env:
{
browser: true,
es6: true,
node: true
},
plugins:
[
'import',
'react',
'jsx-control-statements'
],
extends:
[
'eslint:recommended',
'plugin:react/recommended',
'plugin:jsx-control-statements/recommended'
],
settings:
{
react:
{
pragma: 'React',
version: '16'
}
},
parserOptions:
{
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures:
{
impliedStrict: true,
jsx: 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, 'windows' ],
'lines-around-comment': [ 2,
{
allowBlockStart: true,
allowObjectStart: true,
beforeBlockComment: true,
beforeLineComment: false
}],
'max-len': [ 2, 94,
{
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,
{
anonymous : 'never',
named : 'never',
asyncArrow : 'always'
}],
'space-in-parens': [ 2, 'never' ],
'spaced-comment': [ 2, 'always' ],
'strict': 2,
'valid-typeof': 2,
'eol-last': 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': 0,
'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, { skipUndeclared: true } ],
'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
}]
}
};

@ -0,0 +1 @@
package-lock=false

@ -0,0 +1,7 @@
/*
* <%= pkg.name %> v<%= pkg.version %>
* <%= pkg.description %>
* Copyright: 2017-<%= currentYear %> <%= pkg.author %>
* License: <%= pkg.license %>
*/

@ -0,0 +1,454 @@
/**
* Tasks:
*
* 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 (unless NODE_ENV is set
* to 'production'), opens it and watches for changes in the source code.
*
* gulp devel
* Generates the browser app in development mode (unless NODE_ENV is set
* to 'production'), opens two browsers and watches for changes in the source
* code.
*
* gulp devel:tcp
* Same as gulp devel, but forcing media over TCP.
*
* gulp devel:vp9
* Generates the browser app in development mode (unless NODE_ENV is set
* to 'production'), opens two browsers forcing VP9 and watches for changes in
* the source code.
*
* gulp devel:h264
* Generates the browser app in development mode (unless NODE_ENV is set
* to 'production'), opens two browsers forcing H264 and watches for changes in
* the source code.
* gulp
* Alias for `gulp dist`.
*/
const fs = require('fs');
const path = require('path');
const gulp = require('gulp');
const gulpif = require('gulp-if');
const gutil = require('gulp-util');
const plumber = require('gulp-plumber');
const 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');
const uglify = require('gulp-uglify-es').default;
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const del = require('del');
const mkdirp = require('mkdirp');
const ncp = require('ncp');
const eslint = require('gulp-eslint');
const stylus = require('gulp-stylus');
const cssBase64 = require('gulp-css-base64');
const nib = require('nib');
const browserSync = require('browser-sync');
const PKG = require('./package');
const BANNER = fs.readFileSync('banner.txt').toString();
const BANNER_OPTIONS =
{
pkg : PKG,
currentYear : (new Date()).getFullYear()
};
const OUTPUT_DIR = '../server/public';
// 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(error.stack));
}
function bundle(options)
{
options = options || {};
const watch = Boolean(options.watch);
let bundler = browserify(
{
entries : PKG.main,
extensions : [ '.js', '.jsx' ],
// required for sourcemaps (must be false otherwise).
debug : process.env.NODE_ENV === 'development',
// required for watchify.
cache : {},
// required for watchify.
packageCache : {},
// required to be true only for watchify.
fullPaths : watch
})
.transform('babelify')
.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(`${PKG.name}.js`))
.pipe(buffer())
.pipe(rename(`${PKG.name}.js`))
.pipe(gulpif(process.env.NODE_ENV === 'production',
uglify()
))
.pipe(header(BANNER, BANNER_OPTIONS))
.pipe(gulp.dest(OUTPUT_DIR));
}
return rebundle();
}
gulp.task('clean', () => del(OUTPUT_DIR, { force: true }));
gulp.task('lint', () =>
{
const src =
[
'gulpfile.js',
'lib/**/*.js',
'lib/**/*.jsx'
];
return gulp.src(src)
.pipe(plumber())
.pipe(eslint())
.pipe(eslint.format());
});
gulp.task('css', () =>
{
return gulp.src('stylus/index.styl')
.pipe(plumber())
.pipe(stylus(
{
use : nib(),
compress : process.env.NODE_ENV === 'production'
}))
.on('error', logError)
.pipe(cssBase64(
{
baseDir : '.',
maxWeightResource : 50000 // So big ttf fonts are not included, nice.
}))
.pipe(rename(`${PKG.name}.css`))
.pipe(gulp.dest(OUTPUT_DIR))
.pipe(touch());
});
gulp.task('html', () =>
{
return gulp.src('index.html')
.pipe(gulp.dest(OUTPUT_DIR));
});
gulp.task('resources', (done) =>
{
const dst = path.join(OUTPUT_DIR, 'resources');
mkdirp.sync(dst);
ncp('resources', dst, { stopOnErr: true }, (error) =>
{
if (error && error[0].code !== 'ENOENT')
throw new Error(`resources copy failed: ${error}`);
done();
});
});
gulp.task('bundle', () =>
{
return bundle({ watch: false });
});
gulp.task('bundle:watch', () =>
{
return bundle({ watch: true });
});
gulp.task('dist', gulp.series(
'clean',
'lint',
'bundle',
'html',
'css',
'resources'
));
gulp.task('watch', (done) =>
{
// Watch changes in HTML.
gulp.watch([ 'index.html' ], gulp.series(
'html'
));
// Watch changes in Stylus files.
gulp.watch([ 'stylus/**/*.styl' ], gulp.series(
'css'
));
// Watch changes in resources.
gulp.watch([ 'resources/**/*' ], gulp.series(
'resources', 'css'
));
// Watch changes in JS files.
gulp.watch([ 'gulpfile.js', 'lib/**/*.js', 'lib/**/*.jsx' ], gulp.series(
'lint'
));
done();
});
gulp.task('browser:base', gulp.series(
'clean',
'lint',
'bundle:watch',
'html',
'css',
'resources',
'watch'
));
gulp.task('live', gulp.series(
'browser:base',
(done) =>
{
const config = require('../server/config');
browserSync(
{
open : 'external',
host : config.domain,
startPath : '/?info=true',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
});
done();
}
));
gulp.task('devel', gulp.series(
'browser:base',
async (done) =>
{
const config = require('../server/config');
await new Promise((resolve) =>
{
browserSync.create('producer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel&info=true&_throttleSecret=foo&consume=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
await new Promise((resolve) =>
{
browserSync.create('consumer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel&info=true&_throttleSecret=foo&produce=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
done();
}
));
gulp.task('devel:tcp', gulp.series(
'browser:base',
async (done) =>
{
const config = require('../server/config');
await new Promise((resolve) =>
{
browserSync.create('producer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel:tcp&info=true&_throttleSecret=foo&forceTcp=true&consume=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
await new Promise((resolve) =>
{
browserSync.create('consumer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel:tcp&info=true&_throttleSecret=foo&forceTcp=true&produce=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
done();
}
));
gulp.task('devel:vp9', gulp.series(
'browser:base',
async (done) =>
{
const config = require('../server/config');
await new Promise((resolve) =>
{
browserSync.create('producer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel:vp9&info=true&_throttleSecret=foo&forceVP9=true&numSimulcastStreams=3&webcamScalabilityMode=L1T3&consume=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
await new Promise((resolve) =>
{
browserSync.create('consumer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel:vp9&info=true&_throttleSecret=foo&forceVP9=true&produce=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
done();
}
));
gulp.task('devel:h264', gulp.series(
'browser:base',
async (done) =>
{
const config = require('../server/config');
await new Promise((resolve) =>
{
browserSync.create('producer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel:h264&info=true&_throttleSecret=foo&forceH264=true&consume=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
await new Promise((resolve) =>
{
browserSync.create('consumer1').init(
{
open : 'external',
host : config.domain,
startPath : '/?roomId=devel:h264&info=true&_throttleSecret=foo&forceH264=true&produce=false',
server :
{
baseDir : OUTPUT_DIR
},
https : config.https.tls,
ghostMode : false,
files : path.join(OUTPUT_DIR, '**', '*')
},
resolve);
});
done();
}
));
gulp.task('default', gulp.series('dist'));

@ -0,0 +1,29 @@
<!doctype html>
<html>
<head>
<title>mediasoup demo</title>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
<meta name='description' content='mediasoup demo - Cutting Edge WebRTC Video Conferencing'>
<link rel='stylesheet' href='/mediasoup-demo-app.css?v=foo2'>
<script src='/resources/js/antiglobal.js'></script>
<script>
window.localStorage.setItem('debug', '* -engine* -socket* -RIE* *WARN* *ERROR*');
if (window.antiglobal)
{
window.antiglobal('___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__');
setInterval(window.antiglobal, 180000);
}
</script>
<script async src='/mediasoup-demo-app.js?v=foo2'></script>
</head>
<body>
<div id='mediasoup-demo-app-container'></div>
<div id='mediasoup-demo-app-media-query-detector'></div>
</body>
</html>

@ -0,0 +1,43 @@
import debug from 'debug';
const APP_NAME = 'mediasoup-demo';
export default class Logger
{
constructor(prefix)
{
if (prefix)
{
this._debug = debug(`${APP_NAME}:${prefix}`);
this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
}
else
{
this._debug = debug(APP_NAME);
this._warn = debug(`${APP_NAME}:WARN`);
this._error = debug(`${APP_NAME}:ERROR`);
}
/* 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()
{
return this._debug;
}
get warn()
{
return this._warn;
}
get error()
{
return this._error;
}
}

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,123 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRoomContext } from '../RoomContext';
const BotMessageRegex = new RegExp('^@bot (.*)');
class ChatInput extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
text : ''
};
// TextArea element got via React ref.
// @type {HTMLElement}
this._textareaElem = null;
}
render()
{
const {
connected,
chatDataProducer,
botDataProducer
} = this.props;
const { text } = this.state;
const disabled = !connected || (!chatDataProducer && !botDataProducer);
return (
<div data-component='ChatInput'>
<textarea
ref={(elem) => { this._textareaElem = elem; }}
placeholder={disabled ? 'Chat unavailable' : 'Write here...'}
dir='auto'
autoComplete='off'
disabled={disabled}
value={text}
onChange={this.handleChange.bind(this)}
onKeyPress={this.handleKeyPress.bind(this)}
/>
</div>
);
}
handleChange(event)
{
const text = event.target.value;
this.setState({ text });
}
handleKeyPress(event)
{
// If Shift + Enter do nothing.
if (event.key !== 'Enter' || (event.shiftKey || event.ctrlKey))
return;
// Don't add the sending Enter into the value.
event.preventDefault();
let text = this.state.text.trim();
this.setState({ text: '' });
if (text)
{
const { roomClient } = this.props;
const match = BotMessageRegex.exec(text);
// Chat message.
if (!match)
{
text = text.trim();
roomClient.sendChatMessage(text);
}
// Message to the bot.
else
{
text = match[1].trim();
roomClient.sendBotMessage(text);
}
}
}
}
ChatInput.propTypes =
{
roomClient : PropTypes.any.isRequired,
connected : PropTypes.bool.isRequired,
chatDataProducer : PropTypes.any,
botDataProducer : PropTypes.any
};
const mapStateToProps = (state) =>
{
const dataProducersArray = Object.values(state.dataProducers);
const chatDataProducer = dataProducersArray
.find((dataProducer) => dataProducer.label === 'chat');
const botDataProducer = dataProducersArray
.find((dataProducer) => dataProducer.label === 'bot');
return {
connected : state.room.state === 'connected',
chatDataProducer,
botDataProducer
};
};
const ChatInputContainer = withRoomContext(connect(
mapStateToProps,
undefined
)(ChatInput));
export default ChatInputContainer;

@ -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
};

@ -0,0 +1,237 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ReactTooltip from 'react-tooltip';
import classnames from 'classnames';
import * as cookiesManager from '../cookiesManager';
import * as appPropTypes from './appPropTypes';
import { withRoomContext } from '../RoomContext';
import * as stateActions from '../redux/stateActions';
import PeerView from './PeerView';
class Me extends React.Component
{
constructor(props)
{
super(props);
this._mounted = false;
this._rootNode = null;
}
render()
{
const {
roomClient,
connected,
me,
audioProducer,
videoProducer,
faceDetection,
onSetStatsPeerId
} = this.props;
let micState;
if (!me.canSendMic)
micState = 'unsupported';
else if (!audioProducer)
micState = 'unsupported';
else if (!audioProducer.paused)
micState = 'on';
else
micState = 'off';
let webcamState;
if (!me.canSendWebcam)
webcamState = 'unsupported';
else if (videoProducer && videoProducer.type !== 'share')
webcamState = 'on';
else
webcamState = 'off';
let changeWebcamState;
if (Boolean(videoProducer) && videoProducer.type !== 'share' && me.canChangeWebcam)
changeWebcamState = 'on';
else
changeWebcamState = 'unsupported';
let shareState;
if (Boolean(videoProducer) && videoProducer.type === 'share')
shareState = 'on';
else
shareState = 'off';
const videoVisible = Boolean(videoProducer) && !videoProducer.paused;
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}
>
<If condition={connected}>
<div className='controls'>
<div
className={classnames('button', 'mic', micState)}
onClick={() =>
{
micState === 'on'
? roomClient.muteMic()
: roomClient.unmuteMic();
}}
/>
<div
className={classnames('button', 'webcam', webcamState, {
disabled : me.webcamInProgress || me.shareInProgress
})}
onClick={() =>
{
if (webcamState === 'on')
{
cookiesManager.setDevices({ webcamEnabled: false });
roomClient.disableWebcam();
}
else
{
cookiesManager.setDevices({ webcamEnabled: true });
roomClient.enableWebcam();
}
}}
/>
<div
className={classnames('button', 'change-webcam', changeWebcamState, {
disabled : me.webcamInProgress || me.shareInProgress
})}
onClick={() => roomClient.changeWebcam()}
/>
<div
className={classnames('button', 'share', shareState, {
disabled : me.shareInProgress || me.webcamInProgress
})}
onClick={() =>
{
if (shareState === 'on')
roomClient.disableShare();
else
roomClient.enableShare();
}}
/>
</div>
</If>
<PeerView
isMe
peer={me}
audioProducerId={audioProducer ? audioProducer.id : null}
videoProducerId={videoProducer ? videoProducer.id : null}
audioRtpParameters={audioProducer ? audioProducer.rtpParameters : null}
videoRtpParameters={videoProducer ? videoProducer.rtpParameters : null}
audioTrack={audioProducer ? audioProducer.track : null}
videoTrack={videoProducer ? videoProducer.track : null}
videoVisible={videoVisible}
audioCodec={audioProducer ? audioProducer.codec : null}
videoCodec={videoProducer ? videoProducer.codec : null}
audioScore={audioProducer ? audioProducer.score : null}
videoScore={videoProducer ? videoProducer.score : null}
faceDetection={faceDetection}
onChangeDisplayName={(displayName) =>
{
roomClient.changeDisplayName(displayName);
}}
onChangeMaxSendingSpatialLayer={(spatialLayer) =>
{
roomClient.setMaxSendingSpatialLayer(spatialLayer);
}}
onStatsClick={onSetStatsPeerId}
/>
<ReactTooltip
type='light'
effect='solid'
delayShow={100}
delayHide={100}
delayUpdate={50}
/>
</div>
);
}
componentDidMount()
{
this._mounted = true;
setTimeout(() =>
{
if (!this._mounted || this.props.me.displayNameSet)
return;
ReactTooltip.show(this._rootNode);
}, 4000);
}
componentWillUnmount()
{
this._mounted = false;
}
componentDidUpdate(prevProps)
{
if (!prevProps.me.displayNameSet && this.props.me.displayNameSet)
ReactTooltip.hide(this._rootNode);
}
}
Me.propTypes =
{
roomClient : PropTypes.any.isRequired,
connected : PropTypes.bool.isRequired,
me : appPropTypes.Me.isRequired,
audioProducer : appPropTypes.Producer,
videoProducer : appPropTypes.Producer,
faceDetection : PropTypes.bool.isRequired,
onSetStatsPeerId : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
const producersArray = Object.values(state.producers);
const audioProducer =
producersArray.find((producer) => producer.track.kind === 'audio');
const videoProducer =
producersArray.find((producer) => producer.track.kind === 'video');
return {
connected : state.room.state === 'connected',
me : state.me,
audioProducer : audioProducer,
videoProducer : videoProducer,
faceDetection : state.room.faceDetection
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onSetStatsPeerId : (peerId) => dispatch(stateActions.setRoomStatsPeerId(peerId))
};
};
const MeContainer = withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(Me));
export default MeContainer;

@ -0,0 +1,200 @@
import React from 'react';
import Draggable from 'react-draggable';
import PropTypes from 'prop-types';
import { withRoomContext } from '../RoomContext';
class NetworkThrottle extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
uplink : '',
downlink : '',
rtt : '',
packetLoss : '',
disabled : false
};
}
render()
{
const { uplink, downlink, rtt, packetLoss, disabled } = this.state;
return (
<Draggable
bounds='parent'
defaultPosition={{ x: 20, y: 20 }}
handle='h1.draggable'
>
<form
data-component='NetworkThrottle'
onSubmit={(event) =>
{
event.preventDefault();
this._apply();
}}
>
<h1 className='draggable'>Network Throttle</h1>
<div className='inputs'>
<div className='row'>
<p className='key'>
UPLINK (kbps)
</p>
<input
className='value'
type='text'
placeholder='NO LIMIT'
disabled={disabled}
pattern='[0-9]*'
value={uplink}
autoCorrect='false'
spellCheck='false'
onChange={(event) => this.setState({ uplink: event.target.value })}
/>
</div>
<div className='row'>
<p className='key'>
DOWNLINK (kbps)
</p>
<input
className='value'
type='text'
placeholder='NO LIMIT'
disabled={disabled}
pattern='[0-9]*'
value={downlink}
autoCorrect='false'
spellCheck='false'
onChange={(event) => this.setState({ downlink: event.target.value })}
/>
</div>
<div className='row'>
<p className='key'>
RTT (ms)
</p>
<input
className='value'
type='text'
placeholder='NOT SET'
disabled={disabled}
pattern='[0-9]*'
value={rtt}
autoCorrect='false'
spellCheck='false'
onChange={(event) => this.setState({ rtt: event.target.value })}
/>
</div>
<div className='row'>
<p className='key'>
PACKETLOSS (%)
</p>
<input
className='value'
type='text'
placeholder='NOT SET'
disabled={disabled}
pattern='[0-9]*'
value={packetLoss}
autoCorrect='false'
spellCheck='false'
onChange={(event) => this.setState({ packetLoss: event.target.value })}
/>
</div>
</div>
<div className='buttons'>
<button
type='button'
className='reset'
disabled={disabled}
onClick={() => this._reset()}
>
RESET
</button>
<button
type='submit'
className='apply'
disabled={
disabled ||
(!uplink.trim() && !downlink.trim() && !rtt.trim() && !packetLoss.trim())
}
>
APPLY
</button>
</div>
</form>
</Draggable>
);
}
componentWillUnmount()
{
const { roomClient } = this.props;
roomClient.resetNetworkThrottle({ silent: true });
}
async _apply()
{
const { roomClient, secret } = this.props;
let { uplink, downlink, rtt, packetLoss } = this.state;
uplink = Number(uplink) || 0;
downlink = Number(downlink) || 0;
rtt = Number(rtt) || 0;
packetLoss = Number(packetLoss) || 0;
this.setState({ disabled: true });
await roomClient.applyNetworkThrottle(
{ secret, uplink, downlink, rtt, packetLoss });
window.onunload = () =>
{
roomClient.resetNetworkThrottle({ silent: true, secret });
};
this.setState({ disabled: false });
}
async _reset()
{
const { roomClient, secret } = this.props;
this.setState(
{
uplink : '',
downlink : '',
rtt : '',
packetLoss : '',
disabled : false
});
this.setState({ disabled: true });
await roomClient.resetNetworkThrottle({ secret });
this.setState({ disabled: false });
}
}
NetworkThrottle.propTypes =
{
roomClient : PropTypes.any.isRequired,
secret : PropTypes.string.isRequired
};
export default withRoomContext(NetworkThrottle);

@ -0,0 +1,68 @@
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' />
<div className='body'>
<If condition={notification.title}>
<p className='title'>{notification.title}</p>
</If>
<p className='text'>{notification.text}</p>
</div>
</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;

@ -0,0 +1,138 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as appPropTypes from './appPropTypes';
import { withRoomContext } from '../RoomContext';
import * as stateActions from '../redux/stateActions';
import PeerView from './PeerView';
const Peer = (props) =>
{
const {
roomClient,
peer,
audioConsumer,
videoConsumer,
audioMuted,
faceDetection,
onSetStatsPeerId
} = props;
const audioEnabled = (
Boolean(audioConsumer) &&
!audioConsumer.locallyPaused &&
!audioConsumer.remotelyPaused
);
const videoVisible = (
Boolean(videoConsumer) &&
!videoConsumer.locallyPaused &&
!videoConsumer.remotelyPaused
);
return (
<div data-component='Peer'>
<div className='indicators'>
<If condition={!audioEnabled}>
<div className='icon mic-off' />
</If>
<If condition={!videoConsumer}>
<div className='icon webcam-off' />
</If>
</div>
<PeerView
peer={peer}
audioConsumerId={audioConsumer ? audioConsumer.id : null}
videoConsumerId={videoConsumer ? videoConsumer.id : null}
audioRtpParameters={audioConsumer ? audioConsumer.rtpParameters : null}
videoRtpParameters={videoConsumer ? videoConsumer.rtpParameters : null}
consumerSpatialLayers={videoConsumer ? videoConsumer.spatialLayers : null}
consumerTemporalLayers={videoConsumer ? videoConsumer.temporalLayers : null}
consumerCurrentSpatialLayer={
videoConsumer ? videoConsumer.currentSpatialLayer : null
}
consumerCurrentTemporalLayer={
videoConsumer ? videoConsumer.currentTemporalLayer : null
}
consumerPreferredSpatialLayer={
videoConsumer ? videoConsumer.preferredSpatialLayer : null
}
consumerPreferredTemporalLayer={
videoConsumer ? videoConsumer.preferredTemporalLayer : null
}
consumerPriority={videoConsumer ? videoConsumer.priority : null}
audioTrack={audioConsumer ? audioConsumer.track : null}
videoTrack={videoConsumer ? videoConsumer.track : null}
audioMuted={audioMuted}
videoVisible={videoVisible}
videoMultiLayer={videoConsumer && videoConsumer.type !== 'simple'}
audioCodec={audioConsumer ? audioConsumer.codec : null}
videoCodec={videoConsumer ? videoConsumer.codec : null}
audioScore={audioConsumer ? audioConsumer.score : null}
videoScore={videoConsumer ? videoConsumer.score : null}
faceDetection={faceDetection}
onChangeVideoPreferredLayers={(spatialLayer, temporalLayer) =>
{
roomClient.setConsumerPreferredLayers(
videoConsumer.id, spatialLayer, temporalLayer);
}}
onChangeVideoPriority={(priority) =>
{
roomClient.setConsumerPriority(videoConsumer.id, priority);
}}
onRequestKeyFrame={() =>
{
roomClient.requestConsumerKeyFrame(videoConsumer.id);
}}
onStatsClick={onSetStatsPeerId}
/>
</div>
);
};
Peer.propTypes =
{
roomClient : PropTypes.any.isRequired,
peer : appPropTypes.Peer.isRequired,
audioConsumer : appPropTypes.Consumer,
videoConsumer : appPropTypes.Consumer,
audioMuted : PropTypes.bool,
faceDetection : PropTypes.bool.isRequired,
onSetStatsPeerId : PropTypes.func.isRequired
};
const mapStateToProps = (state, { id }) =>
{
const me = state.me;
const peer = state.peers[id];
const consumersArray = peer.consumers
.map((consumerId) => state.consumers[consumerId]);
const audioConsumer =
consumersArray.find((consumer) => consumer.track.kind === 'audio');
const videoConsumer =
consumersArray.find((consumer) => consumer.track.kind === 'video');
return {
peer,
audioConsumer,
videoConsumer,
audioMuted : me.audioMuted,
faceDetection : state.room.faceDetection
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onSetStatsPeerId : (peerId) => dispatch(stateActions.setRoomStatsPeerId(peerId))
};
};
const PeerContainer = withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(Peer));
export default PeerContainer;

@ -0,0 +1,794 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactTooltip from 'react-tooltip';
import classnames from 'classnames';
import Spinner from 'react-spinner';
import clipboardCopy from 'clipboard-copy';
import hark from 'hark';
import * as faceapi from 'face-api.js';
import Logger from '../Logger';
import * as appPropTypes from './appPropTypes';
import EditableInput from './EditableInput';
const logger = new Logger('PeerView');
const tinyFaceDetectorOptions = new faceapi.TinyFaceDetectorOptions(
{
inputSize : 160,
scoreThreshold : 0.5
});
export default class PeerView extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
audioVolume : 0, // Integer from 0 to 10.,
showInfo : window.SHOW_INFO || false,
videoResolutionWidth : null,
videoResolutionHeight : null,
videoCanPlay : false,
videoElemPaused : false,
maxSpatialLayer : 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 reading video resolution.
this._videoResolutionPeriodicTimer = null;
// requestAnimationFrame for face detection.
this._faceDetectionRequestAnimationFrame = null;
}
render()
{
const {
isMe,
peer,
audioProducerId,
videoProducerId,
audioConsumerId,
videoConsumerId,
videoRtpParameters,
consumerSpatialLayers,
consumerTemporalLayers,
consumerCurrentSpatialLayer,
consumerCurrentTemporalLayer,
consumerPreferredSpatialLayer,
consumerPreferredTemporalLayer,
consumerPriority,
audioMuted,
videoVisible,
videoMultiLayer,
audioCodec,
videoCodec,
audioScore,
videoScore,
onChangeDisplayName,
onChangeMaxSendingSpatialLayer,
onChangeVideoPreferredLayers,
onChangeVideoPriority,
onRequestKeyFrame,
onStatsClick
} = this.props;
const {
audioVolume,
showInfo,
videoResolutionWidth,
videoResolutionHeight,
videoCanPlay,
videoElemPaused,
maxSpatialLayer
} = this.state;
return (
<div data-component='PeerView'>
<div className='info'>
<div className='icons'>
<div
className={classnames('icon', 'info', { on: showInfo })}
onClick={() => this.setState({ showInfo: !showInfo })}
/>
<div
className={classnames('icon', 'stats')}
onClick={() => onStatsClick(peer.id)}
/>
</div>
<div className={classnames('box', { visible: showInfo })}>
<If condition={audioProducerId || audioConsumerId}>
<h1>audio</h1>
<If condition={audioProducerId}>
<p>
{'id: '}
<span
className='copiable'
data-tip='Copy audio producer id to clipboard'
onClick={() => clipboardCopy(`"${audioProducerId}"`)}
>
{audioProducerId}
</span>
</p>
<ReactTooltip
type='light'
effect='solid'
delayShow={1500}
delayHide={50}
/>
</If>
<If condition={audioConsumerId}>
<p>
{'id: '}
<span
className='copiable'
data-tip='Copy video producer id to clipboard'
onClick={() => clipboardCopy(`"${audioConsumerId}"`)}
>
{audioConsumerId}
</span>
</p>
<ReactTooltip
type='light'
effect='solid'
delayShow={1500}
delayHide={50}
/>
</If>
<If condition={audioCodec}>
<p>codec: {audioCodec}</p>
</If>
<If condition={audioProducerId && audioScore}>
{this._printProducerScore(audioProducerId, audioScore)}
</If>
<If condition={audioConsumerId && audioScore}>
{this._printConsumerScore(audioConsumerId, audioScore)}
</If>
</If>
<If condition={videoProducerId || videoConsumerId}>
<h1>video</h1>
<If condition={videoProducerId}>
<p>
{'id: '}
<span
className='copiable'
data-tip='Copy audio consumer id to clipboard'
onClick={() => clipboardCopy(`"${videoProducerId}"`)}
>
{videoProducerId}
</span>
</p>
<ReactTooltip
type='light'
effect='solid'
delayShow={1500}
delayHide={50}
/>
</If>
<If condition={videoConsumerId}>
<p>
{'id: '}
<span
className='copiable'
data-tip='Copy video consumer id to clipboard'
onClick={() => clipboardCopy(`"${videoConsumerId}"`)}
>
{videoConsumerId}
</span>
</p>
<ReactTooltip
type='light'
effect='solid'
delayShow={1500}
delayHide={50}
/>
</If>
<If condition={videoCodec}>
<p>codec: {videoCodec}</p>
</If>
<If condition={videoVisible && videoResolutionWidth !== null}>
<p>resolution: {videoResolutionWidth}x{videoResolutionHeight}</p>
</If>
<If
condition={
videoVisible &&
videoProducerId &&
videoRtpParameters.encodings.length > 1
}
>
<p>
max spatial layer: {maxSpatialLayer > -1 ? maxSpatialLayer : 'none'}
<span>{' '}</span>
<span
className={classnames({
clickable : maxSpatialLayer > -1
})}
onClick={(event) =>
{
event.stopPropagation();
const newMaxSpatialLayer = maxSpatialLayer -1;
onChangeMaxSendingSpatialLayer(newMaxSpatialLayer);
this.setState({ maxSpatialLayer: newMaxSpatialLayer });
}}
>
{'[ down ]'}
</span>
<span>{' '}</span>
<span
className={classnames({
clickable : maxSpatialLayer < videoRtpParameters.encodings.length - 1
})}
onClick={(event) =>
{
event.stopPropagation();
const newMaxSpatialLayer = maxSpatialLayer + 1;
onChangeMaxSendingSpatialLayer(newMaxSpatialLayer);
this.setState({ maxSpatialLayer: newMaxSpatialLayer });
}}
>
{'[ up ]'}
</span>
</p>
</If>
<If condition={!isMe && videoMultiLayer}>
<p>
{`current spatial-temporal layers: ${consumerCurrentSpatialLayer} ${consumerCurrentTemporalLayer}`}
</p>
<p>
{`preferred spatial-temporal layers: ${consumerPreferredSpatialLayer} ${consumerPreferredTemporalLayer}`}
<span>{' '}</span>
<span
className='clickable'
onClick={(event) =>
{
event.stopPropagation();
let newPreferredSpatialLayer = consumerPreferredSpatialLayer;
let newPreferredTemporalLayer;
if (consumerPreferredTemporalLayer > 0)
{
newPreferredTemporalLayer = consumerPreferredTemporalLayer - 1;
}
else
{
if (consumerPreferredSpatialLayer > 0)
newPreferredSpatialLayer = consumerPreferredSpatialLayer - 1;
else
newPreferredSpatialLayer = consumerSpatialLayers - 1;
newPreferredTemporalLayer = consumerTemporalLayers - 1;
}
onChangeVideoPreferredLayers(
newPreferredSpatialLayer, newPreferredTemporalLayer);
}}
>
{'[ down ]'}
</span>
<span>{' '}</span>
<span
className='clickable'
onClick={(event) =>
{
event.stopPropagation();
let newPreferredSpatialLayer = consumerPreferredSpatialLayer;
let newPreferredTemporalLayer;
if (consumerPreferredTemporalLayer < consumerTemporalLayers - 1)
{
newPreferredTemporalLayer = consumerPreferredTemporalLayer + 1;
}
else
{
if (consumerPreferredSpatialLayer < consumerSpatialLayers - 1)
newPreferredSpatialLayer = consumerPreferredSpatialLayer + 1;
else
newPreferredSpatialLayer = 0;
newPreferredTemporalLayer = 0;
}
onChangeVideoPreferredLayers(
newPreferredSpatialLayer, newPreferredTemporalLayer);
}}
>
{'[ up ]'}
</span>
</p>
</If>
<If condition={!isMe && videoCodec && consumerPriority > 0}>
<p>
{`priority: ${consumerPriority}`}
<span>{' '}</span>
<span
className={classnames({
clickable : consumerPriority > 1
})}
onClick={(event) =>
{
event.stopPropagation();
onChangeVideoPriority(consumerPriority - 1);
}}
>
{'[ down ]'}
</span>
<span>{' '}</span>
<span
className={classnames({
clickable : consumerPriority < 255
})}
onClick={(event) =>
{
event.stopPropagation();
onChangeVideoPriority(consumerPriority + 1);
}}
>
{'[ up ]'}
</span>
</p>
</If>
<If condition={!isMe && videoCodec}>
<p>
<span
className='clickable'
onClick={(event) =>
{
event.stopPropagation();
if (!onRequestKeyFrame)
return;
onRequestKeyFrame();
}}
>
{'[ request keyframe ]'}
</span>
</p>
</If>
<If condition={videoProducerId && videoScore}>
{this._printProducerScore(videoProducerId, videoScore)}
</If>
<If condition={videoConsumerId && videoScore}>
{this._printConsumerScore(videoConsumerId, videoScore)}
</If>
</If>
</div>
<div className={classnames('peer', { 'is-me': isMe })}>
<Choose>
<When condition={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)}
/>
</When>
<Otherwise>
<span className='display-name'>
{peer.displayName}
</span>
</Otherwise>
</Choose>
<div className='row'>
<span
className={classnames('device-icon', peer.device.flag)}
/>
<span className='device-version'>
{peer.device.name} {peer.device.version || null}
</span>
</div>
</div>
</div>
<video
ref='videoElem'
className={classnames({
'is-me' : isMe,
hidden : !videoVisible || !videoCanPlay,
'network-error' : (
videoVisible && videoMultiLayer && consumerCurrentSpatialLayer === null
)
})}
autoPlay
playsInline
muted
controls={false}
/>
<audio
ref='audioElem'
autoPlay
muted={isMe || audioMuted}
controls={false}
/>
<canvas
ref='canvas'
className={classnames('face-detection', { 'is-me': isMe })}
/>
<div className='volume-container'>
<div className={classnames('bar', `level${audioVolume}`)} />
</div>
<If condition={videoVisible && videoScore < 5}>
<div className='spinner-container'>
<Spinner />
</div>
</If>
<If condition={videoElemPaused}>
<div className='video-elem-paused' />
</If>
</div>
);
}
componentDidMount()
{
const { audioTrack, videoTrack } = this.props;
this._setTracks(audioTrack, videoTrack);
}
componentWillUnmount()
{
if (this._hark)
this._hark.stop();
clearInterval(this._videoResolutionPeriodicTimer);
cancelAnimationFrame(this._faceDetectionRequestAnimationFrame);
const { videoElem } = this.refs;
if (videoElem)
{
videoElem.oncanplay = null;
videoElem.onplay = null;
videoElem.onpause = null;
}
}
componentWillUpdate()
{
const {
isMe,
audioTrack,
videoTrack,
videoRtpParameters
} = this.props;
const { maxSpatialLayer } = this.state;
if (isMe && videoRtpParameters && maxSpatialLayer === null)
{
this.setState(
{
maxSpatialLayer : videoRtpParameters.encodings.length - 1
});
}
else if (isMe && !videoRtpParameters && maxSpatialLayer !== null)
{
this.setState({ maxSpatialLayer: null });
}
this._setTracks(audioTrack, videoTrack);
}
_setTracks(audioTrack, videoTrack)
{
const { faceDetection } = this.props;
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
return;
this._audioTrack = audioTrack;
this._videoTrack = videoTrack;
if (this._hark)
this._hark.stop();
this._stopVideoResolution();
if (faceDetection)
this._stopFaceDetection();
const { audioElem, videoElem } = this.refs;
if (audioTrack)
{
const stream = new MediaStream;
stream.addTrack(audioTrack);
audioElem.srcObject = stream;
audioElem.play()
.catch((error) => logger.warn('audioElem.play() failed:%o', error));
this._runHark(stream);
}
else
{
audioElem.srcObject = null;
}
if (videoTrack)
{
const stream = new MediaStream;
stream.addTrack(videoTrack);
videoElem.srcObject = stream;
videoElem.oncanplay = () => this.setState({ videoCanPlay: true });
videoElem.onplay = () =>
{
this.setState({ videoElemPaused: false });
audioElem.play()
.catch((error) => logger.warn('audioElem.play() failed:%o', error));
};
videoElem.onpause = () => this.setState({ videoElemPaused: true });
videoElem.play()
.catch((error) => logger.warn('videoElem.play() failed:%o', error));
this._startVideoResolution();
if (faceDetection)
this._startFaceDetection();
}
else
{
videoElem.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 audioVolume = Math.round(Math.pow(10, dBs / 85) * 10);
if (audioVolume === 1)
audioVolume = 0;
if (audioVolume !== this.state.audioVolume)
this.setState({ audioVolume });
});
}
_startVideoResolution()
{
this._videoResolutionPeriodicTimer = setInterval(() =>
{
const { videoResolutionWidth, videoResolutionHeight } = this.state;
const { videoElem } = this.refs;
if (
videoElem.videoWidth !== videoResolutionWidth ||
videoElem.videoHeight !== videoResolutionHeight
)
{
this.setState(
{
videoResolutionWidth : videoElem.videoWidth,
videoResolutionHeight : videoElem.videoHeight
});
}
}, 500);
}
_stopVideoResolution()
{
clearInterval(this._videoResolutionPeriodicTimer);
this.setState(
{
videoResolutionWidth : null,
videoResolutionHeight : null
});
}
_startFaceDetection()
{
const { videoElem, canvas } = this.refs;
const step = async () =>
{
// NOTE: Somehow this is critical. Otherwise the Promise returned by
// faceapi.detectSingleFace() never resolves or rejects.
if (!this._videoTrack || videoElem.readyState < 2)
{
this._faceDetectionRequestAnimationFrame = requestAnimationFrame(step);
return;
}
const detection =
await faceapi.detectSingleFace(videoElem, tinyFaceDetectorOptions);
if (detection)
{
const width = videoElem.offsetWidth;
const height = videoElem.offsetHeight;
canvas.width = width;
canvas.height = height;
// const resizedDetection = detection.forSize(width, height);
const resizedDetections =
faceapi.resizeResults(detection, { width, height });
faceapi.draw.drawDetections(canvas, resizedDetections);
}
else
{
// Trick to hide the canvas rectangle.
canvas.width = 0;
canvas.height = 0;
}
this._faceDetectionRequestAnimationFrame =
requestAnimationFrame(() => setTimeout(step, 100));
};
step();
}
_stopFaceDetection()
{
cancelAnimationFrame(this._faceDetectionRequestAnimationFrame);
const { canvas } = this.refs;
canvas.width = 0;
canvas.height = 0;
}
_printProducerScore(id, score)
{
const scores = Array.isArray(score) ? score : [ score ];
return (
<React.Fragment key={id}>
<p>streams:</p>
{
scores
.sort((a, b) =>
{
if (a.rid)
return (a.rid > b.rid ? 1 : -1);
else
return (a.ssrc > b.ssrc ? 1 : -1);
})
.map(({ ssrc, rid, score }, idx) => ( // eslint-disable-line no-shadow
<p key={idx} className='indent'>
<Choose>
<When condition={rid !== undefined}>
{`rid:${rid}, ssrc:${ssrc}, score:${score}`}
</When>
<Otherwise>
{`ssrc:${ssrc}, score:${score}`}
</Otherwise>
</Choose>
</p>
))
}
</React.Fragment>
);
}
_printConsumerScore(id, score)
{
return (
<p key={id}>
{`score:${score.score}, producerScore:${score.producerScore}, producerScores:[${score.producerScores}]`}
</p>
);
}
}
PeerView.propTypes =
{
isMe : PropTypes.bool,
peer : PropTypes.oneOfType(
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
audioProducerId : PropTypes.string,
videoProducerId : PropTypes.string,
audioConsumerId : PropTypes.string,
videoConsumerId : PropTypes.string,
audioRtpParameters : PropTypes.object,
videoRtpParameters : PropTypes.object,
consumerSpatialLayers : PropTypes.number,
consumerTemporalLayers : PropTypes.number,
consumerCurrentSpatialLayer : PropTypes.number,
consumerCurrentTemporalLayer : PropTypes.number,
consumerPreferredSpatialLayer : PropTypes.number,
consumerPreferredTemporalLayer : PropTypes.number,
consumerPriority : PropTypes.number,
audioTrack : PropTypes.any,
videoTrack : PropTypes.any,
audioMuted : PropTypes.bool,
videoVisible : PropTypes.bool.isRequired,
videoMultiLayer : PropTypes.bool,
audioCodec : PropTypes.string,
videoCodec : PropTypes.string,
audioScore : PropTypes.any,
videoScore : PropTypes.any,
faceDetection : PropTypes.bool.isRequired,
onChangeDisplayName : PropTypes.func,
onChangeMaxSendingSpatialLayer : PropTypes.func,
onChangeVideoPreferredLayers : PropTypes.func,
onChangeVideoPriority : PropTypes.func,
onRequestKeyFrame : PropTypes.func,
onStatsClick : PropTypes.func.isRequired
};

@ -0,0 +1,64 @@
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, activeSpeakerId }) =>
{
return (
<div data-component='Peers'>
{
peers.map((peer) =>
{
return (
<Appear key={peer.id} duration={1000}>
<div
className={classnames('peer-container', {
'active-speaker' : peer.id === activeSpeakerId
})}
>
<Peer id={peer.id} />
</div>
</Appear>
);
})
}
</div>
);
};
Peers.propTypes =
{
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
activeSpeakerId : PropTypes.string
};
const mapStateToProps = (state) =>
{
const peersArray = Object.values(state.peers);
return {
peers : peersArray,
activeSpeakerId : state.room.activeSpeakerId
};
};
const PeersContainer = connect(
mapStateToProps,
null,
null,
{
areStatesEqual : (next, prev) =>
{
return (
prev.peers === next.peers &&
prev.room.activeSpeakerId === next.room.activeSpeakerId
);
}
}
)(Peers);
export default PeersContainer;

@ -0,0 +1,196 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ReactTooltip from 'react-tooltip';
import classnames from 'classnames';
import clipboardCopy from 'clipboard-copy';
import * as appPropTypes from './appPropTypes';
import { withRoomContext } from '../RoomContext';
import * as requestActions from '../redux/requestActions';
import { Appear } from './transitions';
import Me from './Me';
import ChatInput from './ChatInput';
import Peers from './Peers';
import Stats from './Stats';
import Notifications from './Notifications';
import NetworkThrottle from './NetworkThrottle';
class Room extends React.Component
{
render()
{
const {
roomClient,
room,
me,
amActiveSpeaker,
onRoomLinkCopy
} = this.props;
const mediasoupClientVersion = room.mediasoupClientVersion === '__MEDIASOUP_CLIENT_VERSION__'
? 'dev'
: room.mediasoupClientVersion;
return (
<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='info'>
<p className='text'><span className='label'>server:&nbsp;&nbsp;</span>{room.mediasoupVersion}</p>
<p className='text'><span className='label'>client:&nbsp;&nbsp;</span>{mediasoupClientVersion}</p>
<p className='text'><span className='label'>handler:&nbsp;&nbsp;</span>{room.mediasoupClientHandler}</p>
</div>
<div className='room-link-wrapper'>
<div className='room-link'>
<a
className='link'
href={room.url}
target='_blank'
rel='noopener noreferrer'
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();
clipboardCopy(room.url)
.then(onRoomLinkCopy);
}}
>
invitation link
</a>
</div>
</div>
<Peers />
<div
className={classnames('me-container', {
'active-speaker' : amActiveSpeaker
})}
>
<Me />
</div>
<div className='chat-input-container'>
<ChatInput />
</div>
<div className='sidebar'>
<div
className={classnames('button', 'hide-videos', {
on : me.audioOnly,
disabled : me.audioOnlyInProgress
})}
data-tip={'Show/hide participants\' video'}
onClick={() =>
{
me.audioOnly
? roomClient.disableAudioOnly()
: roomClient.enableAudioOnly();
}}
/>
<div
className={classnames('button', 'mute-audio', {
on : me.audioMuted
})}
data-tip={'Mute/unmute participants\' audio'}
onClick={() =>
{
me.audioMuted
? roomClient.unmuteAudio()
: roomClient.muteAudio();
}}
/>
<div
className={classnames('button', 'restart-ice', {
disabled : me.restartIceInProgress
})}
data-tip='Restart ICE'
onClick={() => roomClient.restartIce()}
/>
</div>
<Stats />
<If condition={window.NETWORK_THROTTLE_SECRET}>
<NetworkThrottle
secret={window.NETWORK_THROTTLE_SECRET}
/>
</If>
<ReactTooltip
type='light'
effect='solid'
delayShow={100}
delayHide={100}
delayUpdate={50}
/>
</div>
</Appear>
);
}
componentDidMount()
{
const { roomClient } = this.props;
roomClient.join();
}
}
Room.propTypes =
{
roomClient : PropTypes.any.isRequired,
room : appPropTypes.Room.isRequired,
me : appPropTypes.Me.isRequired,
amActiveSpeaker : PropTypes.bool.isRequired,
onRoomLinkCopy : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
room : state.room,
me : state.me,
amActiveSpeaker : state.me.id === state.room.activeSpeakerId
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onRoomLinkCopy : () =>
{
dispatch(requestActions.notify(
{
text : 'Room link copied to the clipboard'
}));
}
};
};
const RoomContainer = withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(Room));
export default RoomContainer;

@ -0,0 +1,531 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Appear } from './transitions';
import { withRoomContext } from '../RoomContext';
import * as stateActions from '../redux/stateActions';
class Stats extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
sendTransportRemoteStats : null,
sendTransportLocalStats : null,
recvTransportRemoteStats : null,
recvTransportLocalStats : null,
audioProducerRemoteStats : null,
audioProducerLocalStats : null,
videoProducerRemoteStats : null,
videoProducerLocalStats : null,
chatDataProducerRemoteStats : null,
botDataProducerRemoteStats : null,
audioConsumerRemoteStats : null,
audioConsumerLocalStats : null,
videoConsumerRemoteStats : null,
videoConsumerLocalStats : null,
chatDataConsumerRemoteStats : null,
botDataConsumerRemoteStats : null
};
this._delayTimer = null;
}
render()
{
const {
peerId,
peerDisplayName,
isMe,
onClose
} = this.props;
const {
sendTransportRemoteStats,
sendTransportLocalStats,
recvTransportRemoteStats,
recvTransportLocalStats,
audioProducerRemoteStats,
audioProducerLocalStats,
videoProducerRemoteStats,
videoProducerLocalStats,
chatDataProducerRemoteStats,
botDataProducerRemoteStats,
audioConsumerRemoteStats,
audioConsumerLocalStats,
videoConsumerRemoteStats,
videoConsumerLocalStats,
chatDataConsumerRemoteStats,
botDataConsumerRemoteStats
} = this.state;
return (
<div data-component='Stats'>
<div className={classnames('content', { visible: peerId })}>
<div className='header'>
<div className='info'>
<div
className='close-icon'
onClick={onClose}
/>
<Choose>
<When condition={isMe}>
<h1>Your Stats</h1>
</When>
<Otherwise>
<h1>Stats of {peerDisplayName}</h1>
</Otherwise>
</Choose>
</div>
<div className='list'>
<If condition={sendTransportRemoteStats || sendTransportLocalStats}>
<p>
{'send transport stats: '}
<a href='#send-transport-remote-stats'>[remote]</a>
<span>{' '}</span>
<a href='#send-transport-local-stats'>[local]</a>
</p>
</If>
<If condition={recvTransportRemoteStats || recvTransportLocalStats}>
<p>
{'recv transport stats: '}
<a href='#recv-transport-remote-stats'>[remote]</a>
<span>{' '}</span>
<a href='#recv-transport-local-stats'>[local]</a>
</p>
</If>
<If condition={audioProducerRemoteStats || audioProducerLocalStats}>
<p>
{'audio producer stats: '}
<a href='#audio-producer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a href='#audio-producer-local-stats'>[local]</a>
</p>
</If>
<If condition={videoProducerRemoteStats || videoProducerLocalStats}>
<p>
{'video producer stats: '}
<a href='#video-producer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a href='#video-producer-local-stats'>[local]</a>
</p>
</If>
<If condition={chatDataProducerRemoteStats}>
<p>
{'chat dataproducer stats: '}
<a href='#chat-dataproducer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a className='disabled'>[local]</a>
</p>
</If>
<If condition={botDataProducerRemoteStats}>
<p>
{'bot dataproducer stats: '}
<a href='#bot-dataproducer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a className='disabled'>[local]</a>
</p>
</If>
<If condition={audioConsumerRemoteStats || audioConsumerLocalStats}>
<p>
{'audio consumer stats: '}
<a href='#audio-consumer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a href='#audio-consumer-local-stats'>[local]</a>
</p>
</If>
<If condition={videoConsumerRemoteStats || videoConsumerLocalStats}>
<p>
{'video consumer stats: '}
<a href='#video-consumer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a href='#video-consumer-local-stats'>[local]</a>
</p>
</If>
<If condition={chatDataConsumerRemoteStats}>
<p>
{'chat dataconsumer stats: '}
<a href='#chat-dataconsumer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a className='disabled'>[local]</a>
</p>
</If>
<If condition={botDataConsumerRemoteStats}>
<p>
{'bot dataconsumer stats: '}
<a href='#bot-dataconsumer-remote-stats'>[remote]</a>
<span>{' '}</span>
<a className='disabled'>[local]</a>
</p>
</If>
</div>
</div>
<div className='stats'>
<If condition={sendTransportRemoteStats}>
{this._printStats('send transport remote stats', sendTransportRemoteStats)}
</If>
<If condition={sendTransportLocalStats}>
{this._printStats('send transport local stats', sendTransportLocalStats)}
</If>
<If condition={recvTransportRemoteStats}>
{this._printStats('recv transport remote stats', recvTransportRemoteStats)}
</If>
<If condition={recvTransportLocalStats}>
{this._printStats('recv transport local stats', recvTransportLocalStats)}
</If>
<If condition={audioProducerRemoteStats}>
{this._printStats('audio producer remote stats', audioProducerRemoteStats)}
</If>
<If condition={audioProducerLocalStats}>
{this._printStats('audio producer local stats', audioProducerLocalStats)}
</If>
<If condition={videoProducerRemoteStats}>
{this._printStats('video producer remote stats', videoProducerRemoteStats)}
</If>
<If condition={videoProducerLocalStats}>
{this._printStats('video producer local stats', videoProducerLocalStats)}
</If>
<If condition={chatDataProducerRemoteStats}>
{this._printStats('chat dataproducer remote stats', chatDataProducerRemoteStats)}
</If>
<If condition={botDataProducerRemoteStats}>
{this._printStats('bot dataproducer remote stats', botDataProducerRemoteStats)}
</If>
<If condition={audioConsumerRemoteStats}>
{this._printStats('audio consumer remote stats', audioConsumerRemoteStats)}
</If>
<If condition={audioConsumerLocalStats}>
{this._printStats('audio consumer local stats', audioConsumerLocalStats)}
</If>
<If condition={videoConsumerRemoteStats}>
{this._printStats('video consumer remote stats', videoConsumerRemoteStats)}
</If>
<If condition={videoConsumerLocalStats}>
{this._printStats('video consumer local stats', videoConsumerLocalStats)}
</If>
<If condition={chatDataConsumerRemoteStats}>
{this._printStats('chat dataconsumer remote stats', chatDataConsumerRemoteStats)}
</If>
<If condition={botDataConsumerRemoteStats}>
{this._printStats('bot dataconsumer remote stats', botDataConsumerRemoteStats)}
</If>
</div>
</div>
</div>
);
}
componentDidUpdate(prevProps)
{
const { peerId } = this.props;
if (peerId && !prevProps.peerId)
{
this._delayTimer = setTimeout(() => this._start(), 250);
}
else if (!peerId && prevProps.peerId)
{
this._stop();
}
else if (peerId && prevProps.peerId && peerId !== prevProps.peerId)
{
this._stop();
this._start();
}
}
async _start()
{
const {
roomClient,
isMe,
audioConsumerId,
videoConsumerId,
chatDataConsumerId,
botDataConsumerId
} = this.props;
let sendTransportRemoteStats = null;
let sendTransportLocalStats = null;
let recvTransportRemoteStats = null;
let recvTransportLocalStats = null;
let audioProducerRemoteStats = null;
let audioProducerLocalStats = null;
let videoProducerRemoteStats = null;
let videoProducerLocalStats = null;
let chatDataProducerRemoteStats = null;
let botDataProducerRemoteStats = null;
let audioConsumerRemoteStats = null;
let audioConsumerLocalStats = null;
let videoConsumerRemoteStats = null;
let videoConsumerLocalStats = null;
let chatDataConsumerRemoteStats = null;
let botDataConsumerRemoteStats = null;
if (isMe)
{
sendTransportRemoteStats = await roomClient.getSendTransportRemoteStats()
.catch(() => {});
sendTransportLocalStats = await roomClient.getSendTransportLocalStats()
.catch(() => {});
recvTransportRemoteStats = await roomClient.getRecvTransportRemoteStats()
.catch(() => {});
recvTransportLocalStats = await roomClient.getRecvTransportLocalStats()
.catch(() => {});
audioProducerRemoteStats = await roomClient.getAudioRemoteStats()
.catch(() => {});
audioProducerLocalStats = await roomClient.getAudioLocalStats()
.catch(() => {});
videoProducerRemoteStats = await roomClient.getVideoRemoteStats()
.catch(() => {});
videoProducerLocalStats = await roomClient.getVideoLocalStats()
.catch(() => {});
chatDataProducerRemoteStats = await roomClient.getChatDataProducerRemoteStats()
.catch(() => {});
botDataProducerRemoteStats = await roomClient.getBotDataProducerRemoteStats()
.catch(() => {});
botDataConsumerRemoteStats =
await roomClient.getDataConsumerRemoteStats(botDataConsumerId)
.catch(() => {});
}
else
{
audioConsumerRemoteStats = await roomClient.getConsumerRemoteStats(audioConsumerId)
.catch(() => {});
audioConsumerLocalStats = await roomClient.getConsumerLocalStats(audioConsumerId)
.catch(() => {});
videoConsumerRemoteStats = await roomClient.getConsumerRemoteStats(videoConsumerId)
.catch(() => {});
videoConsumerLocalStats = await roomClient.getConsumerLocalStats(videoConsumerId)
.catch(() => {});
chatDataConsumerRemoteStats =
await roomClient.getDataConsumerRemoteStats(chatDataConsumerId)
.catch(() => {});
}
this.setState(
{
sendTransportRemoteStats,
sendTransportLocalStats,
recvTransportRemoteStats,
recvTransportLocalStats,
audioProducerRemoteStats,
audioProducerLocalStats,
videoProducerRemoteStats,
videoProducerLocalStats,
chatDataProducerRemoteStats,
botDataProducerRemoteStats,
audioConsumerRemoteStats,
audioConsumerLocalStats,
videoConsumerRemoteStats,
videoConsumerLocalStats,
chatDataConsumerRemoteStats,
botDataConsumerRemoteStats
});
this._delayTimer = setTimeout(() => this._start(), 2500);
}
_stop()
{
clearTimeout(this._delayTimer);
this.setState(
{
sendTransportRemoteStats : null,
sendTransportLocalStats : null,
recvTransportRemoteStats : null,
recvTransportLocalStats : null,
audioProducerRemoteStats : null,
audioProducerLocalStats : null,
videoProducerRemoteStats : null,
videoProducerLocalStats : null,
chatDataProducerRemoteStats : null,
botDataProducerRemoteStats : null,
audioConsumerRemoteStats : null,
audioConsumerLocalStats : null,
videoConsumerRemoteStats : null,
videoConsumerLocalStats : null,
chatDataConsumerRemoteStats : null,
botDataConsumerRemoteStats : null
});
}
_printStats(title, stats)
{
const anchor = title
.replace(/[ ]+/g, '-');
if (typeof stats.values === 'function')
stats = Array.from(stats.values());
return (
<Appear duration={150}>
<div className='items'>
<h2 id={anchor}>{title}</h2>
{
stats.map((item, idx) => (
<div className='item' key={idx}>
{
Object.keys(item).map((key) => (
<div className='line' key={key}>
<p className='key'>{key}</p>
<div className='value'>
<Choose>
<When condition={typeof item[key] === 'number'}>
{JSON.stringify(Math.round(item[key] * 100) / 100, null, ' ')}
</When>
<Otherwise>
<pre>{JSON.stringify(item[key], null, ' ')}</pre>
</Otherwise>
</Choose>
</div>
</div>
))
}
</div>
))
}
</div>
</Appear>
);
}
}
Stats.propTypes =
{
roomClient : PropTypes.any.isRequired,
peerId : PropTypes.string,
peerDisplayName : PropTypes.string,
isMe : PropTypes.bool,
audioConsumerId : PropTypes.string,
videoConsumerId : PropTypes.string,
chatDataConsumerId : PropTypes.string,
botDataConsumerId : PropTypes.string,
onClose : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
const { room, me, peers, consumers, dataConsumers } = state;
const { statsPeerId } = room;
if (!statsPeerId)
return {};
const isMe = statsPeerId === me.id;
const peer = isMe ? me : peers[statsPeerId];
let audioConsumerId;
let videoConsumerId;
let chatDataConsumerId;
let botDataConsumerId;
if (isMe)
{
for (const dataConsumerId of Object.keys(dataConsumers))
{
const dataConsumer = dataConsumers[dataConsumerId];
if (dataConsumer.label === 'bot')
botDataConsumerId = dataConsumer.id;
}
}
else
{
for (const consumerId of peer.consumers)
{
const consumer = consumers[consumerId];
switch (consumer.track.kind)
{
case 'audio':
audioConsumerId = consumer.id;
break;
case 'video':
videoConsumerId = consumer.id;
break;
}
}
for (const dataConsumerId of peer.dataConsumers)
{
const dataConsumer = dataConsumers[dataConsumerId];
if (dataConsumer.label === 'chat')
chatDataConsumerId = dataConsumer.id;
}
}
return {
peerId : peer.id,
peerDisplayName : peer.displayName,
isMe,
audioConsumerId,
videoConsumerId,
chatDataConsumerId,
botDataConsumerId
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onClose : () => dispatch(stateActions.setRoomStatsPeerId(null))
};
};
const StatsContainer = withRoomContext(connect(
mapStateToProps,
mapDispatchToProps
)(Stats));
export default StatsContainer;

@ -0,0 +1,80 @@
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,
version : PropTypes.string
});
export const Me = PropTypes.shape(
{
id : 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.string.isRequired,
deviceLabel : PropTypes.string,
type : PropTypes.oneOf([ 'front', 'back', 'share' ]),
paused : PropTypes.bool.isRequired,
track : PropTypes.any.isRequired,
rtpParameters : PropTypes.object.isRequired,
codec : PropTypes.string.isRequired
});
export const DataProducer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
sctpStreamParameters : PropTypes.object.isRequired
});
export const Peer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
displayName : PropTypes.string,
device : Device.isRequired,
consumers : PropTypes.arrayOf(PropTypes.string).isRequired
});
export const Consumer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
locallyPaused : PropTypes.bool.isRequired,
remotelyPaused : PropTypes.bool.isRequired,
currentSpatialLayer : PropTypes.number,
preferredSpatialLayer : PropTypes.number,
track : PropTypes.any,
codec : PropTypes.string
});
export const DataConsumer = PropTypes.shape(
{
id : PropTypes.string.isRequired,
sctpStreamParameters : PropTypes.object.isRequired
});
export const Notification = PropTypes.shape(
{
id : PropTypes.string.isRequired,
type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
timeout : PropTypes.number
});

@ -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 });
}

@ -0,0 +1,30 @@
import bowser from 'bowser';
// TODO: For testing.
window.BOWSER = bowser;
export default function()
{
const ua = navigator.userAgent;
const browser = bowser.getParser(ua);
let flag;
if (browser.satisfies({ chrome: '>=0', chromium: '>=0' }))
flag = 'chrome';
else if (browser.satisfies({ firefox: '>=0' }))
flag = 'firefox';
else if (browser.satisfies({ safari: '>=0' }))
flag = 'safari';
else if (browser.satisfies({ opera: '>=0' }))
flag = 'opera';
else if (browser.satisfies({ 'microsoft edge': '>=0' }))
flag = 'edge';
else
flag = 'unknown';
return {
flag,
name : browser.getBrowserName(),
version : browser.getBrowserVersion()
};
}

@ -0,0 +1,111 @@
/**
* Insertable streams.
*
* https://github.com/webrtc/samples/blob/gh-pages/src/content/insertable-streams/endtoend-encryption/js/main.js
*/
import Logger from './Logger';
const logger = new Logger('e2e');
let e2eSupported = undefined;
let worker = undefined;
export function isSupported()
{
if (e2eSupported === undefined)
{
if (RTCRtpSender.prototype.createEncodedStreams)
{
try
{
const stream = new ReadableStream();
window.postMessage(stream, '*', [ stream ]);
worker = new Worker('/resources/js/e2e-worker.js', { name: 'e2e worker' });
logger.debug('isSupported() | supported');
e2eSupported = true;
}
catch (error)
{
logger.debug(`isSupported() | not supported: ${error}`);
e2eSupported = false;
}
}
else
{
logger.debug('isSupported() | not supported');
e2eSupported = false;
}
}
return e2eSupported;
}
export function setCryptoKey(operation, key, useCryptoOffset)
{
logger.debug(
'setCryptoKey() [operation:%o, useCryptoOffset:%o]',
operation, useCryptoOffset);
assertSupported();
worker.postMessage(
{
operation : operation,
currentCryptoKey : key,
useCryptoOffset : useCryptoOffset
});
}
export function setupSenderTransform(sender)
{
logger.debug('setupSenderTransform()');
assertSupported();
const senderStreams = sender.createEncodedStreams();
const readableStream = senderStreams.readable || senderStreams.readableStream;
const writableStream = senderStreams.writable || senderStreams.writableStream;
worker.postMessage(
{
operation : 'encode',
readableStream,
writableStream
},
[ readableStream, writableStream ]
);
}
export function setupReceiverTransform(receiver)
{
logger.debug('setupReceiverTransform()');
assertSupported();
const receiverStreams = receiver.createEncodedStreams();
const readableStream = receiverStreams.readable || receiverStreams.readableStream;
const writableStream = receiverStreams.writable || receiverStreams.writableStream;
worker.postMessage(
{
operation : 'decode',
readableStream,
writableStream
},
[ readableStream, writableStream ]
);
}
function assertSupported()
{
if (e2eSupported === false)
throw new Error('e2e not supported');
else if (e2eSupported === undefined)
throw new Error('e2e not initialized, must call isSupported() first');
}

@ -0,0 +1,351 @@
import domready from 'domready';
import UrlParse from 'url-parse';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import {
applyMiddleware as applyReduxMiddleware,
createStore as createReduxStore
} from 'redux';
import thunk from 'redux-thunk';
// import { createLogger as createReduxLogger } from 'redux-logger';
import randomString from 'random-string';
import * as faceapi from 'face-api.js';
import Logger from './Logger';
import * as utils from './utils';
import randomName from './randomName';
import deviceInfo from './deviceInfo';
import RoomClient from './RoomClient';
import RoomContext from './RoomContext';
import * as cookiesManager from './cookiesManager';
import * as stateActions from './redux/stateActions';
import reducers from './redux/reducers';
import Room from './components/Room';
const logger = new Logger();
const reduxMiddlewares = [ thunk ];
// if (process.env.NODE_ENV === 'development')
// {
// const reduxLogger = createReduxLogger(
// {
// duration : true,
// timestamp : false,
// level : 'log',
// logErrors : true
// });
// reduxMiddlewares.push(reduxLogger);
// }
let roomClient;
const store = createReduxStore(
reducers,
undefined,
applyReduxMiddleware(...reduxMiddlewares)
);
window.STORE = store;
RoomClient.init({ store });
domready(async () =>
{
logger.debug('DOM ready');
await utils.initialize();
run();
});
async function run()
{
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
const urlParser = new UrlParse(window.location.href, true);
const peerId = randomString({ length: 8 }).toLowerCase();
let roomId = urlParser.query.roomId;
let displayName =
urlParser.query.displayName || (cookiesManager.getUser() || {}).displayName;
const handlerName = urlParser.query.handlerName || urlParser.query.handler;
const forceTcp = urlParser.query.forceTcp === 'true';
const produce = urlParser.query.produce !== 'false';
const consume = urlParser.query.consume !== 'false';
const datachannel = urlParser.query.datachannel !== 'false';
const forceVP8 = urlParser.query.forceVP8 === 'true';
const forceH264 = urlParser.query.forceH264 === 'true';
const forceVP9 = urlParser.query.forceVP9 === 'true';
const enableWebcamLayers = urlParser.query.enableWebcamLayers !== 'false';
const enableSharingLayers = urlParser.query.enableSharingLayers !== 'false';
const webcamScalabilityMode = urlParser.query.webcamScalabilityMode;
const sharingScalabilityMode = urlParser.query.sharingScalabilityMode;
const numSimulcastStreams = urlParser.query.numSimulcastStreams ?
Number(urlParser.query.numSimulcastStreams) : 3;
const info = urlParser.query.info === 'true';
const faceDetection = urlParser.query.faceDetection === 'true';
const externalVideo = urlParser.query.externalVideo === 'true';
const throttleSecret = urlParser.query.throttleSecret;
const e2eKey = urlParser.query.e2eKey;
const consumerReplicas = urlParser.query.consumerReplicas;
// Enable face detection on demand.
if (faceDetection)
await faceapi.loadTinyFaceDetectorModel('/resources/face-detector-models');
if (info)
{
// eslint-disable-next-line require-atomic-updates
window.SHOW_INFO = true;
}
if (throttleSecret)
{
// eslint-disable-next-line require-atomic-updates
window.NETWORK_THROTTLE_SECRET = throttleSecret;
}
if (!roomId)
{
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 'handlerName':
case 'handler':
case 'forceTcp':
case 'produce':
case 'consume':
case 'datachannel':
case 'forceVP8':
case 'forceH264':
case 'forceVP9':
case 'enableWebcamLayers':
case 'enableSharingLayers':
case 'webcamScalabilityMode':
case 'sharingScalabilityMode':
case 'numSimulcastStreams':
case 'info':
case 'faceDetection':
case 'externalVideo':
case 'throttleSecret':
case 'e2eKey':
case 'consumerReplicas':
{
break;
}
default:
{
delete roomUrlParser.query[key];
}
}
}
delete roomUrlParser.hash;
const roomUrl = roomUrlParser.toString();
let displayNameSet;
// If displayName was provided via URL or Cookie, we are done.
if (displayName)
{
displayNameSet = true;
}
// Otherwise pick a random name and mark as "not set".
else
{
displayNameSet = false;
displayName = randomName();
}
// Get current device info.
const device = deviceInfo();
store.dispatch(
stateActions.setRoomUrl(roomUrl));
store.dispatch(
stateActions.setRoomFaceDetection(faceDetection));
store.dispatch(
stateActions.setMe({ peerId, displayName, displayNameSet, device }));
roomClient = new RoomClient(
{
roomId,
peerId,
displayName,
device,
handlerName : handlerName,
forceTcp,
produce,
consume,
datachannel,
forceVP8,
forceH264,
forceVP9,
enableWebcamLayers,
enableSharingLayers,
webcamScalabilityMode,
sharingScalabilityMode,
numSimulcastStreams,
externalVideo,
e2eKey,
consumerReplicas
});
// NOTE: For debugging.
// eslint-disable-next-line require-atomic-updates
window.CLIENT = roomClient;
// eslint-disable-next-line require-atomic-updates
window.CC = roomClient;
render(
<Provider store={store}>
<RoomContext.Provider value={roomClient}>
<Room />
</RoomContext.Provider>
</Provider>,
document.getElementById('mediasoup-demo-app-container')
);
}
// NOTE: Debugging stuff.
window.__sendSdps = function()
{
logger.warn('>>> send transport local SDP offer:');
logger.warn(
roomClient._sendTransport._handler._pc.localDescription.sdp);
logger.warn('>>> send transport remote SDP answer:');
logger.warn(
roomClient._sendTransport._handler._pc.remoteDescription.sdp);
};
window.__recvSdps = function()
{
logger.warn('>>> recv transport remote SDP offer:');
logger.warn(
roomClient._recvTransport._handler._pc.remoteDescription.sdp);
logger.warn('>>> recv transport local SDP answer:');
logger.warn(
roomClient._recvTransport._handler._pc.localDescription.sdp);
};
let dataChannelTestInterval = null;
window.__startDataChannelTest = function()
{
let number = 0;
const buffer = new ArrayBuffer(32);
const view = new DataView(buffer);
dataChannelTestInterval = window.setInterval(() =>
{
if (window.DP)
{
view.setUint32(0, number++);
roomClient.sendChatMessage(buffer);
}
}, 100);
};
window.__stopDataChannelTest = function()
{
window.clearInterval(dataChannelTestInterval);
const buffer = new ArrayBuffer(32);
const view = new DataView(buffer);
if (window.DP)
{
view.setUint32(0, Math.pow(2, 32) - 1);
window.DP.send(buffer);
}
};
window.__testSctp = async function({ timeout = 100, bot = false } = {})
{
let dp;
if (!bot)
{
await window.CLIENT.enableChatDataProducer();
dp = window.CLIENT._chatDataProducer;
}
else
{
await window.CLIENT.enableBotDataProducer();
dp = window.CLIENT._botDataProducer;
}
logger.warn(
'<<< testSctp: DataProducer created [bot:%s, streamId:%d, readyState:%s]',
bot ? 'true' : 'false',
dp.sctpStreamParameters.streamId,
dp.readyState);
function send()
{
dp.send(`I am streamId ${dp.sctpStreamParameters.streamId}`);
}
if (dp.readyState === 'open')
{
send();
}
else
{
dp.on('open', () =>
{
logger.warn(
'<<< testSctp: DataChannel open [streamId:%d]',
dp.sctpStreamParameters.streamId);
send();
});
}
setTimeout(() => window.__testSctp({ timeout, bot }), timeout);
};
setInterval(() =>
{
if (window.CLIENT._sendTransport)
{
window.H1 = window.CLIENT._sendTransport._handler;
window.PC1 = window.CLIENT._sendTransport._handler._pc;
window.DP = window.CLIENT._chatDataProducer;
}
else
{
delete window.PC1;
delete window.DP;
}
if (window.CLIENT._recvTransport)
{
window.H2 = window.CLIENT._recvTransport._handler;
window.PC2 = window.CLIENT._recvTransport._handler._pc;
}
else
{
delete window.PC2;
}
}, 2000);

@ -0,0 +1,43 @@
import * as pokemon from 'pokemon';
export default function()
{
const lang = detectLanguage();
return pokemon.random(lang);
}
// TODO: pokemon lib does not work with browserify (it just loads 'en' language)
// so let's just use 'en'.
//
// https://github.com/versatica/mediasoup-demo/issues/45
function detectLanguage()
{
return 'en';
// const lang = (
// (navigator.languages && navigator.languages[0]) ||
// navigator.language ||
// navigator.userLanguage
// );
// if (!lang)
// return 'en';
// if (/^en/i.test(lang))
// return 'en';
// else if (/^de/i.test(lang))
// return 'de';
// else if (/^fr/i.test(lang))
// return 'fr';
// else if (/^ja/i.test(lang))
// return 'ja';
// else if (/^ko/i.test(lang))
// return 'ko';
// else if (/^ru/i.test(lang))
// return 'ru';
// else if (/^de/i.test(lang))
// return 'de';
// else
// return 'en';
}

@ -0,0 +1,150 @@
# APP STATE
```js
{
room :
{
url : 'https://demo.mediasoup.org/?roomId=d0el8y34',
mediasoupVersion : null,
mediasoupClientVersion : null
state : 'connected', // new/connecting/connected/closed
activeSpeakerId : 'alice',
statsPeerId : null,
faceDetection : false
},
me :
{
id : '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,
shareInProgress : false,
audioOnly : false,
audioOnlyInProgress : false,
audioMuted : false,
restartIceInProgress : false
},
producers :
{
'1111-qwer' :
{
id : '1111-qwer',
paused : true,
track : MediaStreamTrack,
codec : 'opus',
rtpParameters : {},
score : [ { ssrc: 1111, score: 10 } ]
},
'1112-asdf' :
{
id : '1112-asdf',
deviceLabel : 'Macbook Webcam',
type : 'front', // front/back/share
paused : false,
track : MediaStreamTrack,
codec : 'vp8',
score : [ { ssrc: 2221, score: 10 }, { ssrc: 2222, score: 9 } ]
}
},
dataProducers :
{
'4444-4444' :
{
id : '4444-4444',
sctpStreamParameters : {},
label : 'chat',
protocol : ''
},
'4444-4445' :
{
id : '4444-4445',
sctpStreamParameters : {},
label : 'bot',
protocol : ''
}
},
peers :
{
'alice' :
{
id : 'alice',
displayName : 'Alice Thomsom',
device : { flag: 'chrome', name: 'Chrome', version: '58' },
consumers : [ '5551-qwer', '5552-zxzx' ],
dataConsumers : [ '6661-asdf' ]
}
},
consumers :
{
'5551-qwer' :
{
id : '5551-qwer',
type : 'simple',
locallyPaused : false,
remotelyPaused : false,
rtpParameters : {},
codec : 'opus',
spatialLayers : 1,
temporalLayers : 1,
currentSpatialLayer : undefined,
currentTemporalLayer : undefined,
preferredSpatialLayer : undefined,
preferredTemporalLayer : undefined,
priority : 1,
track : MediaStreamTrack,
score : [ { ssrc: 3331, score: 10 } ]
},
'5552-zxzx' :
{
id : '5552-zxzx',
type : 'simulcast',
locallyPaused : false,
remotelyPaused : true,
rtpParameters : {},
codec : 'h264',
spatialLayers : 1,
temporalLayers : 1,
currentSpatialLayer : 1,
currentTemporalLayer : 1,
preferredSpatialLayer : 2,
preferredTemporalLayer : 2,
priority : 2,
track : MediaStreamTrack,
score : [ { ssrc: 4441, score: 9 }, { ssrc: 4444, score: 8 } ]
}
},
dataConsumers :
{
'5551-qwer' :
{
id : '5551-qwer',
sctpStreamParameters : {},
label : 'chat',
protocol : 'something'
},
'5552-qwer' :
{
id : '5552-qwer',
sctpStreamParameters : {},
label : 'bot'
}
},
notifications :
[
{
id : 'qweasdw43we',
type : 'info' // info/error
text : 'You joined the room'
},
{
id : 'j7sdhkjjkcc',
type : 'error'
text : 'Could not add webcam'
}
]
}
```

@ -0,0 +1,128 @@
const initialState = {};
const consumers = (state = initialState, action) =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
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_CURRENT_LAYERS':
{
const { consumerId, spatialLayer, temporalLayer } = action.payload;
const consumer = state[consumerId];
const newConsumer =
{
...consumer,
currentSpatialLayer : spatialLayer,
currentTemporalLayer : temporalLayer
};
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_PREFERRED_LAYERS':
{
const { consumerId, spatialLayer, temporalLayer } = action.payload;
const consumer = state[consumerId];
const newConsumer =
{
...consumer,
preferredSpatialLayer : spatialLayer,
preferredTemporalLayer : temporalLayer
};
return { ...state, [consumerId]: newConsumer };
}
case 'SET_CONSUMER_PRIORITY':
{
const { consumerId, priority } = action.payload;
const consumer = state[consumerId];
const newConsumer = { ...consumer, priority };
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 };
}
case 'SET_CONSUMER_SCORE':
{
const { consumerId, score } = action.payload;
const consumer = state[consumerId];
if (!consumer)
return state;
const newConsumer = { ...consumer, score };
return { ...state, [consumerId]: newConsumer };
}
default:
{
return state;
}
}
};
export default consumers;

@ -0,0 +1,41 @@
const initialState = {};
const dataConsumers = (state = initialState, action) =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_DATA_CONSUMER':
{
const { dataConsumer } = action.payload;
return { ...state, [dataConsumer.id]: dataConsumer };
}
case 'REMOVE_DATA_CONSUMER':
{
const { dataConsumerId } = action.payload;
const newState = { ...state };
delete newState[dataConsumerId];
return newState;
}
default:
{
return state;
}
}
};
export default dataConsumers;

@ -0,0 +1,41 @@
const initialState = {};
const dataProducers = (state = initialState, action) =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_DATA_PRODUCER':
{
const { dataProducer } = action.payload;
return { ...state, [dataProducer.id]: dataProducer };
}
case 'REMOVE_DATA_PRODUCER':
{
const { dataProducerId } = action.payload;
const newState = { ...state };
delete newState[dataProducerId];
return newState;
}
default:
{
return state;
}
}
};
export default dataProducers;

@ -0,0 +1,23 @@
import { combineReducers } from 'redux';
import room from './room';
import me from './me';
import producers from './producers';
import dataProducers from './dataProducers';
import peers from './peers';
import consumers from './consumers';
import dataConsumers from './dataConsumers';
import notifications from './notifications';
const reducers = combineReducers(
{
room,
me,
producers,
dataProducers,
peers,
consumers,
dataConsumers,
notifications
});
export default reducers;

@ -0,0 +1,123 @@
const initialState =
{
id : null,
displayName : null,
displayNameSet : false,
device : null,
canSendMic : false,
canSendWebcam : false,
canChangeWebcam : false,
webcamInProgress : false,
shareInProgress : false,
audioOnly : false,
audioOnlyInProgress : false,
audioMuted : false,
restartIceInProgress : false
};
const me = (state = initialState, action) =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
{
return {
...state,
webcamInProgress : false,
shareInProgress : false,
audioOnly : false,
audioOnlyInProgress : false,
audioMuted : false,
restartIceInProgress : false
};
}
else
{
return state;
}
}
case 'SET_ME':
{
const { peerId, displayName, displayNameSet, device } = action.payload;
return { ...state, id: peerId, 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_SHARE_IN_PROGRESS':
{
const { flag } = action.payload;
return { ...state, shareInProgress: 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_AUDIO_MUTED_STATE':
{
const { enabled } = action.payload;
return { ...state, audioMuted: enabled };
}
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,138 @@
const initialState = {};
const peers = (state = initialState, action) =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
case 'ADD_PEER':
{
const { peer } = action.payload;
return { ...state, [peer.id]: peer };
}
case 'REMOVE_PEER':
{
const { peerId } = action.payload;
const newState = { ...state };
delete newState[peerId];
return newState;
}
case 'SET_PEER_DISPLAY_NAME':
{
const { displayName, peerId } = action.payload;
const peer = state[peerId];
if (!peer)
throw new Error('no Peer found');
const newPeer = { ...peer, displayName };
return { ...state, [newPeer.id]: newPeer };
}
case 'ADD_CONSUMER':
{
const { consumer, peerId } = action.payload;
const peer = state[peerId];
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.id]: newPeer };
}
case 'REMOVE_CONSUMER':
{
const { consumerId, peerId } = action.payload;
const peer = state[peerId];
// 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.id]: newPeer };
}
case 'ADD_DATA_CONSUMER':
{
const { dataConsumer, peerId } = action.payload;
// special case for bot DataConsumer.
if (!peerId)
return state;
const peer = state[peerId];
if (!peer)
throw new Error('no Peer found for new DataConsumer');
const newDataConsumers = [ ...peer.dataConsumers, dataConsumer.id ];
const newPeer = { ...peer, dataConsumers: newDataConsumers };
return { ...state, [newPeer.id]: newPeer };
}
case 'REMOVE_DATA_CONSUMER':
{
const { dataConsumerId, peerId } = action.payload;
// special case for bot DataConsumer.
if (!peerId)
return state;
const peer = state[peerId];
// NOTE: This means that the Peer was closed before, so it's ok.
if (!peer)
return state;
const idx = peer.dataConsumers.indexOf(dataConsumerId);
if (idx === -1)
throw new Error('DataConsumer not found');
const newDataConsumers = peer.dataConsumers.slice();
newDataConsumers.splice(idx, 1);
const newPeer = { ...peer, dataConsumers: newDataConsumers };
return { ...state, [newPeer.id]: newPeer };
}
default:
{
return state;
}
}
};
export default peers;

@ -0,0 +1,81 @@
const initialState = {};
const producers = (state = initialState, action) =>
{
switch (action.type)
{
case 'SET_ROOM_STATE':
{
const roomState = action.payload.state;
if (roomState === 'closed')
return {};
else
return state;
}
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 } = action.payload;
const producer = state[producerId];
const newProducer = { ...producer, paused: true };
return { ...state, [producerId]: newProducer };
}
case 'SET_PRODUCER_RESUMED':
{
const { producerId } = action.payload;
const producer = state[producerId];
const newProducer = { ...producer, paused: 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 };
}
case 'SET_PRODUCER_SCORE':
{
const { producerId, score } = action.payload;
const producer = state[producerId];
if (!producer)
return state;
const newProducer = { ...producer, score };
return { ...state, [producerId]: newProducer };
}
default:
{
return state;
}
}
};
export default producers;

@ -0,0 +1,100 @@
const initialState =
{
url : null,
state : 'new', // new/connecting/connected/disconnected/closed,
mediasoupVersion : null,
mediasoupClientVersion : null,
mediasoupClientHandler : undefined,
activeSpeakerId : null,
statsPeerId : null,
faceDetection : false
};
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, activeSpeakerId: null, statsPeerId: null };
}
case 'SET_ROOM_MEDIASOUP_CLIENT_HANDLER':
{
const { mediasoupClientHandler } = action.payload;
return { ...state, mediasoupClientHandler };
}
case 'SET_MEDIASOUP_VERSION':
{
const { version } = action.payload;
return { ...state, mediasoupVersion: version };
}
case 'SET_MEDIASOUP_CLIENT_VERSION':
{
const { version } = action.payload;
return { ...state, mediasoupClientVersion: version };
}
case 'SET_ROOM_ACTIVE_SPEAKER':
{
const { peerId } = action.payload;
return { ...state, activeSpeakerId: peerId };
}
case 'SET_ROOM_STATS_PEER_ID':
{
const { peerId } = action.payload;
if (state.statsPeerId === peerId)
return { ...state, statsPeerId: null };
return { ...state, statsPeerId: peerId };
}
case 'SET_FACE_DETECTION':
{
const flag = action.payload;
return { ...state, faceDetection: flag };
}
case 'REMOVE_PEER':
{
const { peerId } = action.payload;
const newState = { ...state };
if (peerId && peerId === state.activeSpeakerId)
newState.activeSpeakerId = null;
if (peerId && peerId === state.statsPeerId)
newState.statsPeerId = null;
return newState;
}
default:
{
return state;
}
}
};
export default room;

@ -0,0 +1,38 @@
import randomString from 'random-string';
import * as stateActions from './stateActions';
// This returns a redux-thunk action (a function).
export const notify = ({ type = 'info', text, title, timeout }) =>
{
if (!timeout)
{
switch (type)
{
case 'info':
timeout = 3000;
break;
case 'error':
timeout = 5000;
break;
}
}
const notification =
{
id : randomString({ length: 6 }).toLowerCase(),
type,
title,
text,
timeout
};
return (dispatch) =>
{
dispatch(stateActions.addNotification(notification));
setTimeout(() =>
{
dispatch(stateActions.removeNotification(notification.id));
}, timeout);
};
};

@ -0,0 +1,342 @@
export const setRoomUrl = (url) =>
{
return {
type : 'SET_ROOM_URL',
payload : { url }
};
};
export const setRoomState = (state) =>
{
return {
type : 'SET_ROOM_STATE',
payload : { state }
};
};
export const setRoomMediasoupClientHandler = (mediasoupClientHandler) =>
{
return {
type : 'SET_ROOM_MEDIASOUP_CLIENT_HANDLER',
payload : { mediasoupClientHandler }
};
};
export const setMediasoupVersion = (version) =>
{
return {
type : 'SET_MEDIASOUP_VERSION',
payload : { version }
};
};
export const setMediasoupClientVersion = (version) =>
{
return {
type : 'SET_MEDIASOUP_CLIENT_VERSION',
payload : { version }
};
};
export const setRoomActiveSpeaker = (peerId) =>
{
return {
type : 'SET_ROOM_ACTIVE_SPEAKER',
payload : { peerId }
};
};
export const setRoomStatsPeerId = (peerId) =>
{
return {
type : 'SET_ROOM_STATS_PEER_ID',
payload : { peerId }
};
};
export const setRoomFaceDetection = (flag) =>
{
return {
type : 'SET_FACE_DETECTION',
payload : flag
};
};
export const setMe = ({ peerId, displayName, displayNameSet, device }) =>
{
return {
type : 'SET_ME',
payload : { peerId, 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 setAudioMutedState = (enabled) =>
{
return {
type : 'SET_AUDIO_MUTED_STATE',
payload : { enabled }
};
};
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) =>
{
return {
type : 'SET_PRODUCER_PAUSED',
payload : { producerId }
};
};
export const setProducerResumed = (producerId) =>
{
return {
type : 'SET_PRODUCER_RESUMED',
payload : { producerId }
};
};
export const setProducerTrack = (producerId, track) =>
{
return {
type : 'SET_PRODUCER_TRACK',
payload : { producerId, track }
};
};
export const setProducerScore = (producerId, score) =>
{
return {
type : 'SET_PRODUCER_SCORE',
payload : { producerId, score }
};
};
export const addDataProducer = (dataProducer) =>
{
return {
type : 'ADD_DATA_PRODUCER',
payload : { dataProducer }
};
};
export const removeDataProducer = (dataProducerId) =>
{
return {
type : 'REMOVE_DATA_PRODUCER',
payload : { dataProducerId }
};
};
export const setWebcamInProgress = (flag) =>
{
return {
type : 'SET_WEBCAM_IN_PROGRESS',
payload : { flag }
};
};
export const setShareInProgress = (flag) =>
{
return {
type : 'SET_SHARE_IN_PROGRESS',
payload : { flag }
};
};
export const addPeer = (peer) =>
{
return {
type : 'ADD_PEER',
payload : { peer }
};
};
export const removePeer = (peerId) =>
{
return {
type : 'REMOVE_PEER',
payload : { peerId }
};
};
export const setPeerDisplayName = (displayName, peerId) =>
{
return {
type : 'SET_PEER_DISPLAY_NAME',
payload : { displayName, peerId }
};
};
export const addConsumer = (consumer, peerId) =>
{
return {
type : 'ADD_CONSUMER',
payload : { consumer, peerId }
};
};
export const removeConsumer = (consumerId, peerId) =>
{
return {
type : 'REMOVE_CONSUMER',
payload : { consumerId, peerId }
};
};
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 setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) =>
{
return {
type : 'SET_CONSUMER_CURRENT_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) =>
{
return {
type : 'SET_CONSUMER_PREFERRED_LAYERS',
payload : { consumerId, spatialLayer, temporalLayer }
};
};
export const setConsumerPriority = (consumerId, priority) =>
{
return {
type : 'SET_CONSUMER_PRIORITY',
payload : { consumerId, priority }
};
};
export const setConsumerTrack = (consumerId, track) =>
{
return {
type : 'SET_CONSUMER_TRACK',
payload : { consumerId, track }
};
};
export const setConsumerScore = (consumerId, score) =>
{
return {
type : 'SET_CONSUMER_SCORE',
payload : { consumerId, score }
};
};
export const addDataConsumer = (dataConsumer, peerId) =>
{
return {
type : 'ADD_DATA_CONSUMER',
payload : { dataConsumer, peerId }
};
};
export const removeDataConsumer = (dataConsumerId, peerId) =>
{
return {
type : 'REMOVE_DATA_CONSUMER',
payload : { dataConsumerId, peerId }
};
};
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'
};
};

@ -0,0 +1,11 @@
let protooPort = 4443;
if (window.location.hostname === 'test.mediasoup.org')
protooPort = 4444;
export function getProtooUrl({ roomId, peerId, consumerReplicas })
{
const hostname = window.location.hostname;
return `wss://${hostname}:${protooPort}/?roomId=${roomId}&peerId=${peerId}&consumerReplicas=${consumerReplicas}`;
}

@ -0,0 +1,20 @@
let mediaQueryDetectorElem;
export function initialize()
{
// Media query detector stuff.
mediaQueryDetectorElem =
document.getElementById('mediasoup-demo-app-media-query-detector');
return Promise.resolve();
}
export function isDesktop()
{
return Boolean(mediaQueryDetectorElem.offsetParent);
}
export function isMobile()
{
return !mediaQueryDetectorElem.offsetParent;
}

@ -0,0 +1,77 @@
{
"name": "mediasoup-demo-app",
"version": "3.0.0",
"private": true,
"description": "mediasoup demo app",
"author": "Iñaki Baz Castillo <ibc@aliax.net>",
"license": "All Rights Reserved",
"main": "lib/index.jsx",
"scripts": {
"lint": "eslint -c .eslintrc.js --ext .js,.jsx gulpfile.js lib",
"start": "gulp live"
},
"dependencies": {
"@babel/runtime": "^7.8.4",
"bowser": "^2.11.0",
"classnames": "^2.2.6",
"clipboard-copy": "^3.1.0",
"debug": "^4.3.1",
"domready": "^1.0.8",
"face-api.js": "^0.21.0",
"hark": "^1.2.3",
"js-cookie": "^2.2.1",
"mediasoup-client": "^3.6.16",
"pokemon": "^2.0.2",
"prop-types": "^15.7.2",
"protoo-client": "^4.0.5",
"random-string": "^0.2.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-draggable": "^3.3.2",
"react-dropzone": "^10.2.1",
"react-redux": "^7.2.0",
"react-spinner": "^0.2.7",
"react-tooltip": "^3.11.1",
"react-transition-group": "^4.3.0",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"riek": "^1.1.0",
"url-parse": "^1.4.7"
},
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-react": "^7.8.3",
"babel-plugin-jsx-control-statements": "^4.0.0",
"babelify": "^10.0.0",
"browser-sync": "^2.26.7",
"browserify": "^16.5.0",
"del": "^5.1.0",
"envify": "^4.1.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-control-statements": "^2.2.1",
"eslint-plugin-react": "^7.18.3",
"gulp": "^4.0.2",
"gulp-css-base64": "^1.3.4",
"gulp-eslint": "^6.0.0",
"gulp-header": "^2.0.9",
"gulp-if": "^3.0.0",
"gulp-plumber": "^1.2.1",
"gulp-rename": "^1.4.0",
"gulp-stylus": "^2.7.0",
"gulp-touch-cmd": "0.0.1",
"gulp-uglify-es": "^3.0.0",
"gulp-util": "^3.0.8",
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
"nib": "^1.1.2",
"supports-color": "^7.1.0",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.11.1"
}
}

@ -0,0 +1 @@
[{"weights":[{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}],"paths":["tiny_face_detector_model-shard1"]}]

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
id="svg4347"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="buddy.svg">
<defs
id="defs4349" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="8"
inkscape:cx="5.3985295"
inkscape:cy="12.974855"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showborder="true"
inkscape:window-width="979"
inkscape:window-height="809"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0" />
<metadata
id="metadata4352">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1004.3622)">
<rect
style="fill:none"
y="1004.8992"
x="1.3571341"
height="47.070076"
width="44.699638"
id="rect4438" />
<path
inkscape:connector-curvature="0"
d="m 29.874052,1037.8071 c -0.170513,-1.8828 -0.105106,-3.1968 -0.105106,-4.917 0.852576,-0.4474 2.380226,-3.2994 2.638334,-5.7089 0.670389,-0.054 1.727363,-0.7089 2.036865,-3.2912 0.167015,-1.3862 -0.496371,-2.1665 -0.900473,-2.4118 1.090848,-3.2807 3.356621,-13.4299 -4.190513,-14.4788 -0.776672,-1.364 -2.765648,-2.0544 -5.350262,-2.0544 -10.340807,0.1905 -11.588155,7.8088 -9.321205,16.5332 -0.402943,0.2453 -1.066322,1.0256 -0.900475,2.4118 0.310672,2.5823 1.366476,3.2362 2.036857,3.2912 0.256949,2.4083 1.845322,5.2615 2.700243,5.7089 0,1.7202 0.06426,3.0342 -0.106272,4.917 -2.046213,5.5007 -15.8522501,3.9567 -16.4899366,14.5661 H 46.303254 c -0.636524,-10.6094 -14.38299,-9.0654 -16.429202,-14.5661 z"
id="path4440"
style="opacity:0.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" opacity=".1" fill="none"/><path d="M0 0h24v24H0z" fill="none"/><path fill="#FFFFFF" d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 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="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="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 266 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path fill="#fff" d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.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 8z"/></svg>

After

Width:  |  Height:  |  Size: 301 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="#fff" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>

After

Width:  |  Height:  |  Size: 247 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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save