diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..75dec161 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. ... + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Branch/Versions:** +If you are developer, on which branch is this. +If you are reporting as user, on which version or location you experienced the issue. +Version is available under the 3 dots in the topbar under about. + +**Server (if available please complete the following information):** + - OS: [e.g. Debian 11] + +**Client:** + - Browser [e.g. chrome, safari, firefox, edge, ...] + - Browser version [e.g. 99] + - other [e.g. mobil browser under ios] + + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/develop-deb.yml b/.github/workflows/develop-deb.yml new file mode 100644 index 00000000..5953ab07 --- /dev/null +++ b/.github/workflows/develop-deb.yml @@ -0,0 +1,106 @@ +name: Debian package + +on: + push: + branches: [ master ] +# pull_request: +# branches: [ develop ] + +jobs: + build: + + runs-on: ubuntu-latest + env: + CI: false + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v2 + with: + path: edumeet + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Get eduMEET version + id: get-version + run: | + echo "::set-output name=VERSION::$(cat edumeet/server/package.json | jq -r '.version')" + + - name: Build Debian package + id: build-deb + run: | + cd edumeet + cp server/config/config.example.js server/config/config.js + cp server/config/config.example.yaml server/config/config.yaml + cp app/public/config/config.example.js app/public/config/config.js + cd app + yarn install && yarn build + cd ../server + yarn install && yarn build + cat <<< $(jq '.bundleDependencies += .dependencies' package.json) > package.json + npm pack + VERSION=${{ steps.get-version.outputs.VERSION }} + DATE=$(date) + mkdir -p /home/runner/package + cd /home/runner/package + mkdir DEBIAN + mkdir -p usr/local/src/edumeet/server + mkdir -p etc/systemd/system/ + tar -xf /home/runner/work/***/***/***/server/***-server-$VERSION.tgz package/ 1>/dev/null 2>/dev/null || true + mv package/* usr/local/src/edumeet/server/ + mv /home/runner/work/***/***/***/*.service etc/systemd/system/ + rm -rf package + touch DEBIAN/md5sums + touch DEBIAN/md5sums + touch DEBIAN/control + #find . -type f ! -regex '.*.hg.*' ! -regex '.*?debian-binary.*' ! -regex '.*?DEBIAN.*' -printf '%P ' | xargs md5sum 1>/dev/null 2>/dev/null || true + # + cat > DEBIAN/control <= 16), redis + EOF + # + cat > DEBIAN/postinst < + Help us with translations:exclamation: + + #### How to contribute? + + 1. Continue to translate existing [language file](/app/src/intl/translations) + 2. find the _null_ values + > "settings.language": null, + 3. replace them based on the _en.json_ file + > "settings.language": "Select language", + 4. If your language is not listed, create a new translation _.json_ file.. + > copy en.json to [_"two letter country code"_.json](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) and translate to your languange + 5. make a Pull Request, or send us a file by [e-mail](mailto:community@lists.edumeet.org) + + Thank you in advance! + -* Clone the project: + +### Local Recording +
+ See more + +* Local Recording records the browser window video and audio. From the list of media formats that your browser supports you can select your preferred media format in the settings menu advanced video menu setting. MediaRecorder makes small chucks of recording and these recorded blob chunks temporary stored in IndexedDB, if IndexedDB implemented in your browser. Otherwise it stores blobs in memory in an array of blobs. +Local Recording creates a local IndexedDB with the name of the starting timestamp (unix timestamp format) And a storage called chunks. All chunks read in an array and created a final blob that you can download. After blobs array concatenation as a big blob, this big blob saved as file, and finally we delete the temporary local IndexedDB. + +* Local recording is **disabled** by default. It could be enabled by setting _localRecordingEnabled_ to true in (./app/public/config/config.js) + +* **WARNING**: Take care that local recording will increase cpu, memory and disk space consumption. +**Enough free disk space has to be provided!!!** +Keep in mind that Browsers don't allow to use all the disk free capacity! +See more info about browsers storage limits: + * + * + +
+ +# Installation + +See here for [Docker](https://github.com/edumeet/edumeet-docker/) or [Ansible](https://github.com/edumeet/edumeet-ansible/) (based on Docker) installation procedures + +## Debian & Ubuntu based operating systems (.deb package) + +* Prerequisites: Installed NodeJS (v16.x) as described in [Manual installation](#manual-installation-build) section. +* See [Configuration](#configuration) section for client and server configuration details. +* Download from [releases](https://github.com/edumeet/edumeet/releases) assets, or latest job [artifact](https://github.com/edumeet/edumeet/actions?query=workflow%3ADeployer+branch%3Amaster+is%3Asuccess). ```bash -$ git clone https://github.com/havfo/multiparty-meeting.git -$ cd multiparty-meeting +# Unzip the file +unzip edumeet.zip + +# Install the package +sudo apt install edumeet/edumeet.deb + +# After package installation, don't forget to edit configuration files. +sudo nano /etc/educonf/client-config.js +sudo nano /etc/educonf/server-config.js +sudo nano /etc/educonf/server-config.yaml + +# Finally, start the service by (it's enabled by default) +sudo systemctl start edumeet ``` -* Copy `server/config.example.js` to `server/config.js` : - +## Manual installation (build) +Installation example is based on Debian/Ubuntu Linux operating system. +1. Install [NodeJS (v16.x)](https://github.com/nodesource/distributions) and [Yarn ](https://classic.yarnpkg.com/en/docs/install#debian-stable) package manager +- NodeJS (v16.x) [Debian/Ubuntu](https://github.com/nodesource/distributions#deb) ```bash -$ cp server/config.example.js server/config.js +# Using Ubuntu +curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Using Debian, as root +curl -fsSL https://deb.nodesource.com/setup_16.x | bash - +apt-get install -y nodejs +``` +- Yarn package manager: +```bash +curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt update && sudo apt install yarn +``` +2. Install all required dependencies +```bash +sudo apt update && sudo apt install -y curl git python python3-pip build-essential redis openssl libssl-dev pkg-config +``` +3. Clone eduMEET git repository +```bash +git clone https://github.com/edumeet/edumeet.git +cd edumeet +``` +(switch to the "develop" branch to get the latest features) +```bash +git checkout develop +``` +### Configuration +**eduMEET** will start and run normally with just default settings. If there is no configuration files, it will automatically detect your host IP address, and listen on port 443 (https). In order to change default values (e.g. certificates), or activate features (e.g. authentication), use appropriate configuration file (see below for details). + +**:warning: Note:** There are separate configuration files for eduMEET application and eduMEET server: + +**eduMEET application (app)** for: enabling login, change logo or background, adjust A/V parameters, etc... + +Copy [example](/app/public/config/config.example.js) template and edit values (see all available parameters in [./app/public/config/README.md](/app/public/config/README.md)) +```bash +cp app/public/config/config.example.js app/public/config/config.js ``` -* Copy `app/config.example.js` to `app/config.js` : +**eduMEET server** require **:warning:two** configuration files: **config.js**, and **config.{_json_, _yaml_ or _toml_}** (multiple format supported) +**1. config.js** for setting authentication methods and user roles. + +Copy example template and edit values (see additional details in [example](/server/config/config.example.js) file) ```bash -$ cp app/config.example.js app/config.js +cp server/config/config.example.js server/config/config.js ``` -* Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc). +**2. config.{_json_, _yaml_ or _toml_}** for configuring: server port, server certificates, [STUN/TURN](#turn-configuration) configuration, monitoring, etc... (See below examples of different configuration styles). -* Set up the browser app: - -```bash -$ cd app -$ npm install -$ export NODE_ENV=production -$ gulp dist +[**:point_right: _config.yaml_**](/server/config/config.example.yaml) example: +```yaml + listeningPort: 443 + tls: + key: /opt/edumeet/server/certs/privkey.pem + cert: /opt/edumeet/server/certs/cert.pem ``` -This will build the client application and copy everythink to `server/public` from where the server can host client code to browser requests. +[**:point_right: _config.json_**](/server/config/config.example.json) example: +```javascript + { + "listeningPort" : "443", + "tls" : { + "cert" : "/opt/edumeet/server/certs/cert.pem", + "key" : "/opt/edumeet/server/certs/privkey.pem" + } + } +``` +[**:point_right: _config.toml_**](/server/config/config.example.toml) example: +```toml + listeningPort = "443" -* Globally install `gulp-cli` NPM module (may need `sudo`): + [tls] + cert = "/opt/edumeet/server/certs/cert.pem" + key = "/opt/edumeet/server/certs/privkey.pem" +``` +**:red_circle: IMPORTANT:** Use **only one** type for second configuration file (`yaml` file format is highly recommended) +Copy **only one** example template file and edit values (see all available parameters in [./server/config/README.md](/server/config/README.md)) ```bash -$ npm install -g gulp-cli +cp server/config/config.example.yaml server/config/config.yaml + OR!!! +cp server/config/config.example.json server/config/config.json + OR!!! +cp server/config/config.example.toml server/config/config.toml ``` -* Set up the server: +**:warning: NOTE:** application and server components **has to be rebuild** if configuration parameter is changed ([see build steps](#manual-installation-build)). Rebuild is not necessary for Docker or Debian (.deb) version, just restart container/service. + +### Build +**Note:** It is highly recommended to use _yarn_ package manager. ```bash -$ cd .. -$ cd server -$ npm install +cd app +yarn && yarn build + +cd ../server +yarn && yarn build +``` +### Run + +**Run on server** (as root or with sudo) + +```bash +# Run the Node.js server application in a terminal: +cd server +sudo yarn start ``` -## Run it locally +**Run locally** (for development) -* Run the Node.js server application in a terminal: +* The newest build is always in **develop branch** if you want to make a contribution/pull request use it instead of master branch. ```bash -$ node server.js -``` -* test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname` +# run a live build from app folder: +app$ yarn start -## Deploy it in a server - -* Stop your locally running server. Copy systemd-service file `multiparty-meeting.service` to `/etc/systemd/system/` and check location path settings: -```bash -$ cp multiparty-meeting.service /etc/systemd/system/ -$ edit /etc/systemd/system/multiparty-meeting.service +# and run server in server folder: +server$ yarn start ``` -* reload systemd configuration and start service: - +Note: To avoid running server as root, redirects privileged ports with firewall rules: ```bash -$ systemctl daemon-reload -$ systemctl start multiparty-meeting +#adjust ports to your needs + +sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443 +sudo iptables -t nat -A OUTPUT -p tcp --dport 443 -o lo -j REDIRECT --to-port 8443 +sudo iptables -t nat -A PREROUTING -p tcp --dport 3443 -j REDIRECT --to-ports 8443 +sudo iptables -t nat -A OUTPUT -p tcp --dport 3443 -o lo -j REDIRECT --to-port 8443 + +# make it persistent +sudo apt install iptables-persistent +sudo iptables-save > /etc/iptables/rules.v4 +sudo ip6tables-save > /etc/iptables/rules.v6 ``` +* Test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname` + +**Run as a service** (systemd) -* if you want to start multiparty-meeting at boot time: ```bash -$ systemctl enable multiparty-meeting +# Stop your locally running server. Copy systemd-service file `edumeet.service` to `/etc/systemd/system/` and check location path settings: +cp edumeet.service /etc/systemd/system/ + +# modify the install paths, if required +sudo edit /etc/systemd/system/edumeet.service + +# Reload systemd configuration and start service: +sudo systemctl daemon-reload +sudo systemctl start edumeet + +# If you want to start edumeet at boot time: +sudo systemctl enable edumeet ``` ## Ports and firewall +| Port | protocol | description | +| ---- | ----------- | ----------- | +| 443 | tcp | default https webserver and signaling - adjustable in `server/config/config.yaml`) | +| 4443 | tcp | default `yarn start` port for developing with live browser reload, not needed in production environments - adjustable in app/package.json) | +| 40000-49999 | udp, tcp | media ports - adjustable in `server/config/config.yaml` | -* 3443/tcp (default https webserver and signaling - adjustable in `server/config.js`) -* 3000/tcp (default `gulp live` port for developing with live browser reload, not needed in production enviroments - adjustable in app/gulpfile.js) -* 40000-49999/udp/tcp (media ports - adjustable in `server/config.js`) +## Load balanced installation + +To deploy this as a load balanced cluster, have a look at [HAproxy](/docs/HAproxy.md). + +## Learning management integration + +To integrate with an LMS (e.g. Moodle), have a look at [LTI](LTI/LTI.md). ## TURN configuration -* You need an addtional [TURN](https://github.com/coturn/coturn)-server for clients located behind restrictive firewalls! Add your server and credentials to `app/config.js` +If you are part of the GEANT eduGAIN, you can request your turn api key at [https://turn.geant.org/](https://turn.geant.org/) + +You need an additional [TURN](https://github.com/coturn/coturn)-server for clients located behind restrictive firewalls! +Add your server and credentials to `server/config/config.yaml` + +## Community-driven support +| Type | | +| ----------- | ----------- | +| Open mailing list | community@lists.edumeet.org | +| Subscribe | lists.edumeet.org/sympa/subscribe/community/ | +| Open archive | lists.edumeet.org/sympa/arc/community/ | ## Authors * Håvar Aambø Fosstveit * Stefan Otto * Mészáros Mihály - +* Roman Drozd +* Rémai Gábor László +* Piotr Pawałowski This started as a fork of the [work](https://github.com/versatica/mediasoup-demo) done by: -* Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)] +* Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)] ## License -MIT +MIT License (see `LICENSE.md`) + +Contributions to this work were made on behalf of the GÉANT project, a project that has received funding from the European Union’s Horizon 2020 research and innovation programme under Grant Agreement No. 731122 (GN4-2). On behalf of GÉANT project, GÉANT Association is the sole owner of the copyright in all material which was developed by a member of the GÉANT project. + +GÉANT Vereniging (Association) is registered with the Chamber of Commerce in Amsterdam with registration number 40535155 and operates in the UK as a branch of GÉANT Vereniging. Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK. + diff --git a/app/.babelrc b/app/.babelrc deleted file mode 100644 index 3aadd5a3..00000000 --- a/app/.babelrc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "plugins": - [ - "@babel/plugin-proposal-object-rest-spread", - "jsx-control-statements", - "@babel/plugin-proposal-class-properties", - "@babel/plugin-transform-runtime" - ], - "presets": - [ - [ - "@babel/preset-env", - { - "targets": { - "browsers": [ - "chrome >= 67", - "edge >= 17", - "firefox >= 60", - "safari >= 12" - ] - } - } - ], - "@babel/react" - ] -} diff --git a/app/.env b/app/.env new file mode 100644 index 00000000..86c714e9 --- /dev/null +++ b/app/.env @@ -0,0 +1,2 @@ +REACT_APP_VERSION=$npm_package_version +REACT_APP_NAME=$npm_package_name \ No newline at end of file diff --git a/app/.eslintignore b/app/.eslintignore new file mode 100644 index 00000000..b7da0a95 --- /dev/null +++ b/app/.eslintignore @@ -0,0 +1,2 @@ +.eslintrc.js +src/react-app-env.d.ts \ No newline at end of file diff --git a/app/.eslintrc.js b/app/.eslintrc.js deleted file mode 100644 index 92317267..00000000 --- a/app/.eslintrc.js +++ /dev/null @@ -1,231 +0,0 @@ -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' - } - }, - parser: "babel-eslint", - 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, 'unix' ], - 'lines-around-comment': [ 2, - { - allowBlockStart: true, - allowObjectStart: true, - beforeBlockComment: true, - beforeLineComment: false - }], - 'max-len': [ 2, 90, - { - tabWidth: 2, - comments: 110, - ignoreUrls: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - ignoreRegExpLiterals: true - }], - 'newline-after-var': 2, - 'newline-before-return': 2, - 'newline-per-chained-call': 2, - 'no-alert': 2, - 'no-caller': 2, - 'no-case-declarations': 2, - 'no-catch-shadow': 2, - 'no-class-assign': 2, - 'no-confusing-arrow': 2, - 'no-console': 2, - 'no-const-assign': 2, - 'no-debugger': 2, - 'no-dupe-args': 2, - 'no-dupe-keys': 2, - 'no-duplicate-case': 2, - 'no-div-regex': 2, - 'no-empty': [ 2, { allowEmptyCatch: true } ], - 'no-empty-pattern': 2, - 'no-else-return': 0, - 'no-eval': 2, - 'no-extend-native': 2, - 'no-ex-assign': 2, - 'no-extra-bind': 2, - 'no-extra-boolean-cast': 2, - 'no-extra-label': 2, - 'no-extra-semi': 2, - 'no-fallthrough': 2, - 'no-func-assign': 2, - 'no-global-assign': 2, - 'no-implicit-coercion': 2, - 'no-implicit-globals': 2, - 'no-inner-declarations': 2, - 'no-invalid-regexp': 2, - 'no-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': 0, - '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 - }] - } -}; diff --git a/app/.eslintrc.json b/app/.eslintrc.json new file mode 100644 index 00000000..c2bb6eaf --- /dev/null +++ b/app/.eslintrc.json @@ -0,0 +1,386 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "overrides": [ + { + "files": ["**/*.{ts,tsx}"], + "plugins": ["@typescript-eslint"], + "extends":[ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, + "settings": { + "import/resolver": { + "typescript": { + "alwaysTryTypes": true + } + } + }, + "rules": { + "no-unused-vars" : 0, + "@typescript-eslint/ban-types" : 0, + "@typescript-eslint/ban-ts-comment" : 0, + "@typescript-eslint/ban-ts-ignore" : 0, + "@typescript-eslint/explicit-module-boundary-types" : 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" : [ 2, { "functions": false } ], + "@typescript-eslint/no-empty-function" : 0, + "@typescript-eslint/no-non-null-assertion" : 0 + } + } + ], + "plugins": [ + "import", + "react" + ], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "react-app" + ], + "settings": { + "react": { + "pragma": "React", + "version": "16" + } + }, + "parser": "babel-eslint", + "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, + "unix" + ], + "lines-around-comment": [ + 2, + { + "allowBlockStart": true, + "allowObjectStart": true, + "beforeBlockComment": true, + "beforeLineComment": false + } + ], + "max-len": [ + 2, + 90, + { + "tabWidth": 2, + "comments": 110, + "ignoreUrls": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreRegExpLiterals": true + } + ], + "newline-after-var": 2, + "newline-before-return": 2, + "newline-per-chained-call": 2, + "no-alert": 2, + "no-caller": 2, + "no-case-declarations": 2, + "no-catch-shadow": 2, + "no-class-assign": 2, + "no-confusing-arrow": [ + "error", + { + "allowParens": true + } + ], + "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-irregular-whitespace": 2, + "no-trailing-spaces": [ + "error", + { + "ignoreComments": true + } + ], + "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": 0, + "yoda": 2, + "import/extensions": 2, + "import/no-duplicates": 2, + "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 + } + ] + } +} \ No newline at end of file diff --git a/app/.npmrc b/app/.npmrc deleted file mode 100644 index 43c97e71..00000000 --- a/app/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/app/Procfile b/app/Procfile new file mode 100644 index 00000000..a25f4753 --- /dev/null +++ b/app/Procfile @@ -0,0 +1,2 @@ +react: npm start +electron: node src/electron-wait-react \ No newline at end of file diff --git a/app/banner.txt b/app/banner.txt deleted file mode 100644 index 3de4791c..00000000 --- a/app/banner.txt +++ /dev/null @@ -1,7 +0,0 @@ -/* - * <%= pkg.name %> v<%= pkg.version %> - * <%= pkg.description %> - * Copyright: 2017-<%= currentYear %> <%= pkg.author %> - * License: <%= pkg.license %> - */ - diff --git a/app/chooseRoom.html b/app/chooseRoom.html deleted file mode 100644 index 97d00de5..00000000 --- a/app/chooseRoom.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - Multiparty Meeting - - - - -
-
- - - - - diff --git a/app/config/config.example.js b/app/config/config.example.js deleted file mode 100644 index 89c9d58a..00000000 --- a/app/config/config.example.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = -{ - chromeExtension : 'https://chrome.google.com/webstore/detail/fckajcjdaabdgnbdcmhhebdglogjfodi', - loginEnabled : false, - turnServers : [ - { - urls : [ - 'turn:example.com:443?transport=tcp' - ], - username : 'example', - credential : 'example' - } - ], - requestTimeout : 10000, - transportOptions : - { - tcp : true - } -}; diff --git a/app/gulpfile.js b/app/gulpfile.js deleted file mode 100644 index 468c6d0e..00000000 --- a/app/gulpfile.js +++ /dev/null @@ -1,299 +0,0 @@ -/** - * 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 - * Alias for `gulp live`. - */ - -const fs = require('fs'); -const path = require('path'); -const gulp = require('gulp'); -const gulpif = require('gulp-if'); -const gutil = require('gulp-util'); -const plumber = require('gulp-plumber'); -const rename = require('gulp-rename'); -const change = require('gulp-change'); -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.json'); -const BANNER = fs.readFileSync('banner.txt').toString(); -const BANNER_OPTIONS = -{ - pkg : PKG, - currentYear : (new Date()).getFullYear() -}; -const OUTPUT_DIR = '../server/public'; -const appOptions = require('./config/config'); -const SERVER_CONFIG = '../server/config/config'; - -// 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(); -} - -function changeHTML(content) -{ - return content.replace(/chromeExtension/g, appOptions.chromeExtension); -} - -gulp.task('clean', () => del(OUTPUT_DIR, { force: true })); - -const LINTING_FILES = [ - 'gulpfile.js', - 'lib/**/*.js', - 'lib/**/*.jsx' -]; - -gulp.task('lint', () => -{ - return gulp.src(LINTING_FILES) - .pipe(plumber()) - .pipe(eslint()) - .pipe(eslint.format()); -}); - -gulp.task('lint-fix', function() -{ - return gulp.src(LINTING_FILES) - .pipe(plumber()) - .pipe(eslint({ fix: true })) - .pipe(eslint.format()) - .pipe(gulp.dest((file) => file.base)); -}); - -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('*.html') - .pipe(change(changeHTML)) - .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('livebrowser', (done) => -{ - const config = require(SERVER_CONFIG); - - browserSync( - { - open : 'external', - host : config.domain, - port : 3000, - server : - { - baseDir : OUTPUT_DIR - }, - https : config.tls, - ghostMode : false, - files : path.join(OUTPUT_DIR, '**', '*') - }); - - done(); -}); - -gulp.task('browser', (done) => -{ - const config = require(SERVER_CONFIG); - - browserSync( - { - open : 'external', - host : config.domain, - port : 3000, - server : - { - baseDir : OUTPUT_DIR - }, - https : config.tls, - ghostMode : false - }); - - done(); -}); - -gulp.task('watch', (done) => -{ - // Watch changes in HTML. - gulp.watch([ '*.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('dist', gulp.series( - 'clean', - 'lint', - 'bundle', - 'html', - 'css', - 'resources' -)); - -gulp.task('dist-watch', gulp.series( - 'clean', - 'lint', - 'bundle:watch', - 'html', - 'css', - 'resources', - 'watch', -)); - -gulp.task('live', gulp.series( - 'clean', - 'lint', - 'bundle:watch', - 'html', - 'css', - 'resources', - 'watch', - 'livebrowser' -)); - -gulp.task('open', gulp.series('browser')); - -gulp.task('default', gulp.series('live')); diff --git a/app/index.html b/app/index.html deleted file mode 100644 index 62d7bb67..00000000 --- a/app/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - Multiparty Meeting - - - - - - - - - - - - - -
-
- - diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js deleted file mode 100644 index 4d42144b..00000000 --- a/app/lib/RoomClient.js +++ /dev/null @@ -1,1990 +0,0 @@ -import io from 'socket.io-client'; -import * as mediasoupClient from 'mediasoup-client'; -import WebTorrent from 'webtorrent'; -import createTorrent from 'create-torrent'; -import { saveAs } from 'file-saver/FileSaver'; -import Logger from './Logger'; -import hark from 'hark'; -import ScreenShare from './ScreenShare'; -import Spotlights from './Spotlights'; -import { getSignalingUrl } from './urlFactory'; -import * as cookiesManager from './cookiesManager'; -import * as requestActions from './redux/requestActions'; -import * as stateActions from './redux/stateActions'; -import { - turnServers, - requestTimeout, - transportOptions -} from '../config/config'; - -const logger = new Logger('RoomClient'); - -let ROOM_OPTIONS = -{ - requestTimeout : requestTimeout, - transportOptions : transportOptions, - turnServers : turnServers, - maxSpotlights : 4 -}; - -const VIDEO_CONSTRAINS = -{ - width : { ideal: 1280 }, - aspectRatio : 1.334 -}; - -let store; - -export default class RoomClient -{ - /** - * @param {Object} data - * @param {Object} data.store - The Redux store. - */ - static init(data) - { - store = data.store; - } - - constructor( - { roomId, peerName, displayName, device, useSimulcast, produce }) - { - logger.debug( - 'constructor() [roomId:"%s", peerName:"%s", displayName:"%s", device:%s]', - roomId, peerName, displayName, device.flag); - - this._signalingUrl = getSignalingUrl(peerName, roomId); - - // window element to external login site - this._loginWindow; - - // Closed flag. - this._closed = false; - - // Whether we should produce. - this._produce = produce; - - // Torrent support - this._torrentSupport = WebTorrent.WEBRTC_SUPPORT; - - // Whether simulcast should be used. - this._useSimulcast = useSimulcast; - - // This device - this._device = device; - - // My peer name. - this._peerName = peerName; - - // My display name - this._displayName = displayName; - - // Alert sound - this._soundAlert = new Audio('/resources/sounds/notify.mp3'); - - // AudioContext - this._audioContext; - - // Socket.io peer connection - this._signalingSocket = null; - - // The mediasoup room instance - this._room = null; - this._roomId = roomId; - - // Our WebTorrent client - this._webTorrent = this._torrentSupport && new WebTorrent({ - tracker : { - rtcConfig : { - iceServers : ROOM_OPTIONS.turnServers - } - } - }); - - // Max spotlights - this._maxSpotlights = ROOM_OPTIONS.maxSpotlights; - - // Manager of spotlight - this._spotlights = null; - - // Transport for sending. - this._sendTransport = null; - - // Transport for receiving. - this._recvTransport = null; - - // Local mic mediasoup Producer. - this._micProducer = null; - - // Local webcam mediasoup Producer. - this._webcamProducer = null; - - // Map of webcam MediaDeviceInfos indexed by deviceId. - // @type {Map} - this._webcams = new Map(); - - this._audioDevices = new Map(); - - // Local Webcam. Object with: - // - {MediaDeviceInfo} [device] - // - {String} [resolution] - 'qvga' / 'vga' / 'hd'. - this._webcam = { - device : null, - resolution : 'hd' - }; - - this._audioDevice = { - device : null - }; - - this._screenSharing = ScreenShare.create(device); - - this._screenSharingProducer = null; - - this._startKeyListener(); - - this._audioContext = null; - - this.join(); - } - - close() - { - if (this._closed) - return; - - this._closed = true; - - logger.debug('close()'); - - // Leave the mediasoup Room. - this._room.leave(); - - // Close signaling Peer (wait a bit so mediasoup-client can send - // the 'leaveRoom' notification). - setTimeout(() => this._signalingSocket.close(), 250); - - store.dispatch(stateActions.setRoomState('closed')); - } - - _startKeyListener() - { - // Add keypress event listner on document - document.addEventListener('keypress', (event) => - { - const key = String.fromCharCode(event.which); - - const source = event.target; - - const exclude = [ 'input', 'textarea' ]; - - if (exclude.indexOf(source.tagName.toLowerCase()) === -1) - { - logger.debug('keyPress() [key:"%s"]', key); - - switch (key) - { - case 'a': // Activate advanced mode - { - store.dispatch(stateActions.toggleAdvancedMode()); - this.notify('Toggled advanced mode.'); - break; - } - - case '1': // Set democratic view - { - store.dispatch(stateActions.setDisplayMode('democratic')); - this.notify('Changed layout to democratic view.'); - break; - } - - case '2': // Set filmstrip view - { - store.dispatch(stateActions.setDisplayMode('filmstrip')); - this.notify('Changed layout to filmstrip view.'); - break; - } - - case 'm': // Toggle microphone - { - this.toggleMic(); - this.notify('Muted/unmuted your microphone.'); - break; - } - } - } - }); - } - - login() - { - const url = `/login?roomId=${this._room.roomId}&peerName=${this._peerName}`; - - this._loginWindow = window.open(url, 'loginWindow'); - } - - logout() - { - window.location = '/logout'; - } - - closeLoginWindow() - { - this._loginWindow.close(); - } - - _soundNotification() - { - const alertPromise = this._soundAlert.play(); - - if (alertPromise !== undefined) - { - alertPromise - .then() - .catch((error) => - { - logger.error('_soundAlert.play() | failed: %o', error); - }); - } - } - - notify(text) - { - store.dispatch(requestActions.notify({ text: text })); - } - - timeoutCallback(callback) - { - let called = false; - - const interval = setTimeout( - () => - { - if (called) - return; - called = true; - callback(new Error('Request timeout.')); - }, - ROOM_OPTIONS.requestTimeout - ); - - return (...args) => - { - if (called) - return; - called = true; - clearTimeout(interval); - - callback(...args); - }; - } - - sendRequest(method, data) - { - return new Promise((resolve, reject) => - { - if (!this._signalingSocket) - { - reject('No socket connection.'); - } - else - { - this._signalingSocket.emit(method, data, this.timeoutCallback((err, response) => - { - if (err) - { - reject(err); - } - else - { - resolve(response); - } - })); - } - }); - } - - async changeDisplayName(displayName) - { - logger.debug('changeDisplayName() [displayName:"%s"]', displayName); - - // Store in cookie. - cookiesManager.setUser({ displayName }); - - try - { - await this.sendRequest('change-display-name', { displayName }); - - store.dispatch(stateActions.setDisplayName(displayName)); - - this.notify(`Your display name changed to ${displayName}.`); - } - catch (error) - { - logger.error('changeDisplayName() | failed: %o', error); - - this.notify('An error occured while changing your display name.'); - - // We need to refresh the component for it to render the previous - // displayName again. - store.dispatch(stateActions.setDisplayName()); - } - } - - async changeProfilePicture(picture) - { - logger.debug('changeProfilePicture() [picture: "%s"]', picture); - - try - { - await this.sendRequest('change-profile-picture', { picture }); - } - catch (error) - { - logger.error('shareProfilePicure() | failed: %o', error); - } - } - - async sendChatMessage(chatMessage) - { - logger.debug('sendChatMessage() [chatMessage:"%s"]', chatMessage); - - try - { - store.dispatch( - stateActions.addUserMessage(chatMessage.text)); - - await this.sendRequest('chat-message', { chatMessage }); - } - catch (error) - { - logger.error('sendChatMessage() | failed: %o', error); - - this.notify('An error occured while sending chat message.'); - } - } - - saveFile(file) - { - file.getBlob((err, blob) => - { - if (err) - { - return this.notify('An error occurred while saving a file'); - } - - saveAs(blob, file.name); - }); - } - - handleDownload(magnetUri) - { - store.dispatch( - stateActions.setFileActive(magnetUri)); - - const existingTorrent = this._webTorrent.get(magnetUri); - - if (existingTorrent) - { - // Never add duplicate torrents, use the existing one instead. - return this._handleTorrent(existingTorrent); - } - - this._webTorrent.add(magnetUri, this._handleTorrent); - } - - _handleTorrent(torrent) - { - // Torrent already done, this can happen if the - // same file was sent multiple times. - if (torrent.progress === 1) - { - return store.dispatch( - stateActions.setFileDone( - torrent.magnetURI, - torrent.files - )); - } - - let lastMove = 0; - - torrent.on('download', () => - { - if (Date.now() - lastMove > 1000) - { - store.dispatch( - stateActions.setFileProgress( - torrent.magnetURI, - torrent.progress - )); - - lastMove = Date.now(); - } - }); - - torrent.on('done', () => - { - store.dispatch( - stateActions.setFileDone( - torrent.magnetURI, - torrent.files - )); - }); - } - - async shareFiles(files) - { - this.notify('Creating torrent'); - - createTorrent(files, (err, torrent) => - { - if (err) - { - return this.notify( - 'An error occured while uploading a file' - ); - } - - const existingTorrent = this._webTorrent.get(torrent); - - if (existingTorrent) - { - const { displayName, picture } = store.getState().me; - - const file = { - magnetUri : existingTorrent.magnetURI, - displayName, - picture - }; - - return this._sendFile(file); - } - - this._webTorrent.seed(files, (newTorrent) => - { - this.notify( - 'Torrent successfully created' - ); - - const { displayName, picture } = store.getState().me; - const file = { - magnetUri : newTorrent.magnetURI, - displayName, - picture - }; - - store.dispatch(stateActions.addFile( - { - magnetUri : file.magnetUri, - displayName : displayName, - picture : picture, - me : true - })); - - this._sendFile(file); - }); - }); - } - - // { file, name, picture } - async _sendFile(file) - { - logger.debug('sendFile() [file: %o]', file); - - try - { - await this.sendRequest('send-file', { file }); - } - catch (error) - { - logger.error('sendFile() | failed: %o', error); - - this.notify('An error occurred while sharing file.'); - } - } - - async getServerHistory() - { - logger.debug('getServerHistory()'); - - try - { - const { - chatHistory, - fileHistory, - lastN - } = await this.sendRequest('server-history'); - - if (chatHistory.length > 0) - { - logger.debug('Got chat history'); - store.dispatch( - stateActions.addChatHistory(chatHistory)); - } - - if (fileHistory.length > 0) - { - logger.debug('Got files history'); - - store.dispatch(stateActions.addFileHistory(fileHistory)); - } - - if (lastN.length > 0) - { - logger.debug('Got lastN'); - - // Remove our self from list - const index = lastN.indexOf(this._peerName); - - lastN.splice(index, 1); - - this._spotlights.addSpeakerList(lastN); - } - } - catch (error) - { - logger.error('getServerHistory() | failed: %o', error); - - this.notify('An error occured while getting server history.'); - } - } - - toggleMic() - { - logger.debug('toggleMic()'); - - if (this._micProducer.locallyPaused) - this.unmuteMic(); - else - this.muteMic(); - } - - muteMic() - { - logger.debug('muteMic()'); - - this._micProducer.pause(); - } - - unmuteMic() - { - logger.debug('unmuteMic()'); - - this._micProducer.resume(); - } - - // Updated consumers based on spotlights - async updateSpotlights(spotlights) - { - logger.debug('updateSpotlights()'); - - try - { - for (const peer of this._room.peers) - { - if (spotlights.indexOf(peer.name) > -1) // Resume video for speaker - { - for (const consumer of peer.consumers) - { - if (consumer.kind !== 'video' || !consumer.supported) - continue; - - await consumer.resume(); - } - } - else // Pause video for everybody else - { - for (const consumer of peer.consumers) - { - if (consumer.kind !== 'video') - continue; - - await consumer.pause('not-speaker'); - } - } - } - } - catch (error) - { - logger.error('updateSpotlights() failed: %o', error); - } - } - - installExtension() - { - logger.debug('installExtension()'); - - return new Promise((resolve, reject) => - { - window.addEventListener('message', _onExtensionMessage, false); - // eslint-disable-next-line - chrome.webstore.install(null, _successfulInstall, _failedInstall); - function _onExtensionMessage({ data }) - { - if (data.type === 'ScreenShareInjected') - { - logger.debug('installExtension() | installation succeeded'); - - return resolve(); - } - } - - function _failedInstall(reason) - { - window.removeEventListener('message', _onExtensionMessage); - - return reject( - new Error('Failed to install extension: %s', reason)); - } - - function _successfulInstall() - { - logger.debug('installExtension() | installation accepted'); - } - }) - .then(() => - { - // This should be handled better - store.dispatch(stateActions.setScreenCapabilities( - { - canShareScreen : this._room.canSend('video'), - needExtension : false - })); - }) - .catch((error) => - { - logger.error('installExtension() | failed: %o', error); - }); - } - - async enableScreenSharing() - { - logger.debug('enableScreenSharing()'); - - store.dispatch(stateActions.setScreenShareInProgress(true)); - - try - { - await this._setScreenShareProducer(); - } - catch (error) - { - logger.error('enableScreenSharing() | failed: %o', error); - } - - store.dispatch(stateActions.setScreenShareInProgress(false)); - } - - async enableWebcam() - { - logger.debug('enableWebcam()'); - - // Store in cookie. - cookiesManager.setDevices({ webcamEnabled: true }); - - store.dispatch(stateActions.setWebcamInProgress(true)); - - try - { - await this._setWebcamProducer(); - } - catch (error) - { - logger.error('enableWebcam() | failed: %o', error); - } - - store.dispatch(stateActions.setWebcamInProgress(false)); - } - - async disableScreenSharing() - { - logger.debug('disableScreenSharing()'); - - store.dispatch(stateActions.setScreenShareInProgress(true)); - - try - { - await this._screenSharingProducer.close(); - } - catch (error) - { - logger.error('disableScreenSharing() | failed: %o', error); - } - - store.dispatch(stateActions.setScreenShareInProgress(false)); - } - - async disableWebcam() - { - logger.debug('disableWebcam()'); - - // Store in cookie. - cookiesManager.setDevices({ webcamEnabled: false }); - - store.dispatch(stateActions.setWebcamInProgress(true)); - - try - { - this._webcamProducer.close(); - } - catch (error) - { - logger.error('disableWebcam() | failed: %o', error); - } - - store.dispatch(stateActions.setWebcamInProgress(false)); - } - - async changeAudioDevice(deviceId) - { - logger.debug('changeAudioDevice() [deviceId: %s]', deviceId); - - store.dispatch( - stateActions.setAudioInProgress(true)); - - try - { - this._audioDevice.device = this._audioDevices.get(deviceId); - - logger.debug( - 'changeAudioDevice() | new selected webcam [device:%o]', - this._audioDevice.device); - - const { device } = this._audioDevice; - - if (!device) - throw new Error('no audio devices'); - - logger.debug('changeAudioDevice() | calling getUserMedia()'); - - const stream = await navigator.mediaDevices.getUserMedia( - { - audio : - { - deviceId : { exact: device.deviceId } - } - }); - - const track = stream.getAudioTracks()[0]; - - const newTrack = await this._micProducer.replaceTrack(track); - - const harkStream = new MediaStream; - - harkStream.addTrack(newTrack); - if (!harkStream.getAudioTracks()[0]) - throw new Error('changeAudioDevice(): given stream has no audio track'); - if (this._micProducer.hark != null) this._micProducer.hark.stop(); - this._micProducer.hark = hark(harkStream, { play: false }); - - // eslint-disable-next-line no-unused-vars - this._micProducer.hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exagerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - - if (volume !== this._micProducer.volume) - { - this._micProducer.volume = volume; - store.dispatch(stateActions.setProducerVolume(this._micProducer.id, volume)); - } - }); - - track.stop(); - - store.dispatch( - stateActions.setProducerTrack(this._micProducer.id, newTrack)); - - cookiesManager.setAudioDevice({ audioDeviceId: deviceId }); - - await this._updateAudioDevices(); - } - catch (error) - { - logger.error('changeAudioDevice() failed: %o', error); - } - - store.dispatch( - stateActions.setAudioInProgress(false)); - } - - async changeWebcam(deviceId) - { - logger.debug('changeWebcam() [deviceId: %s]', deviceId); - - store.dispatch( - stateActions.setWebcamInProgress(true)); - - try - { - this._webcam.device = this._webcams.get(deviceId); - - logger.debug( - 'changeWebcam() | new selected webcam [device:%o]', - this._webcam.device); - - // Reset video resolution to HD. - this._webcam.resolution = 'hd'; - - const { device } = this._webcam; - - if (!device) - throw new Error('no webcam devices'); - - logger.debug('changeWebcam() | calling getUserMedia()'); - - const stream = await navigator.mediaDevices.getUserMedia( - { - video : - { - deviceId : { exact: device.deviceId }, - ...VIDEO_CONSTRAINS - } - }); - - const track = stream.getVideoTracks()[0]; - - const newTrack = await this._webcamProducer.replaceTrack(track); - - track.stop(); - - store.dispatch( - stateActions.setProducerTrack(this._webcamProducer.id, newTrack)); - - cookiesManager.setVideoDevice({ videoDeviceId: deviceId }); - - await this._updateWebcams(); - } - catch (error) - { - logger.error('changeWebcam() failed: %o', error); - } - - store.dispatch( - stateActions.setWebcamInProgress(false)); - } - - setSelectedPeer(peerName) - { - logger.debug('setSelectedPeer() [peerName:"%s"]', peerName); - - this._spotlights.setPeerSpotlight(peerName); - - store.dispatch( - stateActions.setSelectedPeer(peerName)); - } - - // type: mic/webcam/screen - // mute: true/false - modifyPeerConsumer(peerName, type, mute) - { - logger.debug( - 'modifyPeerConsumer() [peerName:"%s", type:"%s"]', - peerName, - type - ); - - if (type === 'mic') - store.dispatch( - stateActions.setPeerAudioInProgress(peerName, true)); - else if (type === 'webcam') - store.dispatch( - stateActions.setPeerVideoInProgress(peerName, true)); - else if (type === 'screen') - store.dispatch( - stateActions.setPeerScreenInProgress(peerName, true)); - - try - { - for (const peer of this._room.peers) - { - if (peer.name === peerName) - { - for (const consumer of peer.consumers) - { - if (consumer.appData.source !== type || !consumer.supported) - continue; - - if (mute) - consumer.pause(`mute-${type}`); - else - consumer.resume(); - } - } - } - } - catch (error) - { - logger.error('modifyPeerConsumer() failed: %o', error); - } - - if (type === 'mic') - store.dispatch( - stateActions.setPeerAudioInProgress(peerName, false)); - else if (type === 'webcam') - store.dispatch( - stateActions.setPeerVideoInProgress(peerName, false)); - else if (type === 'screen') - store.dispatch( - stateActions.setPeerScreenInProgress(peerName, false)); - } - - async sendRaiseHandState(state) - { - logger.debug('sendRaiseHandState: ', state); - - store.dispatch( - stateActions.setMyRaiseHandStateInProgress(true)); - - try - { - await this.sendRequest('raisehand-message', { raiseHandState: state }); - - store.dispatch( - stateActions.setMyRaiseHandState(state)); - } - catch (error) - { - logger.error('sendRaiseHandState() | failed: %o', error); - - this.notify(`An error occured while ${state ? 'raising' : 'lowering'} hand.`); - - // We need to refresh the component for it to render changed state - store.dispatch(stateActions.setMyRaiseHandState(!state)); - } - - store.dispatch( - stateActions.setMyRaiseHandStateInProgress(false)); - } - - async restartIce() - { - logger.debug('restartIce()'); - - store.dispatch( - stateActions.setRestartIceInProgress(true)); - - try - { - await this._room.restartIce(); - } - catch (error) - { - logger.error('restartIce() failed: %o', error); - } - - // Make it artificially longer. - setTimeout(() => - { - store.dispatch( - stateActions.setRestartIceInProgress(false)); - }, 500); - } - - async resumeAudio() - { - logger.debug('resumeAudio()'); - try - { - await this._audioContext.resume(); - - store.dispatch( - stateActions.setAudioSuspended({ audioSuspended: false })); - - } - catch (error) - { - logger.error('resumeAudioJoin() failed: %o', error); - } - } - - join() - { - this._signalingSocket = io(this._signalingUrl); - - if (this._device.flag === 'firefox') - ROOM_OPTIONS = Object.assign({ iceTransportPolicy: 'relay' }, ROOM_OPTIONS); - - // mediasoup-client Room instance. - this._room = new mediasoupClient.Room(ROOM_OPTIONS); - this._room.roomId = this._roomId; - - this._spotlights = new Spotlights(this._maxSpotlights, this._room); - - store.dispatch(stateActions.setRoomState('connecting')); - - this._signalingSocket.on('connect', () => - { - logger.debug('signaling Peer "connect" event'); - }); - - this._signalingSocket.on('room-ready', () => - { - logger.debug('signaling Peer "room-ready" event'); - - this._joinRoom(); - }); - - this._signalingSocket.on('room-locked', () => - { - logger.debug('signaling Peer "room-locked" event'); - - store.dispatch(stateActions.setRoomLockedOut()); - }); - - this._signalingSocket.on('disconnect', () => - { - logger.warn('signaling Peer "disconnect" event'); - - this.notify('You are disconnected.'); - - // Leave Room. - try { this._room.remoteClose({ cause: 'signaling disconnected' }); } - catch (error) {} - - store.dispatch(stateActions.setRoomState('connecting')); - }); - - this._signalingSocket.on('close', () => - { - if (this._closed) - return; - - logger.warn('signaling Peer "close" event'); - - this.close(); - }); - - this._signalingSocket.on('mediasoup-notification', (data) => - { - const notification = data; - - this._room.receiveNotification(notification); - }); - - this._signalingSocket.on('lock-room', ({ peerName }) => - { - store.dispatch( - stateActions.setRoomLocked()); - - const peer = this._room.getPeerByName(peerName); - - if (peer) - { - this.notify(`${peer.appData.displayName} locked the room.`); - } - }); - - this._signalingSocket.on('unlock-room', ({ peerName }) => - { - store.dispatch( - stateActions.setRoomUnLocked()); - - const peer = this._room.getPeerByName(peerName); - - if (peer) - { - this.notify(`${peer.appData.displayName} unlocked the room.`); - } - }); - - this._signalingSocket.on('active-speaker', ({ peerName }) => - { - store.dispatch( - stateActions.setRoomActiveSpeaker(peerName)); - - if (peerName && peerName !== this._peerName) - this._spotlights.handleActiveSpeaker(peerName); - }); - - this._signalingSocket.on('display-name-changed', ({ peerName, displayName: name }) => - { - // NOTE: Hack, we shouldn't do this, but this is just a demo. - const peer = this._room.getPeerByName(peerName); - - if (!peer) - { - logger.error('peer not found'); - - return; - } - - const oldDisplayName = peer.appData.name; - - peer.appData.displayName = name; - - store.dispatch( - stateActions.setPeerDisplayName(name, peerName)); - - this.notify(`${oldDisplayName} changed their display name to ${name}.`); - }); - - this._signalingSocket.on('profile-picture-changed', ({ peerName, picture }) => - { - store.dispatch(stateActions.setPeerPicture(peerName, picture)); - }); - - // This means: server wants to change MY user information - this._signalingSocket.on('auth', (data) => - { - logger.debug('got auth event from server', data); - - this.changeDisplayName(data.name); - - this.changeProfilePicture(data.picture); - store.dispatch(stateActions.setPicture(data.picture)); - store.dispatch(stateActions.loggedIn()); - - this.notify('You are logged in.'); - - this.closeLoginWindow(); - }); - - this._signalingSocket.on('raisehand-message', (data) => - { - const { peerName, raiseHandState } = data; - - logger.debug('Got raiseHandState from "%s"', peerName); - - // NOTE: Hack, we shouldn't do this, but this is just a demo. - const peer = this._room.getPeerByName(peerName); - - if (!peer) - { - logger.error('peer not found'); - - return; - } - - this.notify(`${peer.appData.displayName} ${raiseHandState ? 'raised' : 'lowered'} their hand.`); - - store.dispatch( - stateActions.setPeerRaiseHandState(peerName, raiseHandState)); - }); - - this._signalingSocket.on('chat-message-receive', (data) => - { - const { peerName, chatMessage } = data; - - logger.debug('Got chat from "%s"', peerName); - - store.dispatch( - stateActions.addResponseMessage({ ...chatMessage, peerName })); - - if (!store.getState().toolarea.toolAreaOpen || - (store.getState().toolarea.toolAreaOpen && - store.getState().toolarea.currentToolTab !== 'chat')) // Make sound - { - this._soundNotification(); - } - }); - - this._signalingSocket.on('file-receive', (data) => - { - const { peerName, file } = data; - - // NOTE: Hack, we shouldn't do this, but this is just a demo. - const peer = this._room.getPeerByName(peerName); - - if (!peer) - { - logger.error('peer not found'); - - return; - } - - store.dispatch(stateActions.addFile(file)); - - this.notify(`${peer.appData.displayName} shared a file.`); - - if (!store.getState().toolarea.toolAreaOpen || - (store.getState().toolarea.toolAreaOpen && - store.getState().toolarea.currentToolTab !== 'files')) // Make sound - { - this._soundNotification(); - } - }); - } - - async _joinRoom() - { - logger.debug('_joinRoom()'); - - // NOTE: We allow rejoining (room.join()) the same mediasoup Room when - // WebSocket re-connects, so we must clean existing event listeners. Otherwise - // they will be called twice after the reconnection. - this._room.removeAllListeners(); - - this._room.on('close', (originator, appData) => - { - if (originator === 'remote') - { - logger.warn('mediasoup Peer/Room remotely closed [appData:%o]', appData); - - store.dispatch(stateActions.setRoomState('closed')); - - return; - } - }); - - this._room.on('request', (request, callback, errback) => - { - logger.debug( - 'sending mediasoup request [method:%s]:%o', request.method, request); - - this.sendRequest('mediasoup-request', request) - .then(callback) - .catch(errback); - }); - - this._room.on('notify', (notification) => - { - logger.debug( - 'sending mediasoup notification [method:%s]:%o', - notification.method, notification); - - this.sendRequest('mediasoup-notification', notification) - .catch((error) => - { - logger.warn('could not send mediasoup notification:%o', error); - }); - }); - - this._room.on('newpeer', (peer) => - { - logger.debug( - 'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer); - - this._soundNotification(); - - this._handlePeer(peer); - }); - - try - { - await this._room.join( - this._peerName, - { - displayName : this._displayName, - device : this._device - } - ); - - store.dispatch( - stateActions.setFileSharingSupported(this._torrentSupport)); - - this._sendTransport = - this._room.createTransport('send', { media: 'SEND_MIC_WEBCAM' }); - - this._sendTransport.on('close', (originator) => - { - logger.debug( - 'Transport "close" event [originator:%s]', originator); - }); - - // Create Transport for receiving. - this._recvTransport = - this._room.createTransport('recv', { media: 'RECV' }); - - this._recvTransport.on('close', (originator) => - { - logger.debug( - 'receiving Transport "close" event [originator:%s]', originator); - }); - - // Set our media capabilities. - store.dispatch(stateActions.setMediaCapabilities( - { - canSendMic : this._room.canSend('audio'), - canSendWebcam : this._room.canSend('video') - })); - store.dispatch(stateActions.setScreenCapabilities( - { - canShareScreen : this._room.canSend('video') && - this._screenSharing.isScreenShareAvailable(), - needExtension : this._screenSharing.needExtension() - })); - - // Don't produce if explicitely requested to not to do it. - if (this._produce) - { - if (this._room.canSend('audio')) - await this._setMicProducer(); - - // Add our webcam (unless the cookie says no). - if (this._room.canSend('video')) - { - const devicesCookie = cookiesManager.getDevices(); - - if (!devicesCookie || devicesCookie.webcamEnabled) - await this.enableWebcam(); - } - } - - store.dispatch(stateActions.setRoomState('connected')); - - // Clean all the existing notifcations. - store.dispatch(stateActions.removeAllNotifications()); - - this.getServerHistory(); - - this.notify('You have joined the room.'); - - this._spotlights.on('spotlights-updated', (spotlights) => - { - store.dispatch(stateActions.setSpotlights(spotlights)); - this.updateSpotlights(spotlights); - }); - - const peers = this._room.peers; - - for (const peer of peers) - { - this._handlePeer(peer, { notify: false }); - } - - this._spotlights.start(); - } - catch (error) - { - logger.error('_joinRoom() failed:%o', error); - - this.notify('An error occured while joining the room.'); - - this.close(); - } - } - - async lockRoom() - { - logger.debug('lockRoom()'); - - try - { - await this.sendRequest('lock-room'); - - store.dispatch( - stateActions.setRoomLocked()); - this.notify('You locked the room.'); - } - catch (error) - { - logger.error('lockRoom() | failed: %o', error); - } - } - - async unlockRoom() - { - logger.debug('unlockRoom()'); - - try - { - await this.sendRequest('unlock-room'); - - store.dispatch( - stateActions.setRoomUnLocked()); - this.notify('You unlocked the room.'); - } - catch (error) - { - logger.error('unlockRoom() | failed: %o', error); - } - } - - async _setMicProducer() - { - if (!this._room.canSend('audio')) - throw new Error('cannot send audio'); - - if (this._micProducer) - throw new Error('mic Producer already exists'); - - let producer; - - try - { - logger.debug('_setMicProducer() | calling getUserMedia()'); - - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - - const track = stream.getAudioTracks()[0]; - - producer = this._room.createProducer(track, null, { source: 'mic' }); - - // No need to keep original track. - track.stop(); - - // Send it. - await producer.send(this._sendTransport); - - this._micProducer = producer; - - store.dispatch(stateActions.addProducer( - { - id : producer.id, - source : 'mic', - locallyPaused : producer.locallyPaused, - remotelyPaused : producer.remotelyPaused, - track : producer.track, - codec : producer.rtpParameters.codecs[0].name - })); - - logger.debug('_setMicProducer() | calling _updateAudioDevices()'); - - await this._updateAudioDevices(); - - producer.on('close', (originator) => - { - logger.debug( - 'mic Producer "close" event [originator:%s]', originator); - - this._micProducer = null; - store.dispatch(stateActions.removeProducer(producer.id)); - }); - - producer.on('pause', (originator) => - { - logger.debug( - 'mic Producer "pause" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerPaused(producer.id, originator)); - }); - - producer.on('resume', (originator) => - { - logger.debug( - 'mic Producer "resume" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerResumed(producer.id, originator)); - }); - - producer.on('handled', () => - { - logger.debug('mic Producer "handled" event'); - }); - - producer.on('unhandled', () => - { - logger.debug('mic Producer "unhandled" event'); - }); - - const harkStream = new MediaStream; - - harkStream.addTrack(producer.track); - if (!harkStream.getAudioTracks()[0]) - throw new Error('_setMicProducer(): given stream has no audio track'); - producer.hark = hark(harkStream, { play: false }); - - // eslint-disable-next-line no-unused-vars - producer.hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exagerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - - if (volume !== producer.volume) - { - producer.volume = volume; - store.dispatch(stateActions.setProducerVolume(producer.id, volume)); - } - }); - - this._audioContext = new AudioContext(); - - // We need to provoke user interaction to get permission from browser to start audio - if (this._audioContext.state === 'suspended') - { - store.dispatch(stateActions.setAudioSuspended({ audioSuspended: true })); - } - } - catch (error) - { - logger.error('_setMicProducer() failed:%o', error); - - this.notify('An error occured while accessing your microphone.'); - - if (producer) - producer.close(); - - } - } - - async _setScreenShareProducer() - { - if (!this._room.canSend('video')) - throw new Error('cannot send screen'); - - let producer; - - try - { - const available = this._screenSharing.isScreenShareAvailable() && - !this._screenSharing.needExtension(); - - if (!available) - throw new Error('screen sharing not available'); - - logger.debug('_setScreenShareProducer() | calling getUserMedia()'); - - const stream = await this._screenSharing.start({ - width : 1280, - height : 720, - frameRate : 3 - }); - - const track = stream.getVideoTracks()[0]; - - producer = this._room.createProducer( - track, { simulcast: false }, { source: 'screen' }); - - // No need to keep original track. - track.stop(); - - // Send it. - await producer.send(this._sendTransport); - - this._screenSharingProducer = producer; - - store.dispatch(stateActions.addProducer( - { - id : producer.id, - source : 'screen', - deviceLabel : 'screen', - type : 'screen', - locallyPaused : producer.locallyPaused, - remotelyPaused : producer.remotelyPaused, - track : producer.track, - codec : producer.rtpParameters.codecs[0].name - })); - - producer.on('close', (originator) => - { - logger.debug( - 'webcam Producer "close" event [originator:%s]', originator); - - this._screenSharingProducer = null; - store.dispatch(stateActions.removeProducer(producer.id)); - }); - - producer.on('trackended', (originator) => - { - logger.debug( - 'webcam Producer "trackended" event [originator:%s]', originator); - - this.disableScreenSharing(); - }); - - producer.on('pause', (originator) => - { - logger.debug( - 'webcam Producer "pause" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerPaused(producer.id, originator)); - }); - - producer.on('resume', (originator) => - { - logger.debug( - 'webcam Producer "resume" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerResumed(producer.id, originator)); - }); - - producer.on('handled', () => - { - logger.debug('webcam Producer "handled" event'); - }); - - producer.on('unhandled', () => - { - logger.debug('webcam Producer "unhandled" event'); - }); - - logger.debug('_setScreenShareProducer() succeeded'); - } - catch (error) - { - logger.error('_setScreenShareProducer() failed:%o', error); - - if (error.name === 'NotAllowedError') // Request to share denied by user - { - this.notify('Request to start sharing your screen was denied.'); - } - else // Some other error - { - this.notify('An error occured while starting to share your screen.'); - } - - if (producer) - producer.close(); - - throw error; - } - } - - async _setWebcamProducer() - { - if (!this._room.canSend('video')) - throw new Error('cannot send video'); - - if (this._webcamProducer) - throw new Error('webcam Producer already exists'); - - let producer; - - try - { - logger.debug('_setWebcamProducer() | calling getUserMedia()'); - - const stream = await navigator.mediaDevices.getUserMedia( - { - video : - { - ...VIDEO_CONSTRAINS - } - }); - - const track = stream.getVideoTracks()[0]; - - producer = this._room.createProducer( - track, { simulcast: this._useSimulcast }, { source: 'webcam' }); - - // No need to keep original track. - track.stop(); - - // Send it. - await producer.send(this._sendTransport); - - this._webcamProducer = producer; - - store.dispatch(stateActions.addProducer( - { - id : producer.id, - source : 'webcam', - locallyPaused : producer.locallyPaused, - remotelyPaused : producer.remotelyPaused, - track : producer.track, - codec : producer.rtpParameters.codecs[0].name - })); - - logger.debug('_setWebcamProducer() | calling _updateWebcams()'); - await this._updateWebcams(); - - producer.on('close', (originator) => - { - logger.debug( - 'webcam Producer "close" event [originator:%s]', originator); - - this._webcamProducer = null; - store.dispatch(stateActions.removeProducer(producer.id)); - }); - - producer.on('pause', (originator) => - { - logger.debug( - 'webcam Producer "pause" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerPaused(producer.id, originator)); - }); - - producer.on('resume', (originator) => - { - logger.debug( - 'webcam Producer "resume" event [originator:%s]', originator); - - store.dispatch(stateActions.setProducerResumed(producer.id, originator)); - }); - - producer.on('handled', () => - { - logger.debug('webcam Producer "handled" event'); - }); - - producer.on('unhandled', () => - { - logger.debug('webcam Producer "unhandled" event'); - }); - - logger.debug('_setWebcamProducer() succeeded'); - } - catch (error) - { - logger.error('_setWebcamProducer() failed:%o', error); - - this.notify('An error occured while accessing your camera.'); - - if (producer) - producer.close(); - - throw error; - } - } - - async _updateAudioDevices() - { - logger.debug('_updateAudioDevices()'); - - // Reset the list. - this._audioDevices = new Map(); - - try - { - logger.debug('_updateAudioDevices() | calling enumerateDevices()'); - - const devices = await navigator.mediaDevices.enumerateDevices(); - - for (const device of devices) - { - if (device.kind !== 'audioinput') - continue; - - device.value = device.deviceId; - - this._audioDevices.set(device.deviceId, device); - } - - const array = Array.from(this._audioDevices.values()); - const len = array.length; - const currentAudioDeviceId = - this._audioDevice.device ? this._audioDevice.device.deviceId : undefined; - - logger.debug('_updateAudioDevices() [audiodevices:%o]', array); - - if (len === 0) - this._audioDevice.device = null; - else if (!this._audioDevices.has(currentAudioDeviceId)) - this._audioDevice.device = array[0]; - - store.dispatch( - stateActions.setCanChangeAudioDevice(len >= 2)); - if (len >= 1) - store.dispatch( - stateActions.setAudioDevices(this._audioDevices)); - } - catch (error) - { - logger.error('_updateAudioDevices() failed:%o', error); - } - } - - async _updateWebcams() - { - logger.debug('_updateWebcams()'); - - // Reset the list. - this._webcams = new Map(); - - try - { - logger.debug('_updateWebcams() | calling enumerateDevices()'); - - const devices = await navigator.mediaDevices.enumerateDevices(); - - for (const device of devices) - { - if (device.kind !== 'videoinput') - continue; - - device.value = device.deviceId; - - this._webcams.set(device.deviceId, device); - } - - const array = Array.from(this._webcams.values()); - const len = array.length; - const currentWebcamId = - this._webcam.device ? this._webcam.device.deviceId : undefined; - - logger.debug('_updateWebcams() [webcams:%o]', array); - - if (len === 0) - this._webcam.device = null; - else if (!this._webcams.has(currentWebcamId)) - this._webcam.device = array[0]; - - if (len >= 1) - store.dispatch( - stateActions.setWebcamDevices(this._webcams)); - } - catch (error) - { - logger.error('_updateWebcams() failed:%o', error); - } - } - - _handlePeer(peer, { notify = true } = {}) - { - const displayName = peer.appData.displayName; - - store.dispatch(stateActions.addPeer( - { - name : peer.name, - displayName : displayName, - device : peer.appData.device, - raiseHandState : peer.appData.raiseHandState, - consumers : [] - })); - - if (notify) - { - this.notify(`${displayName} joined the room.`); - } - - for (const consumer of peer.consumers) - { - this._handleConsumer(consumer); - } - - peer.on('close', (originator) => - { - logger.debug( - 'peer "close" event [name:"%s", originator:%s]', - peer.name, originator); - - store.dispatch(stateActions.removePeer(peer.name)); - - if (this._room.joined) - { - this.notify(`${displayName} left the room.`); - } - }); - - peer.on('newconsumer', (consumer) => - { - logger.debug( - 'peer "newconsumer" event [name:"%s", id:%s, consumer:%o]', - peer.name, consumer.id, consumer); - - this._handleConsumer(consumer); - }); - } - - _handleConsumer(consumer) - { - const codec = consumer.rtpParameters.codecs[0]; - - store.dispatch(stateActions.addConsumer( - { - id : consumer.id, - peerName : consumer.peer.name, - source : consumer.appData.source, - supported : consumer.supported, - locallyPaused : consumer.locallyPaused, - remotelyPaused : consumer.remotelyPaused, - track : null, - codec : codec ? codec.name : null - }, - consumer.peer.name) - ); - - consumer.on('close', (originator) => - { - logger.debug( - 'consumer "close" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - - store.dispatch(stateActions.removeConsumer( - consumer.id, consumer.peer.name)); - }); - - consumer.on('handled', (originator) => - { - logger.debug( - 'consumer "handled" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - if (consumer.kind === 'audio') - { - const stream = new MediaStream; - - stream.addTrack(consumer.track); - if (!stream.getAudioTracks()[0]) - throw new Error('consumer.on("handled" | given stream has no audio track'); - - consumer.hark = hark(stream, { play: false }); - - // eslint-disable-next-line no-unused-vars - consumer.hark.on('volume_change', (dBs, threshold) => - { - // The exact formula to convert from dBs (-100..0) to linear (0..1) is: - // Math.pow(10, dBs / 20) - // However it does not produce a visually useful output, so let exagerate - // it a bit. Also, let convert it from 0..1 to 0..10 and avoid value 1 to - // minimize component renderings. - let volume = Math.round(Math.pow(10, dBs / 85) * 10); - - if (volume === 1) - volume = 0; - - if (volume !== consumer.volume) - { - consumer.volume = volume; - store.dispatch(stateActions.setConsumerVolume(consumer.id, volume)); - } - }); - } - }); - - consumer.on('pause', (originator) => - { - logger.debug( - 'consumer "pause" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - - store.dispatch(stateActions.setConsumerPaused(consumer.id, originator)); - }); - - consumer.on('resume', (originator) => - { - logger.debug( - 'consumer "resume" event [id:%s, originator:%s, consumer:%o]', - consumer.id, originator, consumer); - - store.dispatch(stateActions.setConsumerResumed(consumer.id, originator)); - }); - - consumer.on('effectiveprofilechange', (profile) => - { - logger.debug( - 'consumer "effectiveprofilechange" event [id:%s, consumer:%o, profile:%s]', - consumer.id, consumer, profile); - - store.dispatch(stateActions.setConsumerEffectiveProfile(consumer.id, profile)); - }); - - // Receive the consumer (if we can). - if (consumer.supported) - { - if (consumer.kind === 'video' && - !this._spotlights.peerInSpotlights(consumer.peer.name)) - { // Start paused - logger.debug( - 'consumer paused by default'); - consumer.pause('not-speaker'); - } - - consumer.receive(this._recvTransport) - .then((track) => - { - store.dispatch(stateActions.setConsumerTrack(consumer.id, track)); - }) - .catch((error) => - { - logger.error( - 'unexpected error while receiving a new Consumer:%o', error); - }); - } - } -} diff --git a/app/lib/Spotlights.js b/app/lib/Spotlights.js deleted file mode 100644 index 8b91e992..00000000 --- a/app/lib/Spotlights.js +++ /dev/null @@ -1,184 +0,0 @@ -import { EventEmitter } from 'events'; -import Logger from './Logger'; - -const logger = new Logger('Spotlight'); - -export default class Spotlights extends EventEmitter -{ - constructor(maxSpotlights, room) - { - super(); - - this._room = room; - this._maxSpotlights = maxSpotlights; - this._peerList = []; - this._selectedSpotlights = []; - this._currentSpotlights = []; - this._started = false; - } - - start() - { - const peers = this._room.peers; - - for (const peer of peers) - { - this._handlePeer(peer); - } - - this._handleRoom(); - - this._started = true; - this._spotlightsUpdated(); - } - - peerInSpotlights(peerName) - { - if (this._started) - { - return this._currentSpotlights.indexOf(peerName) !== -1; - } - else - { - return false; - } - } - - setPeerSpotlight(peerName) - { - logger.debug('setPeerSpotlight() [peerName:"%s"]', peerName); - - const index = this._selectedSpotlights.indexOf(peerName); - - if (index !== -1) - { - this._selectedSpotlights = []; - } - else - { - this._selectedSpotlights = [ peerName ]; - } - - /* - if (index === -1) // We don't have this peer in the list, adding - { - this._selectedSpotlights.push(peerName); - } - else // We have this peer, remove - { - this._selectedSpotlights.splice(index, 1); - } - */ - - if (this._started) - this._spotlightsUpdated(); - } - - _handleRoom() - { - this._room.on('newpeer', (peer) => - { - logger.debug( - 'room "newpeer" event [name:"%s", peer:%o]', peer.name, peer); - this._handlePeer(peer); - }); - } - - addSpeakerList(speakerList) - { - this._peerList = [ ...new Set([ ...speakerList, ...this._peerList ]) ]; - - if (this._started) - this._spotlightsUpdated(); - } - - _handlePeer(peer) - { - logger.debug('_handlePeer() [peerName:"%s"]', peer.name); - - if (this._peerList.indexOf(peer.name) === -1) // We don't have this peer in the list - { - peer.on('close', () => - { - let index = this._peerList.indexOf(peer.name); - - if (index !== -1) // We have this peer in the list, remove - { - this._peerList.splice(index, 1); - } - - index = this._selectedSpotlights.indexOf(peer.name); - - if (index !== -1) // We have this peer in the list, remove - { - this._selectedSpotlights.splice(index, 1); - } - - this._spotlightsUpdated(); - }); - - logger.debug('_handlePeer() | adding peer [peerName:"%s"]', peer.name); - - this._peerList.push(peer.name); - - this._spotlightsUpdated(); - } - } - - handleActiveSpeaker(peerName) - { - logger.debug('handleActiveSpeaker() [peerName:"%s"]', peerName); - - const index = this._peerList.indexOf(peerName); - - if (index > -1) - { - this._peerList.splice(index, 1); - this._peerList = [ peerName ].concat(this._peerList); - - this._spotlightsUpdated(); - } - } - - _spotlightsUpdated() - { - let spotlights; - - if (this._selectedSpotlights.length > 0) - { - spotlights = [ ...new Set([ ...this._selectedSpotlights, ...this._peerList ]) ]; - } - else - { - spotlights = this._peerList; - } - - if ( - !this._arraysEqual( - this._currentSpotlights, spotlights.slice(0, this._maxSpotlights) - ) - ) - { - logger.debug('_spotlightsUpdated() | spotlights updated, emitting'); - - this._currentSpotlights = spotlights.slice(0, this._maxSpotlights); - this.emit('spotlights-updated', this._currentSpotlights); - } - else - logger.debug('_spotlightsUpdated() | spotlights not updated'); - } - - _arraysEqual(arr1, arr2) - { - if (arr1.length !== arr2.length) - return false; - - for (let i = arr1.length; i--;) - { - if (arr1[i] !== arr2[i]) - return false; - } - - return true; - } -} diff --git a/app/lib/components/Containers/HiddenPeers.jsx b/app/lib/components/Containers/HiddenPeers.jsx deleted file mode 100644 index 595a838e..00000000 --- a/app/lib/components/Containers/HiddenPeers.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import * as stateActions from '../../redux/stateActions'; - -class HiddenPeers extends Component -{ - constructor(props) - { - super(props); - this.state = { className: '' }; - } - - componentDidUpdate(prevProps) - { - const { hiddenPeersCount } = this.props; - - if (hiddenPeersCount !== prevProps.hiddenPeersCount) - { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ className: 'pulse' }, () => - { - if (this.timeout) - { - clearTimeout(this.timeout); - } - - this.timeout = setTimeout(() => - { - this.setState({ className: '' }); - }, 500); - }); - } - } - - render() - { - const { - hiddenPeersCount, - openUsersTab - } = this.props; - - return ( -
-
openUsersTab()}> -

+{hiddenPeersCount}
participant - {(hiddenPeersCount === 1) ? null : 's'} -

-
-
- ); - } -} - -HiddenPeers.propTypes = -{ - hiddenPeersCount : PropTypes.number, - openUsersTab : PropTypes.func.isRequired -}; - -const mapDispatchToProps = (dispatch) => -{ - return { - openUsersTab : () => - { - dispatch(stateActions.openToolArea()); - dispatch(stateActions.setToolTab('users')); - } - }; -}; - -const HiddenPeersContainer = connect( - null, - mapDispatchToProps -)(HiddenPeers); - -export default HiddenPeersContainer; diff --git a/app/lib/components/Containers/Me.jsx b/app/lib/components/Containers/Me.jsx deleted file mode 100644 index 673cd525..00000000 --- a/app/lib/components/Containers/Me.jsx +++ /dev/null @@ -1,240 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ReactTooltip from 'react-tooltip'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { getDeviceInfo } from 'mediasoup-client'; -import * as appPropTypes from '../appPropTypes'; -import { withRoomContext } from '../../RoomContext'; -import PeerView from '../VideoContainers/PeerView'; -import ScreenView from '../VideoContainers/ScreenView'; - -class Me extends React.Component -{ - state = { - controlsVisible : false - }; - - handleMouseOver = () => - { - this.setState({ - controlsVisible : true - }); - }; - - handleMouseOut = () => - { - this.setState({ - controlsVisible : false - }); - }; - - constructor(props) - { - super(props); - - this._mounted = false; - this._rootNode = null; - this._tooltip = true; - - // TODO: Issue when using react-tooltip in Edge: - // https://github.com/wwayne/react-tooltip/issues/328 - if (getDeviceInfo().flag === 'msedge') - this._tooltip = false; - } - - render() - { - const { - roomClient, - connected, - me, - advancedMode, - micProducer, - webcamProducer, - screenProducer - } = this.props; - - let micState; - - if (!me.canSendMic) - micState = 'unsupported'; - else if (!micProducer) - micState = 'unsupported'; - else if (!micProducer.locallyPaused && !micProducer.remotelyPaused) - micState = 'on'; - else - micState = 'off'; - - let webcamState; - - if (!me.canSendWebcam) - webcamState = 'unsupported'; - else if (webcamProducer) - webcamState = 'on'; - else - webcamState = 'off'; - - const videoVisible = ( - Boolean(webcamProducer) && - !webcamProducer.locallyPaused && - !webcamProducer.remotelyPaused - ); - - const screenVisible = ( - Boolean(screenProducer) && - !screenProducer.locallyPaused && - !screenProducer.remotelyPaused - ); - - let tip; - - if (!me.displayNameSet) - tip = 'Click on your name to change it'; - - return ( -
(this._rootNode = node)} - data-tip={tip} - data-tip-disable={!tip} - data-type='dark' - onMouseOver={this.handleMouseOver} - onMouseOut={this.handleMouseOut} - > -
- -
-
- { - micState === 'on' ? - roomClient.muteMic() : - roomClient.unmuteMic(); - }} - /> - -
- { - webcamState === 'on' ? - roomClient.disableWebcam() : - roomClient.enableWebcam(); - }} - /> -
- - - - { - roomClient.changeDisplayName(displayName); - }} - /> -
- - -
- -
-
-
- ); - } - - componentDidMount() - { - this._mounted = true; - - if (this._tooltip) - { - setTimeout(() => - { - if (!this._mounted || this.props.me.displayNameSet) - return; - - ReactTooltip.show(this._rootNode); - }, 4000); - } - } - - componentWillUnmount() - { - this._mounted = false; - } - - componentWillReceiveProps(nextProps) - { - if (this._tooltip) - { - if (nextProps.me.displayNameSet) - ReactTooltip.hide(this._rootNode); - } - } -} - -Me.propTypes = -{ - roomClient : PropTypes.any.isRequired, - connected : PropTypes.bool.isRequired, - advancedMode : PropTypes.bool, - me : appPropTypes.Me.isRequired, - micProducer : appPropTypes.Producer, - webcamProducer : appPropTypes.Producer, - screenProducer : appPropTypes.Producer -}; - -const mapStateToProps = (state) => -{ - const producersArray = Object.values(state.producers); - const micProducer = - producersArray.find((producer) => producer.source === 'mic'); - const webcamProducer = - producersArray.find((producer) => producer.source === 'webcam'); - const screenProducer = - producersArray.find((producer) => producer.source === 'screen'); - - return { - connected : state.room.state === 'connected', - me : state.me, - micProducer : micProducer, - webcamProducer : webcamProducer, - screenProducer : screenProducer - }; -}; - -const MeContainer = withRoomContext(connect( - mapStateToProps -)(Me)); - -export default MeContainer; diff --git a/app/lib/components/Containers/Peer.jsx b/app/lib/components/Containers/Peer.jsx deleted file mode 100644 index cb54edc5..00000000 --- a/app/lib/components/Containers/Peer.jsx +++ /dev/null @@ -1,266 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import * as appPropTypes from '../appPropTypes'; -import { withRoomContext } from '../../RoomContext'; -import * as stateActions from '../../redux/stateActions'; -import PeerView from '../VideoContainers/PeerView'; -import ScreenView from '../VideoContainers/ScreenView'; - -class Peer extends Component -{ - state = { - controlsVisible : false - }; - - handleMouseOver = () => - { - this.setState({ - controlsVisible : true - }); - }; - - handleMouseOut = () => - { - this.setState({ - controlsVisible : false - }); - }; - - render() - { - const { - roomClient, - advancedMode, - peer, - micConsumer, - webcamConsumer, - screenConsumer, - toggleConsumerFullscreen, - toggleConsumerWindow, - style, - windowConsumer - } = this.props; - - const micEnabled = ( - Boolean(micConsumer) && - !micConsumer.locallyPaused && - !micConsumer.remotelyPaused - ); - - const videoVisible = ( - Boolean(webcamConsumer) && - !webcamConsumer.locallyPaused && - !webcamConsumer.remotelyPaused - ); - - const screenVisible = ( - Boolean(screenConsumer) && - !screenConsumer.locallyPaused && - !screenConsumer.remotelyPaused - ); - - let videoProfile; - - if (webcamConsumer) - videoProfile = webcamConsumer.profile; - - let screenProfile; - - if (screenConsumer) - screenProfile = screenConsumer.profile; - - return ( -
- -
-

incompatible video

-
-
- - -
-

this video is paused

-
-
- -
-
- -
- -
-
-
- { - e.stopPropagation(); - micEnabled ? - roomClient.modifyPeerConsumer(peer.name, 'mic', true) : - roomClient.modifyPeerConsumer(peer.name, 'mic', false); - }} - /> - -
- { - e.stopPropagation(); - toggleConsumerWindow(webcamConsumer); - }} - /> - -
- { - e.stopPropagation(); - toggleConsumerFullscreen(webcamConsumer); - }} - /> -
- - -
- - -
-
-
- { - e.stopPropagation(); - toggleConsumerWindow(screenConsumer); - }} - /> - -
- { - e.stopPropagation(); - toggleConsumerFullscreen(screenConsumer); - }} - /> -
- -
- -
- ); - } -} - -Peer.propTypes = -{ - roomClient : PropTypes.any.isRequired, - advancedMode : PropTypes.bool, - peer : appPropTypes.Peer.isRequired, - micConsumer : appPropTypes.Consumer, - webcamConsumer : appPropTypes.Consumer, - screenConsumer : appPropTypes.Consumer, - windowConsumer : PropTypes.number, - streamDimensions : PropTypes.object, - style : PropTypes.object, - toggleConsumerFullscreen : PropTypes.func.isRequired, - toggleConsumerWindow : PropTypes.func.isRequired -}; - -const mapStateToProps = (state, { name }) => -{ - const peer = state.peers[name]; - const consumersArray = peer.consumers - .map((consumerId) => state.consumers[consumerId]); - const micConsumer = - consumersArray.find((consumer) => consumer.source === 'mic'); - const webcamConsumer = - consumersArray.find((consumer) => consumer.source === 'webcam'); - const screenConsumer = - consumersArray.find((consumer) => consumer.source === 'screen'); - - return { - peer, - micConsumer, - webcamConsumer, - screenConsumer, - windowConsumer : state.room.windowConsumer - }; -}; - -const mapDispatchToProps = (dispatch) => -{ - return { - toggleConsumerFullscreen : (consumer) => - { - if (consumer) - dispatch(stateActions.toggleConsumerFullscreen(consumer.id)); - }, - toggleConsumerWindow : (consumer) => - { - if (consumer) - dispatch(stateActions.toggleConsumerWindow(consumer.id)); - } - }; -}; - -const PeerContainer = withRoomContext(connect( - mapStateToProps, - mapDispatchToProps -)(Peer)); - -export default PeerContainer; diff --git a/app/lib/components/Controls/Sidebar.jsx b/app/lib/components/Controls/Sidebar.jsx deleted file mode 100644 index 32fa6e67..00000000 --- a/app/lib/components/Controls/Sidebar.jsx +++ /dev/null @@ -1,232 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import classnames from 'classnames'; -import * as appPropTypes from '../appPropTypes'; -import { withRoomContext } from '../../RoomContext'; -import FullScreen from '../FullScreen'; - -class Sidebar extends Component -{ - constructor(props) - { - super(props); - - this.fullscreen = new FullScreen(document); - this.state = { - fullscreen : false - }; - } - - handleToggleFullscreen = () => - { - if (this.fullscreen.fullscreenElement) - { - this.fullscreen.exitFullscreen(); - } - else - { - this.fullscreen.requestFullscreen(document.documentElement); - } - }; - - handleFullscreenChange = () => - { - this.setState({ - fullscreen : this.fullscreen.fullscreenElement !== null - }); - }; - - componentDidMount() - { - if (this.fullscreen.fullscreenEnabled) - { - this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); - } - } - - componentWillUnmount() - { - if (this.fullscreen.fullscreenEnabled) - { - this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); - } - } - - render() - { - const { - roomClient, - toolbarsVisible, - me, - screenProducer, - locked - } = this.props; - - let screenState; - let screenTip; - let lockState = 'unlocked'; - - if (me.needExtension) - { - screenState = 'need-extension'; - screenTip = 'Install screen sharing extension'; - } - else if (!me.canShareScreen) - { - screenState = 'unsupported'; - screenTip = 'Screen sharing not supported'; - } - else if (screenProducer) - { - screenState = 'on'; - screenTip = 'Stop screen sharing'; - } - else - { - screenState = 'off'; - screenTip = 'Start screen sharing'; - } - - if (locked) - { - lockState = 'locked'; - } - - return ( -
- -
- - -
- { - switch (screenState) - { - case 'on': - { - roomClient.disableScreenSharing(); - break; - } - case 'off': - { - roomClient.enableScreenSharing(); - break; - } - case 'need-extension': - { - roomClient.installExtension(); - break; - } - default: - { - break; - } - } - }} - /> - - - - -
roomClient.logout()} - > - -
-
- -
roomClient.login()} - /> - - - -
- { - if (locked) - { - roomClient.unlockRoom(); - } - else - { - roomClient.lockRoom(); - } - }} - /> -
roomClient.sendRaiseHandState(!me.raiseHand)} - /> - -
roomClient.close()} - /> -
- ); - } -} - -Sidebar.propTypes = { - roomClient : PropTypes.any.isRequired, - toolbarsVisible : PropTypes.bool.isRequired, - me : appPropTypes.Me.isRequired, - screenProducer : appPropTypes.Producer, - locked : PropTypes.bool.isRequired -}; - -const mapStateToProps = (state) => - ({ - toolbarsVisible : state.room.toolbarsVisible, - screenProducer : Object.values(state.producers) - .find((producer) => producer.source === 'screen'), - me : state.me, - locked : state.room.locked - }); - -export default withRoomContext(connect( - mapStateToProps -)(Sidebar)); diff --git a/app/lib/components/Layouts/Filmstrip.jsx b/app/lib/components/Layouts/Filmstrip.jsx deleted file mode 100644 index f5b19163..00000000 --- a/app/lib/components/Layouts/Filmstrip.jsx +++ /dev/null @@ -1,216 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ResizeObserver from 'resize-observer-polyfill'; -import { connect } from 'react-redux'; -import debounce from 'lodash/debounce'; -import classnames from 'classnames'; -import { withRoomContext } from '../../RoomContext'; -import Peer from '../Containers/Peer'; -import HiddenPeers from '../Containers/HiddenPeers'; - -class Filmstrip extends Component -{ - constructor(props) - { - super(props); - - this.activePeerContainer = React.createRef(); - } - - state = { - lastSpeaker : null, - width : 400 - }; - - // Find the name of the peer which is currently speaking. This is either - // the latest active speaker, or the manually selected peer, or, if no - // person has spoken yet, the first peer in the list of peers. - getActivePeerName = () => - { - if (this.props.selectedPeerName) - { - return this.props.selectedPeerName; - } - - if (this.state.lastSpeaker) - { - return this.state.lastSpeaker; - } - - const peerNames = Object.keys(this.props.peers); - - if (peerNames.length > 0) - { - return peerNames[0]; - } - }; - - isSharingCamera = (peerName) => this.props.peers[peerName] && - this.props.peers[peerName].consumers.some((consumer) => - this.props.consumers[consumer].source === 'screen'); - - getRatio = () => - { - let ratio = 4 / 3; - - if (this.isSharingCamera(this.getActivePeerName())) - { - ratio *= 2; - } - - return ratio; - }; - - updateDimensions = debounce(() => - { - const container = this.activePeerContainer.current; - - if (container) - { - const ratio = this.getRatio(); - - let width = container.clientWidth; - - if (width / ratio > container.clientHeight) - { - width = container.clientHeight * ratio; - } - - this.setState({ - width - }); - } - }, 200); - - componentDidMount() - { - window.addEventListener('resize', this.updateDimensions); - const observer = new ResizeObserver(this.updateDimensions); - - observer.observe(this.activePeerContainer.current); - this.updateDimensions(); - } - - componentWillUnmount() - { - window.removeEventListener('resize', this.updateDimensions); - } - - componentDidUpdate(prevProps) - { - if (prevProps !== this.props) - { - this.updateDimensions(); - - if (this.props.activeSpeakerName !== this.props.myName) - { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - lastSpeaker : this.props.activeSpeakerName - }); - } - } - } - - render() - { - const { - roomClient, - peers, - advancedMode, - spotlights, - spotlightsLength - } = this.props; - - const activePeerName = this.getActivePeerName(); - - return ( -
-
- -
- -
-
-
- -
-
- { Object.keys(peers).map((peerName) => - { - if (spotlights.find((spotlightsElement) => spotlightsElement === peerName)) - { - return ( -
roomClient.setSelectedPeer(peerName)} - className={classnames('film', { - selected : this.props.selectedPeerName === peerName, - active : this.state.lastSpeaker === peerName - })} - > -
- -
-
- ); - } - })} -
-
-
- - - -
- -
- ); - } -} - -Filmstrip.propTypes = { - roomClient : PropTypes.any.isRequired, - activeSpeakerName : PropTypes.string, - advancedMode : PropTypes.bool, - peers : PropTypes.object.isRequired, - consumers : PropTypes.object.isRequired, - myName : PropTypes.string.isRequired, - selectedPeerName : PropTypes.string, - spotlightsLength : PropTypes.number, - spotlights : PropTypes.array.isRequired -}; - -const mapStateToProps = (state) => -{ - const spotlightsLength = state.room.spotlights ? state.room.spotlights.length : 0; - - return { - activeSpeakerName : state.room.activeSpeakerName, - selectedPeerName : state.room.selectedPeerName, - peers : state.peers, - consumers : state.consumers, - myName : state.me.name, - spotlights : state.room.spotlights, - spotlightsLength - }; -}; - -export default withRoomContext(connect( - mapStateToProps, - undefined -)(Filmstrip)); diff --git a/app/lib/components/Layouts/Peers.jsx b/app/lib/components/Layouts/Peers.jsx deleted file mode 100644 index 573d2779..00000000 --- a/app/lib/components/Layouts/Peers.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import debounce from 'lodash/debounce'; -import { Appear } from '../transitions'; -import Peer from '../Containers/Peer'; -import HiddenPeers from '../Containers/HiddenPeers'; -import ResizeObserver from 'resize-observer-polyfill'; - -const RATIO = 1.334; - -class Peers extends React.Component -{ - constructor(props) - { - super(props); - - this.state = { - peerWidth : 400, - peerHeight : 300 - }; - - this.peersRef = React.createRef(); - } - - updateDimensions = debounce(() => - { - if (!this.peersRef.current) - { - return; - } - - const n = this.props.boxes; - - if (n === 0) - { - return; - } - - const width = this.peersRef.current.clientWidth; - const height = this.peersRef.current.clientHeight; - - let x, y, space; - - for (let rows = 1; rows < 100; rows = rows + 1) - { - x = width / Math.ceil(n / rows); - y = x / RATIO; - if (height < (y * rows)) - { - y = height / rows; - x = RATIO * y; - break; - } - space = height - (y * (rows)); - if (space < y) - { - break; - } - } - if (Math.ceil(this.state.peerWidth) !== Math.ceil(0.9 * x)) - { - this.setState({ - peerWidth : 0.9 * x, - peerHeight : 0.9 * y - }); - } - }, 200); - - componentDidMount() - { - window.addEventListener('resize', this.updateDimensions); - const observer = new ResizeObserver(this.updateDimensions); - - observer.observe(this.peersRef.current); - } - - componentWillUnmount() - { - window.removeEventListener('resize', this.updateDimensions); - } - - componentDidUpdate() - { - this.updateDimensions(); - } - - render() - { - const { - advancedMode, - activeSpeakerName, - peers, - spotlights, - spotlightsLength - } = this.props; - - const style = - { - 'width' : this.state.peerWidth, - 'height' : this.state.peerHeight - }; - - return ( -
- { Object.keys(peers).map((peerName) => - { - if (spotlights.find((spotlightsElement) => spotlightsElement === peerName)) - { - return ( - -
-
- -
-
-
- ); - } - })} -
- - - -
-
- ); - } -} - -Peers.propTypes = - { - advancedMode : PropTypes.bool, - peers : PropTypes.object.isRequired, - boxes : PropTypes.number, - activeSpeakerName : PropTypes.string, - selectedPeerName : PropTypes.string, - spotlightsLength : PropTypes.number, - spotlights : PropTypes.array.isRequired - }; - -const mapStateToProps = (state) => -{ - const spotlights = state.room.spotlights; - const spotlightsLength = spotlights ? state.room.spotlights.length : 0; - const boxes = spotlightsLength + Object.values(state.consumers) - .filter((consumer) => consumer.source === 'screen').length; - - return { - peers : state.peers, - boxes, - activeSpeakerName : state.room.activeSpeakerName, - selectedPeerName : state.room.selectedPeerName, - spotlights, - spotlightsLength - }; -}; - -const PeersContainer = connect( - mapStateToProps -)(Peers); - -export default PeersContainer; diff --git a/app/lib/components/Notifications.jsx b/app/lib/components/Notifications.jsx deleted file mode 100644 index 2d0baaf2..00000000 --- a/app/lib/components/Notifications.jsx +++ /dev/null @@ -1,70 +0,0 @@ -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, toolAreaOpen }) => -{ - return ( -
- { - notifications.map((notification) => - { - return ( - -
onClick(notification.id)} - > -
-

{notification.text}

-
- - ); - }) - } -
- ); -}; - -Notifications.propTypes = -{ - notifications : PropTypes.arrayOf(appPropTypes.Notification).isRequired, - onClick : PropTypes.func.isRequired, - toolAreaOpen : PropTypes.bool -}; - -const mapStateToProps = (state) => -{ - const { notifications } = state; - - return { - notifications, - toolAreaOpen : state.toolarea.toolAreaOpen - }; -}; - -const mapDispatchToProps = (dispatch) => -{ - return { - onClick : (notificationId) => - { - dispatch(stateActions.removeNotification(notificationId)); - } - }; -}; - -const NotificationsContainer = connect( - mapStateToProps, - mapDispatchToProps -)(Notifications); - -export default NotificationsContainer; diff --git a/app/lib/components/PeerAudio/AudioPeer.jsx b/app/lib/components/PeerAudio/AudioPeer.jsx deleted file mode 100644 index ad1a3b69..00000000 --- a/app/lib/components/PeerAudio/AudioPeer.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import * as appPropTypes from '../appPropTypes'; -import PeerAudio from './PeerAudio'; - -const AudioPeer = ({ micConsumer }) => -{ - return ( - - ); -}; - -AudioPeer.propTypes = -{ - micConsumer : appPropTypes.Consumer, - name : PropTypes.string -}; - -const mapStateToProps = (state, { name }) => -{ - const peer = state.peers[name]; - const consumersArray = peer.consumers - .map((consumerId) => state.consumers[consumerId]); - const micConsumer = - consumersArray.find((consumer) => consumer.source === 'mic'); - - return { - micConsumer - }; -}; - -const AudioPeerContainer = connect( - mapStateToProps -)(AudioPeer); - -export default AudioPeerContainer; diff --git a/app/lib/components/PeerAudio/AudioPeers.jsx b/app/lib/components/PeerAudio/AudioPeers.jsx deleted file mode 100644 index 3dce02e3..00000000 --- a/app/lib/components/PeerAudio/AudioPeers.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import * as appPropTypes from '../appPropTypes'; -import AudioPeer from './AudioPeer'; - -const AudioPeers = ({ peers }) => -{ - return ( -
- { - peers.map((peer) => - { - return ( - - ); - }) - } -
- ); -}; - -AudioPeers.propTypes = - { - peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired - }; - -const mapStateToProps = (state) => -{ - const peers = Object.values(state.peers); - - return { - peers - }; -}; - -const AudioPeersContainer = connect( - mapStateToProps -)(AudioPeers); - -export default AudioPeersContainer; diff --git a/app/lib/components/PeerAudio/PeerAudio.jsx b/app/lib/components/PeerAudio/PeerAudio.jsx deleted file mode 100644 index 871c8c42..00000000 --- a/app/lib/components/PeerAudio/PeerAudio.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class PeerAudio extends React.Component -{ - constructor(props) - { - super(props); - - // Latest received audio track. - // @type {MediaStreamTrack} - this._audioTrack = null; - } - - render() - { - return ( -