@ -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;
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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: </span>{room.mediasoupVersion}</p>
|
||||||
|
<p className='text'><span className='label'>client: </span>{mediasoupClientVersion}</p>
|
||||||
|
<p className='text'><span className='label'>handler: </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"]}]
|
After Width: | Height: | Size: 458 KiB |
After Width: | Height: | Size: 209 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 449 B |
After Width: | Height: | Size: 861 B |
After Width: | Height: | Size: 365 B |
After Width: | Height: | Size: 946 B |
After Width: | Height: | Size: 632 B |
After Width: | Height: | Size: 917 B |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 390 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 266 B |
After Width: | Height: | Size: 301 B |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 355 B |
After Width: | Height: | Size: 554 B |
After Width: | Height: | Size: 553 B |
After Width: | Height: | Size: 214 B |
After Width: | Height: | Size: 320 B |
After Width: | Height: | Size: 553 B |
After Width: | Height: | Size: 354 B |