Compare commits
No commits in common. "master" and "1.2" have entirely different histories.
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
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.
|
||||
10
.github/ISSUE_TEMPLATE/custom.md
vendored
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
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.
|
||||
106
.github/workflows/develop-deb.yml
vendored
|
|
@ -1,106 +0,0 @@
|
|||
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 <<EOF
|
||||
Package: edumeet
|
||||
Version: $VERSION
|
||||
Maintainer: eduMEET team (${{ github.server_url }}/${{ github.repository }})
|
||||
Section: admin
|
||||
Date : $DATE
|
||||
Architecture: amd64
|
||||
Priority: optional
|
||||
Description: eduMMET is multiparty web-meetings based on mediasoup and WebRTC
|
||||
Packet created from ${{ github.ref_name }} branch, commit:${{ github.sha }}
|
||||
Visit ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }} for details
|
||||
Depends: nodejs (>= 16), redis
|
||||
EOF
|
||||
#
|
||||
cat > DEBIAN/postinst <<EOF
|
||||
#!/bin/bash
|
||||
mkdir -p /etc/edumeet/
|
||||
ln -s /usr/local/src/edumeet/server/config/config.js /etc/edumeet/server-config.js || true
|
||||
ln -s /usr/local/src/edumeet/server/dist/config/config.yaml /etc/edumeet/server-config.yaml || true
|
||||
ln -s /usr/local/src/edumeet/server/public/config/config.js /etc/edumeet/client-config.js || true
|
||||
systemctl daemon-reload
|
||||
systemctl enable edumeet
|
||||
echo "eduMEET multiparty meeting is installed.\n"
|
||||
echo "\n\nClient and server configuration files are in /etc/edumeet directory\n"
|
||||
echo "Please visit ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }} for configuration details.\n\n"
|
||||
echo "\n\nAfter configuration, start service with 'sudo systemctl start edumeet' command.\n\n"
|
||||
EOF
|
||||
#
|
||||
chmod 775 DEBIAN/postinst
|
||||
cd ../
|
||||
dpkg-deb -Zgzip --build package
|
||||
mv /home/runner/package.deb /home/runner/edumeet-$VERSION.deb
|
||||
|
||||
- name : Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: edumeet-${{ steps.get-version.outputs.VERSION }}
|
||||
path: "/home/runner/edumeet-*.deb"
|
||||
|
||||
- name: Add to release assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.get-version.outputs.VERSION }}
|
||||
files: "/home/runner/edumeet-*.deb"
|
||||
13
.gitignore
vendored
|
|
@ -1,16 +1,9 @@
|
|||
node_modules/
|
||||
|
||||
/app/build/
|
||||
/app/public/config/config.js
|
||||
/app/public/images/logo.*
|
||||
!/app/public/images/logo.edumeet.svg
|
||||
/app/config/
|
||||
!/app/config/config.example.js
|
||||
/server/config/
|
||||
!/server/config/config.example.*
|
||||
!/server/config/README.md
|
||||
!/server/config/config.example.js
|
||||
/server/public/
|
||||
/server/certs/
|
||||
/server/dist/
|
||||
!/server/certs/mediasoup-demo.localhost.*
|
||||
.vscode
|
||||
/app/public/config/*.pem
|
||||
yarn-error.log
|
||||
|
|
|
|||
260
CHANGELOG.md
|
|
@ -1,269 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## 3.5.3
|
||||
Hotfix configuration issue
|
||||
## 3.5.2
|
||||
Security update for dependencies
|
||||
## 3.5.1
|
||||
* Updated Documentation
|
||||
|
||||
## 3.5.0
|
||||
|
||||
### Added
|
||||
|
||||
* Update build steps
|
||||
* Update documentations
|
||||
* Major chat refactoring
|
||||
* Added chat badge displaying unread messages
|
||||
* Merged file sharing + chat
|
||||
* Save chat history (both in leave screen and in chat directly)
|
||||
* Sorting messages
|
||||
* Format messages
|
||||
* Combine messages
|
||||
* Local recording (chrome, firefox, edge)
|
||||
* This is still default off in config because of bad UX connected to getUserMedias
|
||||
* New app configuration system
|
||||
* New server configuration system
|
||||
* Add leaving room confirmation dialog
|
||||
* New audio config with audio profiles
|
||||
* New translations
|
||||
* Screen Sharing now supports audio (chrome, edge)
|
||||
* Add QoS set DSCP bits in IP header according to new option networkPriorities in app config
|
||||
Our media traffic should get now higher priority where QoS has been implemented.
|
||||
Routers will add higher priority or put media packets to low latency queue.
|
||||
If QoS implemented in L2 too, then switches also set the L2 CoS bits according DSCP.
|
||||
WIFI AP-s that implements QoS, also based on it could activate the multimedia extension MMS.
|
||||
Hopefully it will help and add little bit more priority lower latency in congested networks.
|
||||
At the time of this writing it is only implemented in Chrome.
|
||||
|
||||
### Improved
|
||||
* Scalability and performance:
|
||||
* Improvements for handling simulcast and video container size
|
||||
* Improved performance for encryption of media (+25% capacity)
|
||||
* Improved balancing of peers to workers
|
||||
* CSS for mobile
|
||||
* UI improvements (bugs fixes) #922
|
||||
* Improved button layout
|
||||
* Improved login dialog
|
||||
* Added bcrypt for local auth strategy
|
||||
|
||||
|
||||
### Fixed
|
||||
* Fix auto unmute buttons on separate bar
|
||||
* Fix browser version comparison
|
||||
* Safari Screen Share Bug Fix
|
||||
* Fix session overwrites
|
||||
* Fix loosing the current language after leaving the room)
|
||||
* Extra video duplication fix
|
||||
* Fix loosing the current language after leaving the room)
|
||||
* Fix notifications (they were displayed always in English)
|
||||
* Translation updates
|
||||
* A lot of bugs in auth
|
||||
* Sanitizations for room names
|
||||
|
||||
## 3.4.0
|
||||
|
||||
### Added
|
||||
|
||||
* Multiparty meeting renamed to edumeet
|
||||
* Merged room selector- and join- dialog - default keyboard focus on login field
|
||||
* Localization selectable
|
||||
* Added aspect ratio 16 : 9 and this is default now, ref #439
|
||||
* New worker load calculation so router selection is based on that and not random anymore
|
||||
* New permissions and roles:
|
||||
* Propagate userRoles to client state, ref #437
|
||||
* Extend userRoles and use the new audio, video permissions, ref #437
|
||||
* New permission to modify peer roles
|
||||
* Create room actions for giving and taking roles
|
||||
* Add functions to client for modifying roles live
|
||||
* Ability to give roles to users
|
||||
* Add new permission to config
|
||||
* Add room to userMapping. Example of giving moderator if there is no authenticated user
|
||||
* Promote all peers from lobby when a peer joins with the PROMOTE_PEER permission and activateOnHostJoin is true in config
|
||||
* Make menus more intuitive on mobile
|
||||
* Simplify electron screenshare check
|
||||
* Logo support
|
||||
* Improve autoMute mic-indicators, Improve audio level scaling
|
||||
* Clean up participant list
|
||||
* Add indicator for peers in focus. Ref #360
|
||||
* TCP enabled by default, prefer UDP
|
||||
* Ability for Prometheus exporter to listen on localhost
|
||||
* Make list headers bolder
|
||||
* Trim displayName inputs. Add random number to Guest displayName.
|
||||
* Removed facingMode from mobile
|
||||
* Documentation for prometheus exporter
|
||||
* Added switching of own video mirror in settings
|
||||
* Added hiding of own videos
|
||||
* Request audio and webcam permission at once, when user is requesting media
|
||||
* Add initial support for local and saml auth
|
||||
* Local login form
|
||||
* Add bcrypt encrypted passwords for local strategy
|
||||
* Add displayName mapping to usermapping
|
||||
* Add saml attriute mappings
|
||||
* Use shared cookieparser for web and websocket
|
||||
* Update TopBar leave button
|
||||
* Add joinAudio capability
|
||||
* Standardize Auth button
|
||||
|
||||
### Migration
|
||||
|
||||
from last master version:
|
||||
* Copy paste defaultAudio and centralAudioOtions from config.example.js to config.js
|
||||
* Copy paste whole theme from config.example.js to config
|
||||
* Configure logo in config.js
|
||||
|
||||
### Upgrade depencies
|
||||
|
||||
* webtorrent from 0.107.17 to 0.108.1 …
|
||||
* Upgrade React-scripts
|
||||
|
||||
### Languages
|
||||
|
||||
* Updates translations: hu, tr, no, pl, uk
|
||||
* Addad translations: Hindi (hi), Russian (ru), Kazakh (ka)
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Hopefully fix to silent peer issue, ref #256
|
||||
* Set timeout for TURN API request, fixes #484
|
||||
* Possible fix for #582 Crackling sound
|
||||
* Fix for #444 Settings persistence
|
||||
* Fix Audio settings from config.js take no effect
|
||||
* Fix only firefox handles applyConstraints to audio tracks correctly
|
||||
* Fix (autoMuted) mic too big click area
|
||||
* Fix userRoles check
|
||||
* Fix spoltights ignoring maxLastN
|
||||
* Fix wrong config parameter naming (voiceActivatedUnmute)
|
||||
* Fix locale checking state
|
||||
* Fix express error handler
|
||||
* Add constraint for user facing camera. Update media devices properly
|
||||
* Cleanup on close
|
||||
* Fixed null values for peers not yet assigned to a router
|
||||
* Fix URL sanitizer bug
|
||||
* Removed audio request modification
|
||||
* Fix race in spotlights
|
||||
* Fix piping bug when peers returning to router
|
||||
* Fix piped router count
|
||||
* Add comma to prometheus config code
|
||||
* Removed code that is problematic and also unused
|
||||
* Remove duplicated callbackURL
|
||||
* Fix moderator buttons layout
|
||||
* Fix: Special chars are not sanitized in URL
|
||||
* Fix another roomId bug
|
||||
* Tidy: replace obj.entries with obj.values to avoid unused key
|
||||
* Fix close room link
|
||||
|
||||
## 3.3
|
||||
|
||||
* Add: Rooms now scale across cores
|
||||
* Add: Permissions and roles. Users can now have different roles (moderator, admin etc.) that give different permissions.
|
||||
* Add: TURN API or fallback TURN server
|
||||
* Add: Configurable room size limit
|
||||
* Add: Prometheus monitoring support
|
||||
* Add: Possible to share several videos (ex: 2 webcams)
|
||||
* Add: Configurable audio settings (echocancellation etc.)
|
||||
* Add: Configurable audio output device (in supported browsers)
|
||||
* Add: Audio auto mute/unmute based on volume
|
||||
* Add: Handle unsupported browsers properly
|
||||
* Add: Lots of appearance settings
|
||||
* Add: Side drawer can now stay permanently open
|
||||
* Add: Move control buttons to separate control bar
|
||||
* Add: Can now "raise hand"
|
||||
* Add: Screen sharing in Safari 13+, Opera and Edge
|
||||
* Add: Extended advanced info about network in client
|
||||
* Add: Configurable screen sharing frame rate
|
||||
* Add: Help and About dialogs
|
||||
* Add: More keyboard shortcuts
|
||||
* Add: Quality indicator on videos
|
||||
* Add: More translations
|
||||
* Fix: Various UI fixes and improvements
|
||||
* Fix: Better audio/video device handling
|
||||
* Fix: Update keyboard shortcut handling
|
||||
* Fix: Authentication for load balanced scenarios
|
||||
* Fix: Signaling when entering lobby
|
||||
* Fix: Signaling timeouts and retries
|
||||
* Fix: Filesharing fixes (sharing same file twice, etc.)
|
||||
* Fix: Better handling of hark
|
||||
* Fix: Use applyContraints instead of restarting producers
|
||||
* Fix: Now handles reconnects properly if client loses connection
|
||||
* Fix: Rotating devices don't show rotated videos
|
||||
* Fix: Various fixes to client authentication
|
||||
|
||||
## 3.2.1
|
||||
|
||||
* Fix: permananent top bar by default
|
||||
* Fix: `httpOnly` mode https redirect
|
||||
* Add some extra checks for video stream and track
|
||||
* Add Italian translation
|
||||
* Add Czech translation
|
||||
* Add new server option `trustProxy` for load balancing http only use case
|
||||
* Add HAproxy load balance example
|
||||
* Add LTI LMS integration documentation
|
||||
* Fix spacing of leave button
|
||||
* Fix for sharing same file multiple times
|
||||
|
||||
## 3.2
|
||||
|
||||
* Add munin plugin
|
||||
* Add `muted=true` search param to disable audio by default
|
||||
* Modify webtorrent tracker
|
||||
* Add key shortcut `space` for audio mute
|
||||
* Add key shortcut `v` for video mute
|
||||
* Add user configurable LastN
|
||||
* Add option to permananent top bar (permanent by default)
|
||||
* Update mediasoup server
|
||||
* Add `simulcast` options to app config (disabled by default)
|
||||
* Add `stats` option to get counts of rooms and peers
|
||||
* Add `httpOnly` option for loadbalancer backend setups
|
||||
* LTI integration for LMS systems like moodle
|
||||
* Add translations (12+1 languages)
|
||||
* Add support IPv6
|
||||
* Many other fixes and refactorings
|
||||
|
||||
## 3.1
|
||||
|
||||
* Browser session storage
|
||||
* Virtual lobby for rooms
|
||||
* Allow minimum TLSv1.2 and recommended ciphers
|
||||
* Code splitting for faster load times
|
||||
* Various GUI fixes
|
||||
* Internationalization support
|
||||
* Can require sign in for access
|
||||
|
||||
## 3.0
|
||||
|
||||
* Updated to mediasoup v3
|
||||
* Replace lib "passport-datporten" with "openid-client" (a general OIDC certified client)
|
||||
* OpenID Connect discovery
|
||||
* Auth code flow
|
||||
* Add spdy http2 support.
|
||||
* Notice it does not supports node 11.x
|
||||
* Updated to Material UI v4
|
||||
|
||||
## 2.0
|
||||
|
||||
* Material UI
|
||||
* Separate settings for lastN for desktop and mobile
|
||||
|
||||
## 1.2
|
||||
|
||||
* Add Lock Room feature
|
||||
* Fix suspended Web Audio context / fixed delayed getUsermedia
|
||||
* Added support for the new getdisplaymedia API in Chrome 72
|
||||
|
||||
## 1.1
|
||||
|
||||
### 1.1
|
||||
* Moved Filesharing code out from React code to RoomClient
|
||||
* Major cleanup of CSS. Variables for most colors and sizes exposed in :root
|
||||
* Started using React Context instead of middleware
|
||||
* Small fixes to buttons and layout
|
||||
|
||||
## 1.0
|
||||
|
||||
### 1.0
|
||||
* Fixed toolarea button based on feedback from users
|
||||
* Added possibility to move video to separate window
|
||||
* Added SIP gateway
|
||||
|
||||
## RC1 1.0
|
||||
|
||||
### RC1 1.0
|
||||
* First stable release?
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
Source code contributions should pass static code analysis as performed by `npm run lint` in `server` and `app` respectively.
|
||||
|
||||
Please contribute by creating your pull requests against the `develop` branch.
|
||||
21
LICENSE.md
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 GÉANT Association
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
61
LTI/LTI.md
|
|
@ -1,61 +0,0 @@
|
|||
# Learning Tools Interoperability (LTI)
|
||||
|
||||
## LTI
|
||||
|
||||
Read more about IMS Global defined interface for tools like our VideoConference system integration with Learning Management Systems(LMS) (e.g. moodle).
|
||||
See: [IMS Global Learning Tool Interoperability](https://www.imsglobal.org/activity/learning-tools-interoperability)
|
||||
|
||||
We implemented LTI interface version 1.0/1.1
|
||||
|
||||
### Server config auth section LTI settings
|
||||
|
||||
Set in server configuration a random key and secret
|
||||
|
||||
``` json
|
||||
auth :
|
||||
{
|
||||
lti :
|
||||
{
|
||||
consumerKey : 'key',
|
||||
consumerSecret : 'secret'
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Configure your LMS system with secret and key settings above
|
||||
|
||||
#### Auth tool URL
|
||||
|
||||
Set tool URL to your server with path /auth/lti
|
||||
|
||||
``` url
|
||||
https://mm.example.com/auth/lti
|
||||
```
|
||||
|
||||
#### In moodle find external tool plugin setting and external tool action
|
||||
|
||||
See: [moodle external tool settings](https://docs.moodle.org/38/en/External_tool_settings)
|
||||
|
||||
#### Add and activity
|
||||
|
||||

|
||||
|
||||
#### Setup Activity
|
||||
|
||||
##### Activity setup basic form
|
||||
|
||||
Open fully the settings **Click on show more!!**
|
||||

|
||||
|
||||
##### Empty full form
|
||||
|
||||

|
||||
|
||||
##### Filled out form
|
||||
|
||||

|
||||
|
||||
## moodle plugin
|
||||
|
||||
Alternatively you can use edumeet moodle plugin:
|
||||
[https://github.com/edumeet/moodle-mod_edumeet](https://github.com/edumeet/moodle-mod_edumeet)
|
||||
BIN
LTI/lti1.png
|
Before Width: | Height: | Size: 86 KiB |
BIN
LTI/lti2.png
|
Before Width: | Height: | Size: 58 KiB |
BIN
LTI/lti3.png
|
Before Width: | Height: | Size: 115 KiB |
BIN
LTI/lti4.png
|
Before Width: | Height: | Size: 133 KiB |
317
README.md
|
|
@ -1,291 +1,118 @@
|
|||
#  **WebRTC meeting service using [mediasoup](https://mediasoup.org).**
|
||||
Official website: [edumeet.org](https://edumeet.org)
|
||||
# multiparty-meeting
|
||||
|
||||
https://user-images.githubusercontent.com/37835902/152279867-639db9bc-bf78-430f-b96f-d17733527474.mp4
|
||||
A WebRTC meeting service using [mediasoup](https://mediasoup.org).
|
||||
|
||||
Try it online at [letsmeet.no](https://letsmeet.no)
|
||||
Try it online at https://letsmeet.no. You can add /roomname to the URL for specifying a room.
|
||||
|
||||
## Main features
|
||||
## Features
|
||||
* Audio/Video
|
||||
* Chat
|
||||
* Screen sharing
|
||||
* File sharing
|
||||
* Different video layouts
|
||||
|
||||
| Feature | Description |
|
||||
| ------------- | ------------- |
|
||||
| **A/V streaming** | Share your microphone and camera + additional video stream |
|
||||
| **Video layouts** | Choose between **Democratic** and **Filmstrip** views. More in progress. |
|
||||
| **Screen sharing** | Share your screen to make some presentation right from your desktop |
|
||||
| **File sharing** | Share your files with the peers (torrent solution under the hood) |
|
||||
| **Chat messages** | Text conversation with other participants |
|
||||
| **Local Recording** | Record window/tab/screen content in browser supported formats with room audio and save them (**disabled by default**) |
|
||||
| **Authentication** | Supported types: **OIDC**, **SAML**, **local db (text-based)** |
|
||||
There is also a SIP gateway that can be found [here](https://github.com/havfo/multiparty-meeting-sipgw). To try it, call: roomname@letsmeet.no.
|
||||
|
||||
## Docker
|
||||
If you want the automatic approach, you can find a docker image [here](https://hub.docker.com/r/misi/mm/).
|
||||
|
||||
### Internationalization (22 languages)
|
||||
<details>
|
||||
<summary>Help us with translations:exclamation:</summary>
|
||||
|
||||
#### 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!
|
||||
</details>
|
||||
## Manual installation
|
||||
|
||||
|
||||
### Local Recording
|
||||
<details>
|
||||
<summary>See more</summary>
|
||||
|
||||
* 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:
|
||||
* <https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria#storage_limits>
|
||||
* <https://chromium.googlesource.com/chromium/src/+/refs/heads/master/storage/browser/quota/quota_settings.cc#68>
|
||||
|
||||
</details>
|
||||
|
||||
# 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).
|
||||
* Clone the project:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
$ git clone https://github.com/havfo/multiparty-meeting.git
|
||||
$ cd multiparty-meeting
|
||||
```
|
||||
|
||||
## 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
|
||||
# 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
|
||||
```
|
||||
|
||||
**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 server/config/config.example.js server/config/config.js
|
||||
```
|
||||
|
||||
**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).
|
||||
|
||||
[**: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
|
||||
```
|
||||
[**: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"
|
||||
|
||||
[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
|
||||
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
|
||||
```
|
||||
|
||||
**: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.
|
||||
* Copy `server/config.example.js` to `server/config.js` :
|
||||
|
||||
```bash
|
||||
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
|
||||
$ cp server/config.example.js server/config.js
|
||||
```
|
||||
|
||||
**Run locally** (for development)
|
||||
|
||||
* The newest build is always in **develop branch** if you want to make a contribution/pull request use it instead of master branch.
|
||||
* Copy `app/config.example.js` to `app/config.js` :
|
||||
|
||||
```bash
|
||||
# run a live build from app folder:
|
||||
app$ yarn start
|
||||
|
||||
# and run server in server folder:
|
||||
server$ yarn start
|
||||
$ cp app/config.example.js app/config.js
|
||||
```
|
||||
|
||||
Note: To avoid running server as root, redirects privileged ports with firewall rules:
|
||||
* Edit your two `config.js` with appropriate settings (listening IP/port, logging options, **valid** TLS certificate, etc).
|
||||
|
||||
* Set up the browser app:
|
||||
|
||||
```bash
|
||||
#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
|
||||
$ cd app
|
||||
$ npm install
|
||||
$ export NODE_ENV=production
|
||||
$ gulp dist
|
||||
```
|
||||
* Test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname`
|
||||
This will build the client application and copy everythink to `server/public` from where the server can host client code to browser requests.
|
||||
|
||||
**Run as a service** (systemd)
|
||||
* Globally install `gulp-cli` NPM module (may need `sudo`):
|
||||
|
||||
```bash
|
||||
# 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/
|
||||
$ npm install -g gulp-cli
|
||||
```
|
||||
|
||||
# modify the install paths, if required
|
||||
sudo edit /etc/systemd/system/edumeet.service
|
||||
* Set up the server:
|
||||
|
||||
# Reload systemd configuration and start service:
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start edumeet
|
||||
```bash
|
||||
$ cd ..
|
||||
$ cd server
|
||||
$ npm install
|
||||
```
|
||||
|
||||
# If you want to start edumeet at boot time:
|
||||
sudo systemctl enable edumeet
|
||||
## Run it locally
|
||||
|
||||
* Run the Node.js server application in a terminal:
|
||||
|
||||
```bash
|
||||
$ node server.js
|
||||
```
|
||||
* test your service in a webRTC enabled browser: `https://yourDomainOrIPAdress:3443/roomname`
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
* reload systemd configuration and start service:
|
||||
|
||||
```bash
|
||||
$ systemctl daemon-reload
|
||||
$ systemctl start multiparty-meeting
|
||||
```
|
||||
|
||||
* if you want to start multiparty-meeting at boot time:
|
||||
```bash
|
||||
$ systemctl enable multiparty-meeting
|
||||
```
|
||||
|
||||
## 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` |
|
||||
|
||||
## 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).
|
||||
* 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`)
|
||||
|
||||
## TURN configuration
|
||||
|
||||
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/ |
|
||||
* 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`
|
||||
|
||||
## 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/)]
|
||||
|
||||
|
||||
## License
|
||||
|
||||
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.
|
||||
|
||||
MIT
|
||||
|
|
|
|||
26
app/.babelrc
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
2
app/.env
|
|
@ -1,2 +0,0 @@
|
|||
REACT_APP_VERSION=$npm_package_version
|
||||
REACT_APP_NAME=$npm_package_name
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
.eslintrc.js
|
||||
src/react-app-env.d.ts
|
||||
231
app/.eslintrc.js
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
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
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
|
@ -1,386 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1
app/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
react: npm start
|
||||
electron: node src/electron-wait-react
|
||||
7
app/banner.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* <%= pkg.name %> v<%= pkg.version %>
|
||||
* <%= pkg.description %>
|
||||
* Copyright: 2017-<%= currentYear %> <%= pkg.author %>
|
||||
* License: <%= pkg.license %>
|
||||
*/
|
||||
|
||||
74
app/chooseRoom.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Multiparty Meeting</title>
|
||||
</head>
|
||||
<style>
|
||||
body{
|
||||
margin:auto;
|
||||
padding:0.5vmin;
|
||||
text-align:center;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 40%;
|
||||
width: 90%;
|
||||
transform: translate(-50%, 0%);
|
||||
background-image: url('/resources/images/background.svg');
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: repeat;
|
||||
}
|
||||
input:hover {opacity:0.9;}
|
||||
input[type=text]{
|
||||
font-size: 1.5em;
|
||||
padding: 1.5vmin;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
border: 0;
|
||||
color: #fff;
|
||||
margin: 0.8vmin;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
button:hover {background-color: #28bd7b;}
|
||||
button{
|
||||
font-size: 1.5em;
|
||||
padding: 1.5vmin;
|
||||
margin: 0.8vmin;
|
||||
background-color: #38cd8b;
|
||||
border-radius: 1.8vmin;
|
||||
color: #fff;
|
||||
border: 0;
|
||||
}
|
||||
img{
|
||||
height: 15vmin;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a>
|
||||
<img src='/resources/images/logo.svg'></img><br>
|
||||
</a>
|
||||
<input id="room" type="text" onkeypress="checkEnter(event)" value="" placeholder="your room name">
|
||||
<button onclick = "start(location.href)">Go to room</button>
|
||||
</body>
|
||||
<script>
|
||||
var room = document.getElementById("room");
|
||||
var stateObj = { foo: "bar" };
|
||||
room.addEventListener("input", function(e) {
|
||||
console.log(e.charCode);
|
||||
history.replaceState(stateObj, "Multiparty Meeting", "/"+room.value);
|
||||
},true);
|
||||
room.focus();
|
||||
function start(target){
|
||||
location.href;history.replaceState(stateObj, "Multiparty Meeting", "/");
|
||||
window.location = target;
|
||||
}
|
||||
function checkEnter(event){
|
||||
var x = event.charCode || event.keyCode;
|
||||
if (x == 13 ) {
|
||||
start(location.href);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
19
app/config/config.example.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
}
|
||||
};
|
||||
299
app/gulpfile.js
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
/**
|
||||
* 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'));
|
||||
30
app/index.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Multiparty Meeting</title>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'>
|
||||
<meta name='description' content='multiparty meeting - Cutting Edge WebRTC Video Conferencing'>
|
||||
|
||||
<link rel='stylesheet' href='/multiparty-meeting.css'>
|
||||
<link rel="chrome-webstore-item" href="chromeExtension">
|
||||
|
||||
<script src='/resources/js/antiglobal.js'></script>
|
||||
<script>
|
||||
window.localStorage.setItem('debug', '* -engine* -socket* -RIE* *WARN* *ERROR*');
|
||||
|
||||
if (window.antiglobal)
|
||||
{
|
||||
window.antiglobal('__multipartyMeetingScreenShareExtensionAvailable__', '___browserSync___oldSocketIo', 'io', '___browserSync___', '__core-js_shared__');
|
||||
setInterval(window.antiglobal, 180000);
|
||||
}
|
||||
</script>
|
||||
<script async src='/multiparty-meeting.js'></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='multiparty-meeting'></div>
|
||||
<div id='multiparty-meeting-media-query-detector'></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import debug from 'debug';
|
||||
|
||||
const APP_NAME = 'edumeet';
|
||||
const APP_NAME = 'multiparty-meeting';
|
||||
|
||||
export default class Logger
|
||||
{
|
||||
1990
app/lib/RoomClient.js
Normal file
|
|
@ -1,59 +1,44 @@
|
|||
import isElectron from 'is-electron';
|
||||
|
||||
let electron = null;
|
||||
|
||||
/**
|
||||
* Check if window.require function exits
|
||||
* because electron default is "nodeIntegration: false"
|
||||
* and this case window.require is not a function.
|
||||
* It caused issue with Rocket Chat electron client.
|
||||
*
|
||||
* TODO: do it more inteligently.
|
||||
*/
|
||||
if (isElectron() && typeof window.require === 'function')
|
||||
electron = window.require('electron');
|
||||
|
||||
class ElectronScreenShare
|
||||
class ChromeScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start()
|
||||
start(options = { })
|
||||
{
|
||||
return Promise.resolve()
|
||||
.then(() =>
|
||||
const state = this;
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
window.addEventListener('message', _onExtensionMessage, false);
|
||||
window.postMessage({ type: 'getStreamId' }, '*');
|
||||
|
||||
function _onExtensionMessage({ data })
|
||||
{
|
||||
return electron.desktopCapturer.getSources({ types: [ 'window', 'screen' ] });
|
||||
})
|
||||
.then((sources) =>
|
||||
{
|
||||
for (const source of sources)
|
||||
if (data.type !== 'gotStreamId')
|
||||
{
|
||||
// Currently only getting whole screen
|
||||
if (source.name === 'Entire Screen')
|
||||
{
|
||||
return navigator.mediaDevices.getUserMedia({
|
||||
audio : true,
|
||||
video :
|
||||
{
|
||||
mandatory :
|
||||
{
|
||||
chromeMediaSource : 'desktop',
|
||||
chromeMediaSourceId : source.id
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
})
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return stream;
|
||||
});
|
||||
const constraints = state._toConstraints(options, data.streamId);
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
window.removeEventListener('message', _onExtensionMessage);
|
||||
|
||||
state._stream = stream;
|
||||
resolve(stream);
|
||||
})
|
||||
.catch((err) =>
|
||||
{
|
||||
window.removeEventListener('message', _onExtensionMessage);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
|
|
@ -69,76 +54,60 @@ class ElectronScreenShare
|
|||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class DisplayMediaScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.mediaDevices.getDisplayMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
|
||||
{
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
isAudioEnabled()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
if ('__multipartyMeetingScreenShareExtensionAvailable__' in window)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints(options)
|
||||
return true;
|
||||
}
|
||||
|
||||
_toConstraints(options, streamId)
|
||||
{
|
||||
const constraints = {
|
||||
video : {},
|
||||
audio : true
|
||||
video : {
|
||||
mandatory : {
|
||||
chromeMediaSource : 'desktop',
|
||||
chromeMediaSourceId : streamId
|
||||
},
|
||||
optional : [ {
|
||||
googTemporalLayeredScreencast : true
|
||||
} ]
|
||||
},
|
||||
audio : false
|
||||
};
|
||||
|
||||
if (isFinite(options.width))
|
||||
{
|
||||
constraints.video.width = options.width;
|
||||
constraints.video.mandatory.maxWidth = options.width;
|
||||
constraints.video.mandatory.minWidth = options.width;
|
||||
}
|
||||
if (isFinite(options.height))
|
||||
{
|
||||
constraints.video.height = options.height;
|
||||
constraints.video.mandatory.maxHeight = options.height;
|
||||
constraints.video.mandatory.minHeight = options.height;
|
||||
}
|
||||
if (isFinite(options.frameRate))
|
||||
{
|
||||
constraints.video.frameRate = options.frameRate;
|
||||
constraints.video.mandatory.maxFrameRate = options.frameRate;
|
||||
constraints.video.mandatory.minFrameRate = options.frameRate;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class DisplayMediaScreenShareWithAudio
|
||||
class Chrome72ScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
|
|
@ -174,39 +143,17 @@ class DisplayMediaScreenShareWithAudio
|
|||
return true;
|
||||
}
|
||||
|
||||
isAudioEnabled()
|
||||
needExtension()
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints(options)
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
video : {},
|
||||
audio : {
|
||||
sampleRate : options.sampleRate,
|
||||
channelCount : options.channelCount,
|
||||
volume : options.volume,
|
||||
autoGainControl : options.autoGainControl,
|
||||
echoCancellation : options.echoCancellation,
|
||||
noiseSuppression : options.noiseSuppression,
|
||||
sampleSize : options.sampleSize
|
||||
}
|
||||
video : true
|
||||
};
|
||||
|
||||
if (isFinite(options.width))
|
||||
{
|
||||
constraints.video.width = options.width;
|
||||
}
|
||||
if (isFinite(options.height))
|
||||
{
|
||||
constraints.video.height = options.height;
|
||||
}
|
||||
if (isFinite(options.frameRate))
|
||||
{
|
||||
constraints.video.frameRate = options.frameRate;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
|
@ -247,7 +194,7 @@ class FirefoxScreenShare
|
|||
return true;
|
||||
}
|
||||
|
||||
isAudioEnabled()
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -291,6 +238,57 @@ class FirefoxScreenShare
|
|||
}
|
||||
}
|
||||
|
||||
class EdgeScreenShare
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
start(options = {})
|
||||
{
|
||||
const constraints = this._toConstraints(options);
|
||||
|
||||
return navigator.getDisplayMedia(constraints)
|
||||
.then((stream) =>
|
||||
{
|
||||
this._stream = stream;
|
||||
|
||||
return Promise.resolve(stream);
|
||||
});
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._stream instanceof MediaStream === false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._stream.getTracks().forEach((track) => track.stop());
|
||||
this._stream = null;
|
||||
}
|
||||
|
||||
isScreenShareAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_toConstraints()
|
||||
{
|
||||
const constraints = {
|
||||
video : true
|
||||
};
|
||||
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultScreenShare
|
||||
{
|
||||
isScreenShareAvailable()
|
||||
|
|
@ -298,7 +296,7 @@ class DefaultScreenShare
|
|||
return false;
|
||||
}
|
||||
|
||||
isAudioEnabled()
|
||||
needExtension()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -308,39 +306,26 @@ export default class ScreenShare
|
|||
{
|
||||
static create(device)
|
||||
{
|
||||
if (electron)
|
||||
return new ElectronScreenShare();
|
||||
else if (device.platform !== 'desktop')
|
||||
return new DefaultScreenShare();
|
||||
else
|
||||
switch (device.flag)
|
||||
{
|
||||
switch (device.flag)
|
||||
case 'firefox':
|
||||
{
|
||||
case 'firefox':
|
||||
{
|
||||
if (device.bowser.satisfies({ firefox: '<66' }))
|
||||
return new FirefoxScreenShare();
|
||||
else
|
||||
return new DisplayMediaScreenShare();
|
||||
}
|
||||
case 'safari':
|
||||
{
|
||||
if (device.bowser.satisfies({ safari: '>=13' }))
|
||||
return new DisplayMediaScreenShare();
|
||||
else
|
||||
return new DefaultScreenShare();
|
||||
}
|
||||
case 'opera':
|
||||
case 'chrome':
|
||||
case 'chromium':
|
||||
case 'edge':
|
||||
{
|
||||
return new DisplayMediaScreenShareWithAudio();
|
||||
}
|
||||
default:
|
||||
{
|
||||
return new DefaultScreenShare();
|
||||
}
|
||||
return new FirefoxScreenShare();
|
||||
}
|
||||
case 'chrome':
|
||||
{
|
||||
if (device.version < 72.0)
|
||||
return new ChromeScreenShare();
|
||||
else
|
||||
return new Chrome72ScreenShare();
|
||||
}
|
||||
case 'msedge':
|
||||
{
|
||||
return new EdgeScreenShare();
|
||||
}
|
||||
default:
|
||||
{
|
||||
return new DefaultScreenShare();
|
||||
}
|
||||
}
|
||||
}
|
||||
184
app/lib/Spotlights.js
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
78
app/lib/components/Containers/HiddenPeers.jsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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 (
|
||||
<div data-component='HiddenPeers'>
|
||||
<div className={classnames('view-container', this.state.className)} onClick={() => openUsersTab()}>
|
||||
<p>+{hiddenPeersCount} <br /> participant
|
||||
{(hiddenPeersCount === 1) ? null : 's'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
240
app/lib/components/Containers/Me.jsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
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 (
|
||||
<div
|
||||
data-component='Me'
|
||||
ref={(node) => (this._rootNode = node)}
|
||||
data-tip={tip}
|
||||
data-tip-disable={!tip}
|
||||
data-type='dark'
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
<div className={classnames('view-container', 'webcam')}>
|
||||
<If condition={connected}>
|
||||
<div className={classnames('controls', 'visible')}>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘m‘'
|
||||
data-type='dark'
|
||||
data-place='bottom'
|
||||
data-for='me'
|
||||
className={classnames('button', 'mic', micState, {
|
||||
disabled : me.audioInProgress,
|
||||
visible : micState == 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
micState === 'on' ?
|
||||
roomClient.muteMic() :
|
||||
roomClient.unmuteMic();
|
||||
}}
|
||||
/>
|
||||
<ReactTooltip
|
||||
id='me'
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
className={classnames('button', 'webcam', webcamState, {
|
||||
disabled : me.webcamInProgress,
|
||||
visible : webcamState == 'off' || this.state.controlsVisible
|
||||
})}
|
||||
onClick={() =>
|
||||
{
|
||||
webcamState === 'on' ?
|
||||
roomClient.disableWebcam() :
|
||||
roomClient.enableWebcam();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<PeerView
|
||||
isMe
|
||||
advancedMode={advancedMode}
|
||||
peer={me}
|
||||
audioTrack={micProducer ? micProducer.track : null}
|
||||
volume={micProducer ? micProducer.volume : null}
|
||||
videoTrack={webcamProducer ? webcamProducer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
audioCodec={micProducer ? micProducer.codec : null}
|
||||
videoCodec={webcamProducer ? webcamProducer.codec : null}
|
||||
onChangeDisplayName={(displayName) =>
|
||||
{
|
||||
roomClient.changeDisplayName(displayName);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<If condition={screenProducer}>
|
||||
<div className={classnames('view-container', 'screen')}>
|
||||
<ScreenView
|
||||
isMe
|
||||
advancedMode={advancedMode}
|
||||
screenTrack={screenProducer ? screenProducer.track : null}
|
||||
screenVisible={screenVisible}
|
||||
screenCodec={screenProducer ? screenProducer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this._mounted = true;
|
||||
|
||||
if (this._tooltip)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (!this._mounted || this.props.me.displayNameSet)
|
||||
return;
|
||||
|
||||
ReactTooltip.show(this._rootNode);
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
if (this._tooltip)
|
||||
{
|
||||
if (nextProps.me.displayNameSet)
|
||||
ReactTooltip.hide(this._rootNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Me.propTypes =
|
||||
{
|
||||
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;
|
||||
266
app/lib/components/Containers/Peer.jsx
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
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 (
|
||||
<div
|
||||
data-component='Peer'
|
||||
className={classnames({
|
||||
screen : screenConsumer
|
||||
})}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
>
|
||||
<If condition={videoVisible && !webcamConsumer.supported}>
|
||||
<div className='incompatible-video'>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={!videoVisible}>
|
||||
<div className='paused-video'>
|
||||
<p>this video is paused</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div className={classnames('view-container', 'webcam')} style={style}>
|
||||
<div className='indicators'>
|
||||
<If condition={peer.raiseHandState}>
|
||||
<div className={
|
||||
classnames(
|
||||
'icon', 'raise-hand', {
|
||||
on : peer.raiseHandState,
|
||||
off : !peer.raiseHandState
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('controls', {
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('button', 'mic', {
|
||||
on : micEnabled,
|
||||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'newwindow', {
|
||||
disabled : !videoVisible ||
|
||||
(windowConsumer === webcamConsumer.id)
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerWindow(webcamConsumer);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', {
|
||||
disabled : !videoVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(webcamConsumer);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PeerView
|
||||
advancedMode={advancedMode}
|
||||
peer={peer}
|
||||
volume={micConsumer ? micConsumer.volume : null}
|
||||
videoTrack={webcamConsumer ? webcamConsumer.track : null}
|
||||
videoVisible={videoVisible}
|
||||
videoProfile={videoProfile}
|
||||
audioCodec={micConsumer ? micConsumer.codec : null}
|
||||
videoCodec={webcamConsumer ? webcamConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<If condition={screenConsumer}>
|
||||
<div className={classnames('view-container', 'screen')} style={style}>
|
||||
<div
|
||||
className={classnames('controls', {
|
||||
visible : this.state.controlsVisible
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('button', 'newwindow', {
|
||||
disabled : !screenVisible ||
|
||||
(windowConsumer === screenConsumer.id)
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerWindow(screenConsumer);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', {
|
||||
disabled : !screenVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(screenConsumer);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ScreenView
|
||||
advancedMode={advancedMode}
|
||||
screenTrack={screenConsumer ? screenConsumer.track : null}
|
||||
screenVisible={screenVisible}
|
||||
screenProfile={screenProfile}
|
||||
screenCodec={screenConsumer ? screenConsumer.codec : null}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
232
app/lib/components/Controls/Sidebar.jsx
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
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 (
|
||||
<div
|
||||
className={classnames('sidebar room-controls', {
|
||||
'visible' : toolbarsVisible
|
||||
})}
|
||||
data-component='Sidebar'
|
||||
>
|
||||
<If condition={this.fullscreen.fullscreenEnabled}>
|
||||
<div
|
||||
className={classnames('button', 'fullscreen', {
|
||||
on : this.state.fullscreen
|
||||
})}
|
||||
onClick={this.handleToggleFullscreen}
|
||||
data-tip='Fullscreen'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
/>
|
||||
</If>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'screen', screenState)}
|
||||
data-tip={screenTip}
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() =>
|
||||
{
|
||||
switch (screenState)
|
||||
{
|
||||
case 'on':
|
||||
{
|
||||
roomClient.disableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'off':
|
||||
{
|
||||
roomClient.enableScreenSharing();
|
||||
break;
|
||||
}
|
||||
case 'need-extension':
|
||||
{
|
||||
roomClient.installExtension();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<If condition={me.loginEnabled}>
|
||||
<Choose>
|
||||
<When condition={me.loggedIn}>
|
||||
<div
|
||||
className='button logout'
|
||||
data-tip='Logout'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.logout()}
|
||||
>
|
||||
<img src={me.picture || 'resources/images/avatar-empty.jpeg'} />
|
||||
</div>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<div
|
||||
className='button login off'
|
||||
data-tip='Login'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.login()}
|
||||
/>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</If>
|
||||
<div
|
||||
className={classnames('button', 'lock', lockState, {
|
||||
on : locked
|
||||
})}
|
||||
data-tip={`Room is ${lockState}`}
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() =>
|
||||
{
|
||||
if (locked)
|
||||
{
|
||||
roomClient.unlockRoom();
|
||||
}
|
||||
else
|
||||
{
|
||||
roomClient.lockRoom();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={classnames('button', 'raise-hand', {
|
||||
on : me.raiseHand,
|
||||
disabled : me.raiseHandInProgress
|
||||
})}
|
||||
data-tip='Raise hand'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.sendRaiseHandState(!me.raiseHand)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'leave-meeting')}
|
||||
data-tip='Leave meeting'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
onClick={() => roomClient.close()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
|
|
@ -55,7 +55,6 @@ export default class FullScreen
|
|||
|
||||
requestFullscreenFunction(element)
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
element[this.vendor[key.requestFullscreen]];
|
||||
}
|
||||
|
||||
216
app/lib/components/Layouts/Filmstrip.jsx
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
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 (
|
||||
<div data-component='Filmstrip'>
|
||||
<div className='active-peer-container' ref={this.activePeerContainer}>
|
||||
<If condition={peers[activePeerName]}>
|
||||
<div
|
||||
className='active-peer'
|
||||
style={{
|
||||
width : this.state.width,
|
||||
height : this.state.width / this.getRatio()
|
||||
}}
|
||||
>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={activePeerName}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div className='filmstrip'>
|
||||
<div className='filmstrip-content'>
|
||||
{ Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
|
||||
{
|
||||
return (
|
||||
<div
|
||||
key={peerName}
|
||||
onClick={() => roomClient.setSelectedPeer(peerName)}
|
||||
className={classnames('film', {
|
||||
selected : this.props.selectedPeerName === peerName,
|
||||
active : this.state.lastSpeaker === peerName
|
||||
})}
|
||||
>
|
||||
<div className='film-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peerName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden-peer-container'>
|
||||
<If condition={(spotlightsLength<Object.keys(peers).length)}>
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length-spotlightsLength}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
175
app/lib/components/Layouts/Peers.jsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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 (
|
||||
<div data-component='Peers' ref={this.peersRef}>
|
||||
{ Object.keys(peers).map((peerName) =>
|
||||
{
|
||||
if (spotlights.find((spotlightsElement) => spotlightsElement === peerName))
|
||||
{
|
||||
return (
|
||||
<Appear key={peerName} duration={1000}>
|
||||
<div
|
||||
className={classnames('peer-container', {
|
||||
'selected' : this.props.selectedPeerName === peerName,
|
||||
'active-speaker' : peerName === activeSpeakerName
|
||||
})}
|
||||
>
|
||||
<div className='peer-content'>
|
||||
<Peer
|
||||
advancedMode={advancedMode}
|
||||
name={peerName}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<div className='hidden-peer-container'>
|
||||
<If condition={spotlightsLength < Object.keys(peers).length}>
|
||||
<HiddenPeers
|
||||
hiddenPeersCount={Object.keys(peers).length - spotlightsLength}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
70
app/lib/components/Notifications.jsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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 (
|
||||
<div
|
||||
data-component='Notifications'
|
||||
className={classnames({
|
||||
'toolarea-open' : toolAreaOpen
|
||||
})}
|
||||
>
|
||||
{
|
||||
notifications.map((notification) =>
|
||||
{
|
||||
return (
|
||||
<Appear key={notification.id} duration={250}>
|
||||
<div
|
||||
className={classnames('notification', notification.type)}
|
||||
onClick={() => onClick(notification.id)}
|
||||
>
|
||||
<div className='icon' />
|
||||
<p className='text'>{notification.text}</p>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Notifications.propTypes =
|
||||
{
|
||||
notifications : PropTypes.arrayOf(appPropTypes.Notification).isRequired,
|
||||
onClick : PropTypes.func.isRequired,
|
||||
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;
|
||||
39
app/lib/components/PeerAudio/AudioPeer.jsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
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 (
|
||||
<PeerAudio
|
||||
audioTrack={micConsumer ? micConsumer.track : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
44
app/lib/components/PeerAudio/AudioPeers.jsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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 (
|
||||
<div data-component='AudioPeers'>
|
||||
{
|
||||
peers.map((peer) =>
|
||||
{
|
||||
return (
|
||||
<AudioPeer
|
||||
key={peer.name}
|
||||
name={peer.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
67
app/lib/components/PeerAudio/PeerAudio.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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 (
|
||||
<audio
|
||||
ref='audio'
|
||||
autoPlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack } = this.props;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack } = nextProps;
|
||||
|
||||
this._setTrack(audioTrack);
|
||||
}
|
||||
|
||||
_setTrack(audioTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
|
||||
const { audio } = this.refs;
|
||||
|
||||
if (audioTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
audio.srcObject = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.srcObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PeerAudio.propTypes =
|
||||
{
|
||||
audioTrack : PropTypes.any
|
||||
};
|
||||
263
app/lib/components/Room.jsx
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRoomContext } from '../RoomContext';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import CookieConsent from 'react-cookie-consent';
|
||||
import * as appPropTypes from './appPropTypes';
|
||||
import * as requestActions from '../redux/requestActions';
|
||||
import * as stateActions from '../redux/stateActions';
|
||||
import { Appear } from './transitions';
|
||||
import Me from './Containers/Me';
|
||||
import Peers from './Layouts/Peers';
|
||||
import AudioPeers from './PeerAudio/AudioPeers';
|
||||
import Notifications from './Notifications';
|
||||
import ToolArea from './ToolArea/ToolArea';
|
||||
import FullScreenView from './VideoContainers/FullScreenView';
|
||||
import VideoWindow from './VideoWindow/VideoWindow';
|
||||
import Draggable from 'react-draggable';
|
||||
import { idle } from '../utils';
|
||||
import Sidebar from './Controls/Sidebar';
|
||||
import Filmstrip from './Layouts/Filmstrip';
|
||||
|
||||
// Hide toolbars after 10 seconds of inactivity.
|
||||
const TIMEOUT = 10 * 1000;
|
||||
|
||||
class Room extends React.Component
|
||||
{
|
||||
/**
|
||||
* Hides the different toolbars on the page after a
|
||||
* given amount of time has passed since the
|
||||
* last time the cursor was moved.
|
||||
*/
|
||||
waitForHide = idle(() =>
|
||||
{
|
||||
this.props.setToolbarsVisible(false);
|
||||
}, TIMEOUT);
|
||||
|
||||
handleMovement = () =>
|
||||
{
|
||||
// If the toolbars were hidden, show them again when
|
||||
// the user moves their cursor.
|
||||
if (!this.props.room.toolbarsVisible)
|
||||
{
|
||||
this.props.setToolbarsVisible(true);
|
||||
}
|
||||
|
||||
this.waitForHide();
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
window.addEventListener('mousemove', this.handleMovement);
|
||||
window.addEventListener('touchstart', this.handleMovement);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
window.removeEventListener('mousemove', this.handleMovement);
|
||||
window.removeEventListener('touchstart', this.handleMovement);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
room,
|
||||
amActiveSpeaker,
|
||||
onRoomLinkCopy
|
||||
} = this.props;
|
||||
|
||||
const View = {
|
||||
filmstrip : Filmstrip,
|
||||
democratic : Peers
|
||||
}[room.mode];
|
||||
|
||||
if (room.audioSuspended)
|
||||
{
|
||||
return (
|
||||
<Fragment>
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<div className='sound-suspended'>
|
||||
This webpage required sound and video to play, please click to allow.
|
||||
<div
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.notify('Joining.');
|
||||
roomClient.resumeAudio();
|
||||
}}
|
||||
className='button'
|
||||
>
|
||||
<span>Allow</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
else if (room.lockedOut)
|
||||
{
|
||||
return (
|
||||
<Fragment>
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<div className='locked-out'>
|
||||
This room is locked at the moment, try again later.
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
<Fragment>
|
||||
<Appear duration={300}>
|
||||
<div data-component='Room'>
|
||||
<CookieConsent>
|
||||
This website uses cookies to enhance the user experience.
|
||||
</CookieConsent>
|
||||
|
||||
<FullScreenView advancedMode={room.advancedMode} />
|
||||
|
||||
<VideoWindow advancedMode={room.advancedMode} />
|
||||
|
||||
<div className='room-wrapper'>
|
||||
<div data-component='Logo' />
|
||||
<AudioPeers />
|
||||
|
||||
<Notifications />
|
||||
|
||||
<If condition={room.advancedMode}>
|
||||
<div className='state' data-tip='Server status'>
|
||||
<div className={classnames('icon', room.state)} />
|
||||
<p className={classnames('text', room.state)}>{room.state}</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div
|
||||
className={classnames('room-link-wrapper room-controls', {
|
||||
'visible' : this.props.room.toolbarsVisible
|
||||
})}
|
||||
>
|
||||
<div className='room-link'>
|
||||
<CopyToClipboard
|
||||
text={room.url}
|
||||
onCopy={onRoomLinkCopy}
|
||||
>
|
||||
<a
|
||||
className='link'
|
||||
href={room.url}
|
||||
target='_blank'
|
||||
data-tip='Click to copy room link'
|
||||
rel='noopener noreferrer'
|
||||
onClick={(event) =>
|
||||
{
|
||||
// If this is a 'Open in new window/tab' don't prevent
|
||||
// click default action.
|
||||
if (
|
||||
event.ctrlKey || event.shiftKey || event.metaKey ||
|
||||
// Middle click (IE > 9 and everyone else).
|
||||
(event.button && event.button === 1)
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
invitation link
|
||||
</a>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<View advancedMode={room.advancedMode} />
|
||||
|
||||
<Draggable handle='.me-container' bounds='body' cancel='.display-name'>
|
||||
<div
|
||||
className={classnames('me-container', {
|
||||
'active-speaker' : amActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Me
|
||||
advancedMode={room.advancedMode}
|
||||
/>
|
||||
</div>
|
||||
</Draggable>
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ToolArea />
|
||||
</div>
|
||||
</Appear>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Room.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
amActiveSpeaker : PropTypes.bool.isRequired,
|
||||
toolAreaOpen : PropTypes.bool.isRequired,
|
||||
screenProducer : appPropTypes.Producer,
|
||||
onRoomLinkCopy : PropTypes.func.isRequired,
|
||||
setToolbarsVisible : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const producersArray = Object.values(state.producers);
|
||||
const screenProducer =
|
||||
producersArray.find((producer) => producer.source === 'screen');
|
||||
|
||||
return {
|
||||
room : state.room,
|
||||
me : state.me,
|
||||
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||
amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
|
||||
screenProducer : screenProducer
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
onRoomLinkCopy : () =>
|
||||
{
|
||||
dispatch(requestActions.notify(
|
||||
{
|
||||
text : 'Room link copied to the clipboard'
|
||||
}));
|
||||
},
|
||||
|
||||
setToolbarsVisible : (visible) =>
|
||||
{
|
||||
dispatch(stateActions.setToolbarsVisible(visible));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const RoomContainer = withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Room));
|
||||
|
||||
export default RoomContainer;
|
||||
97
app/lib/components/ToolArea/Chat/Chat.jsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import MessageList from './MessageList';
|
||||
|
||||
class Chat extends Component
|
||||
{
|
||||
createNewMessage(text, sender, name, picture)
|
||||
{
|
||||
return {
|
||||
type : 'message',
|
||||
text,
|
||||
time : Date.now(),
|
||||
name,
|
||||
sender,
|
||||
picture
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
senderPlaceHolder,
|
||||
autofocus,
|
||||
displayName,
|
||||
picture
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='Chat'>
|
||||
<MessageList />
|
||||
<form
|
||||
data-component='Sender'
|
||||
onSubmit={(e) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
const userInput = e.target.message.value;
|
||||
|
||||
if (userInput)
|
||||
{
|
||||
const message = this.createNewMessage(userInput, 'response', displayName, picture);
|
||||
|
||||
roomClient.sendChatMessage(message);
|
||||
}
|
||||
e.target.message.value = '';
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type='text'
|
||||
className='new-message'
|
||||
name='message'
|
||||
placeholder={senderPlaceHolder}
|
||||
autoFocus={autofocus}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
className='send'
|
||||
value='Send'
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chat.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
senderPlaceHolder : PropTypes.string,
|
||||
autofocus : PropTypes.bool,
|
||||
displayName : PropTypes.string,
|
||||
picture : PropTypes.string
|
||||
};
|
||||
|
||||
Chat.defaultProps =
|
||||
{
|
||||
senderPlaceHolder : 'Type a message...',
|
||||
autofocus : false,
|
||||
displayName : null
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
displayName : state.me.displayName,
|
||||
picture : state.me.picture
|
||||
};
|
||||
};
|
||||
|
||||
const ChatContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(Chat));
|
||||
|
||||
export default ChatContainer;
|
||||
98
app/lib/components/ToolArea/Chat/MessageList.jsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import marked from 'marked';
|
||||
import { connect } from 'react-redux';
|
||||
import scrollToBottom from '../scrollToBottom';
|
||||
|
||||
const linkRenderer = new marked.Renderer();
|
||||
|
||||
linkRenderer.link = (href, title, text) =>
|
||||
{
|
||||
title = title ? title : href;
|
||||
text = text ? text : href;
|
||||
|
||||
return (`<a target='_blank' href='${ href }' title='${ title }'>${ text }</a>`);
|
||||
};
|
||||
|
||||
class MessageList extends Component
|
||||
{
|
||||
getTimeString(time)
|
||||
{
|
||||
return `${(time.getHours() < 10 ? '0' : '')}${time.getHours()}:${(time.getMinutes() < 10 ? '0' : '')}${time.getMinutes()}`;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
chatmessages
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='MessageList' id='messages'>
|
||||
<Choose>
|
||||
<When condition={chatmessages.length > 0}>
|
||||
{
|
||||
chatmessages.map((message, i) =>
|
||||
{
|
||||
const messageTime = new Date(message.time);
|
||||
|
||||
const picture = (message.sender === 'response' ?
|
||||
message.picture : this.props.myPicture) || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<div className='message' key={i}>
|
||||
<div className={message.sender}>
|
||||
<img className='message-avatar' src={picture} />
|
||||
|
||||
<div className='message-content'>
|
||||
<div
|
||||
className='message-text'
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html : marked.parse(
|
||||
message.text,
|
||||
{ sanitize: true, renderer: linkRenderer }
|
||||
) }}
|
||||
/>
|
||||
|
||||
<span className='message-time'>
|
||||
{message.name} - {this.getTimeString(messageTime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</When>
|
||||
<Otherwise>
|
||||
<div className='empty'>
|
||||
<p>No one has said anything yet...</p>
|
||||
</div>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageList.propTypes =
|
||||
{
|
||||
chatmessages : PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
myPicture : PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
chatmessages : state.chatmessages,
|
||||
myPicture : state.me.picture
|
||||
};
|
||||
};
|
||||
|
||||
const MessageListContainer = compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(MessageList);
|
||||
|
||||
export default MessageListContainer;
|
||||
18
app/lib/components/ToolArea/FileSharing/DragDropSharing.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import WebTorrent from 'webtorrent';
|
||||
import dragDrop from 'drag-drop';
|
||||
import { shareFiles } from './index';
|
||||
|
||||
export const configureDragDrop = () =>
|
||||
{
|
||||
if (WebTorrent.WEBRTC_SUPPORT)
|
||||
{
|
||||
dragDrop('body', async (files) => await shareFiles(files));
|
||||
}
|
||||
};
|
||||
|
||||
export const HoldingOverlay = () => (
|
||||
<div id='holding-overlay'>
|
||||
Drop files here to share them
|
||||
</div>
|
||||
);
|
||||
113
app/lib/components/ToolArea/FileSharing/File.jsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import magnet from 'magnet-uri';
|
||||
|
||||
const DEFAULT_PICTURE = 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
class File extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
torrentSupport,
|
||||
file
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='file-entry'>
|
||||
<img className='file-avatar' src={file.picture || DEFAULT_PICTURE} />
|
||||
|
||||
<div className='file-content'>
|
||||
<Choose>
|
||||
<When condition={file.me}>
|
||||
<p>You shared a file.</p>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>{file.displayName} shared a file.</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<If condition={!file.active && !file.files}>
|
||||
<div className='file-info'>
|
||||
<Choose>
|
||||
<When condition={torrentSupport}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.handleDownload(file.magnetUri);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/download-icon.svg' />
|
||||
</span>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<p>
|
||||
Your browser does not support downloading files using WebTorrent.
|
||||
</p>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
<p>{magnet.decode(file.magnetUri).dn}</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={file.timeout}>
|
||||
<Fragment>
|
||||
<p>
|
||||
If this process takes a long time, there might not be anyone seeding
|
||||
this torrent. Try asking someone to reupload the file that you want.
|
||||
</p>
|
||||
</Fragment>
|
||||
</If>
|
||||
|
||||
<If condition={file.active}>
|
||||
<progress value={file.progress} />
|
||||
</If>
|
||||
|
||||
<If condition={file.files}>
|
||||
<Fragment>
|
||||
<p>File finished downloading.</p>
|
||||
|
||||
{file.files.map((sharedFile, i) => (
|
||||
<div className='file-info' key={i}>
|
||||
<span
|
||||
className='button'
|
||||
onClick={() =>
|
||||
{
|
||||
roomClient.saveFile(sharedFile);
|
||||
}}
|
||||
>
|
||||
<img src='resources/images/save-icon.svg' />
|
||||
</span>
|
||||
|
||||
<p>{sharedFile.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</Fragment>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File.propTypes = {
|
||||
roomClient : PropTypes.object.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { magnetUri }) =>
|
||||
{
|
||||
return {
|
||||
file : state.files[magnetUri],
|
||||
torrentSupport : state.room.torrentSupport
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(File));
|
||||
40
app/lib/components/ToolArea/FileSharing/FileList.jsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React, { Component } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import scrollToBottom from '../scrollToBottom';
|
||||
import File from './File';
|
||||
|
||||
class FileList extends Component
|
||||
{
|
||||
render()
|
||||
{
|
||||
const {
|
||||
files
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className='shared-files'>
|
||||
{ Object.keys(files).map((magnetUri) =>
|
||||
<File key={magnetUri} magnetUri={magnetUri} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileList.propTypes = {
|
||||
files : PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
files : state.files
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
scrollToBottom()
|
||||
)(FileList);
|
||||
88
app/lib/components/ToolArea/FileSharing/FileSharing.jsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import FileList from './FileList';
|
||||
|
||||
class FileSharing extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this._fileInput = React.createRef();
|
||||
}
|
||||
|
||||
handleFileChange = async (event) =>
|
||||
{
|
||||
if (event.target.files.length > 0)
|
||||
{
|
||||
this.props.roomClient.shareFiles(event.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = () =>
|
||||
{
|
||||
if (this.props.torrentSupport)
|
||||
{
|
||||
// We want to open the file dialog when we click a button
|
||||
// instead of actually rendering the input element itself.
|
||||
this._fileInput.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
torrentSupport
|
||||
} = this.props;
|
||||
|
||||
const buttonDescription = torrentSupport ?
|
||||
'Share file' : 'File sharing not supported';
|
||||
|
||||
return (
|
||||
<div data-component='FileSharing'>
|
||||
<div className='sharing-toolbar'>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
ref={this._fileInput}
|
||||
type='file'
|
||||
onChange={this.handleFileChange}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<div
|
||||
type='button'
|
||||
onClick={this.handleClick}
|
||||
className={classNames('share-file', {
|
||||
disabled : !torrentSupport
|
||||
})}
|
||||
>
|
||||
<span>{buttonDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileSharing.propTypes = {
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
torrentSupport : PropTypes.bool.isRequired,
|
||||
tabOpen : PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
torrentSupport : state.room.torrentSupport,
|
||||
tabOpen : state.toolarea.currentToolTab === 'files'
|
||||
};
|
||||
};
|
||||
|
||||
export default withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(FileSharing));
|
||||
38
app/lib/components/ToolArea/ParticipantList/ListMe.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Me } from '../../appPropTypes';
|
||||
|
||||
const ListMe = ({ me }) =>
|
||||
{
|
||||
const picture = me.picture || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<li className='list-item me'>
|
||||
<div data-component='ListPeer'>
|
||||
<img className='avatar' src={picture} />
|
||||
|
||||
<div className='peer-info'>
|
||||
{me.displayName}
|
||||
</div>
|
||||
|
||||
<div className='indicators'>
|
||||
<If condition={me.raisedHand}>
|
||||
<div className='icon raise-hand on' />
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
ListMe.propTypes = {
|
||||
me : Me.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
me : state.me
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(ListMe);
|
||||
124
app/lib/components/ToolArea/ParticipantList/ListPeer.jsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import React 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';
|
||||
|
||||
const ListPeer = (props) =>
|
||||
{
|
||||
const {
|
||||
roomClient,
|
||||
peer,
|
||||
micConsumer,
|
||||
screenConsumer
|
||||
} = props;
|
||||
|
||||
const micEnabled = (
|
||||
Boolean(micConsumer) &&
|
||||
!micConsumer.locallyPaused &&
|
||||
!micConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const screenVisible = (
|
||||
Boolean(screenConsumer) &&
|
||||
!screenConsumer.locallyPaused &&
|
||||
!screenConsumer.remotelyPaused
|
||||
);
|
||||
|
||||
const picture = peer.picture || 'resources/images/avatar-empty.jpeg';
|
||||
|
||||
return (
|
||||
<div data-component='ListPeer'>
|
||||
<img className='avatar' src={picture} />
|
||||
|
||||
<div className='peer-info'>
|
||||
{peer.displayName}
|
||||
</div>
|
||||
<div className='indicators'>
|
||||
<If condition={peer.raiseHandState}>
|
||||
<div className={
|
||||
classnames(
|
||||
'icon', 'raise-hand', {
|
||||
on : peer.raiseHandState,
|
||||
off : !peer.raiseHandState
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${micEnabled && micConsumer ? micConsumer.volume:0}`)} />
|
||||
</div>
|
||||
<div className='controls'>
|
||||
<If condition={screenConsumer}>
|
||||
<div
|
||||
className={classnames('button', 'screen', {
|
||||
on : screenVisible,
|
||||
off : !screenVisible,
|
||||
disabled : peer.peerScreenInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
screenVisible ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'screen', false);
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
<div
|
||||
className={classnames('button', 'mic', {
|
||||
on : micEnabled,
|
||||
off : !micEnabled,
|
||||
disabled : peer.peerAudioInProgress
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
micEnabled ?
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', true) :
|
||||
roomClient.modifyPeerConsumer(peer.name, 'mic', false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListPeer.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peer : appPropTypes.Peer.isRequired,
|
||||
micConsumer : appPropTypes.Consumer,
|
||||
webcamConsumer : appPropTypes.Consumer,
|
||||
screenConsumer : appPropTypes.Consumer
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { name }) =>
|
||||
{
|
||||
const peer = state.peers[name];
|
||||
const consumersArray = peer.consumers
|
||||
.map((consumerId) => state.consumers[consumerId]);
|
||||
const micConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'mic');
|
||||
const webcamConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'webcam');
|
||||
const screenConsumer =
|
||||
consumersArray.find((consumer) => consumer.source === 'screen');
|
||||
|
||||
return {
|
||||
peer,
|
||||
micConsumer,
|
||||
webcamConsumer,
|
||||
screenConsumer
|
||||
};
|
||||
};
|
||||
|
||||
const ListPeerContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(ListPeer));
|
||||
|
||||
export default ListPeerContainer;
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import PropTypes from 'prop-types';
|
||||
import ListPeer from './ListPeer';
|
||||
import ListMe from './ListMe';
|
||||
|
||||
const ParticipantList =
|
||||
({
|
||||
roomClient,
|
||||
advancedMode,
|
||||
peers,
|
||||
selectedPeerName,
|
||||
spotlights
|
||||
}) => (
|
||||
<div data-component='ParticipantList'>
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Me:</li>
|
||||
<ListMe />
|
||||
</ul>
|
||||
<br />
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Participants in Spotlight:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return (spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames('list-item', {
|
||||
selected : peer.name === selectedPeerName
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<br />
|
||||
<ul className='list'>
|
||||
<li className='list-header'>Passive Participants:</li>
|
||||
{peers.filter((peer) =>
|
||||
{
|
||||
return !(spotlights.find((spotlight) =>
|
||||
{ return (spotlight === peer.name); }));
|
||||
}).map((peer) => (
|
||||
<li
|
||||
key={peer.name}
|
||||
className={classNames('list-item', {
|
||||
selected : peer.name === selectedPeerName
|
||||
})}
|
||||
onClick={() => roomClient.setSelectedPeer(peer.name)}
|
||||
>
|
||||
<ListPeer name={peer.name} advancedMode={advancedMode} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
ParticipantList.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired,
|
||||
selectedPeerName : PropTypes.string,
|
||||
spotlights : PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
const peersArray = Object.values(state.peers);
|
||||
|
||||
return {
|
||||
peers : peersArray,
|
||||
selectedPeerName : state.room.selectedPeerName,
|
||||
spotlights : state.room.spotlights
|
||||
};
|
||||
};
|
||||
|
||||
const ParticipantListContainer = withRoomContext(connect(
|
||||
mapStateToProps
|
||||
)(ParticipantList));
|
||||
|
||||
export default ParticipantListContainer;
|
||||
123
app/lib/components/ToolArea/Settings/Settings.jsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as appPropTypes from '../../appPropTypes';
|
||||
import { withRoomContext } from '../../../RoomContext';
|
||||
import * as stateActions from '../../../redux/stateActions';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dropdown from 'react-dropdown';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
const modes = [ {
|
||||
value : 'democratic',
|
||||
label : 'Democratic view'
|
||||
}, {
|
||||
value : 'filmstrip',
|
||||
label : 'Filmstrip view'
|
||||
} ];
|
||||
|
||||
const findOption = (options, value) => options.find((option) => option.value === value);
|
||||
|
||||
const Settings = ({
|
||||
roomClient,
|
||||
room,
|
||||
me,
|
||||
onToggleAdvancedMode,
|
||||
handleChangeMode
|
||||
}) =>
|
||||
{
|
||||
let webcams;
|
||||
|
||||
if (me.webcamDevices)
|
||||
webcams = Array.from(me.webcamDevices.values());
|
||||
else
|
||||
webcams = [];
|
||||
|
||||
let audioDevices;
|
||||
let audioDevicesText;
|
||||
|
||||
if (me.canChangeAudioDevice)
|
||||
audioDevicesText = 'Select audio input device';
|
||||
else
|
||||
audioDevicesText = 'Unable to select audio input device';
|
||||
|
||||
if (me.audioDevices)
|
||||
audioDevices = Array.from(me.audioDevices.values());
|
||||
else
|
||||
audioDevices = [];
|
||||
|
||||
return (
|
||||
<div className='settings'>
|
||||
<Dropdown
|
||||
options={webcams}
|
||||
value={findOption(webcams, me.selectedWebcam)}
|
||||
onChange={(webcam) => roomClient.changeWebcam(webcam.value)}
|
||||
placeholder={'Select camera'}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
disabled={!me.canChangeAudioDevice}
|
||||
options={audioDevices}
|
||||
value={findOption(audioDevices, me.selectedAudioDevice)}
|
||||
onChange={(device) => roomClient.changeAudioDevice(device.value)}
|
||||
placeholder={audioDevicesText}
|
||||
/>
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
/>
|
||||
<div
|
||||
data-tip='keyboard shortcut: ‘a‘'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<input
|
||||
id='room-mode'
|
||||
type='checkbox'
|
||||
checked={room.advancedMode}
|
||||
onChange={onToggleAdvancedMode}
|
||||
/>
|
||||
<label htmlFor='room-mode'>Advanced mode</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tip='keyboard shortcut: type a digit'
|
||||
data-type='dark'
|
||||
data-place='left'
|
||||
>
|
||||
<Dropdown
|
||||
options={modes}
|
||||
value={findOption(modes, room.mode)}
|
||||
onChange={(mode) => handleChangeMode(mode.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes =
|
||||
{
|
||||
roomClient : PropTypes.any.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
room : appPropTypes.Room.isRequired,
|
||||
onToggleAdvancedMode : PropTypes.func.isRequired,
|
||||
handleChangeMode : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
me : state.me,
|
||||
room : state.room
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onToggleAdvancedMode : stateActions.toggleAdvancedMode,
|
||||
handleChangeMode : stateActions.setDisplayMode
|
||||
};
|
||||
|
||||
const SettingsContainer = withRoomContext(connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Settings));
|
||||
|
||||
export default SettingsContainer;
|
||||
41
app/lib/components/ToolArea/TabHeader.jsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
|
||||
const TabHeader = ({ currentToolTab, setToolTab, id, name, badge }) => (
|
||||
<div
|
||||
className={classNames('tab-header', {
|
||||
checked : currentToolTab === id
|
||||
})}
|
||||
onClick={() => setToolTab(id)}
|
||||
>
|
||||
{name}
|
||||
|
||||
<If condition={badge > 0}>
|
||||
<span className='badge'>{badge}</span>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
|
||||
TabHeader.propTypes = {
|
||||
currentToolTab : PropTypes.string.isRequired,
|
||||
setToolTab : PropTypes.func.isRequired,
|
||||
id : PropTypes.string.isRequired,
|
||||
name : PropTypes.string.isRequired,
|
||||
badge : PropTypes.number
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentToolTab : state.toolarea.currentToolTab
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setToolTab : stateActions.setToolTab
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TabHeader);
|
||||
134
app/lib/components/ToolArea/ToolArea.jsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import ParticipantList from './ParticipantList/ParticipantList';
|
||||
import Chat from './Chat/Chat';
|
||||
import Settings from './Settings/Settings';
|
||||
import FileSharing from './FileSharing/FileSharing';
|
||||
import TabHeader from './TabHeader';
|
||||
|
||||
class ToolArea extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
currentToolTab,
|
||||
toolAreaOpen,
|
||||
unreadMessages,
|
||||
unreadFiles,
|
||||
toggleToolArea,
|
||||
unread
|
||||
} = this.props;
|
||||
|
||||
const VisibleTab = {
|
||||
chat : Chat,
|
||||
files : FileSharing,
|
||||
users : ParticipantList,
|
||||
settings : Settings
|
||||
}[currentToolTab];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className={classNames('toolarea-shade', {
|
||||
open : toolAreaOpen
|
||||
})}
|
||||
onClick={toggleToolArea}
|
||||
/>
|
||||
|
||||
<div
|
||||
data-component='ToolArea'
|
||||
className={classNames({
|
||||
open : toolAreaOpen
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className='toolarea-button'
|
||||
onClick={toggleToolArea}
|
||||
>
|
||||
<span className='content'>
|
||||
<div
|
||||
className='toolarea-icon'
|
||||
/>
|
||||
<p>Toolbox</p>
|
||||
</span>
|
||||
<If condition={!toolAreaOpen && unread > 0}>
|
||||
<span className={classNames('badge', { long: unread >= 10 })}>
|
||||
{unread}
|
||||
</span>
|
||||
</If>
|
||||
</div>
|
||||
<div className='tab-headers'>
|
||||
<TabHeader
|
||||
id='chat'
|
||||
name='Chat'
|
||||
badge={unreadMessages}
|
||||
/>
|
||||
|
||||
<TabHeader
|
||||
id='files'
|
||||
name='Files'
|
||||
badge={unreadFiles}
|
||||
/>
|
||||
|
||||
<TabHeader
|
||||
id='users'
|
||||
name='Users'
|
||||
/>
|
||||
|
||||
<TabHeader
|
||||
id='settings'
|
||||
name='Settings'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='tab'>
|
||||
<VisibleTab />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ToolArea.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
currentToolTab : PropTypes.string.isRequired,
|
||||
setToolTab : PropTypes.func.isRequired,
|
||||
unreadMessages : PropTypes.number.isRequired,
|
||||
unreadFiles : PropTypes.number.isRequired,
|
||||
toolAreaOpen : PropTypes.bool,
|
||||
toggleToolArea : PropTypes.func.isRequired,
|
||||
closeToolArea : PropTypes.func.isRequired,
|
||||
unread : PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
currentToolTab : state.toolarea.currentToolTab,
|
||||
unreadMessages : state.toolarea.unreadMessages,
|
||||
unreadFiles : state.toolarea.unreadFiles,
|
||||
toolAreaOpen : state.toolarea.toolAreaOpen,
|
||||
unread : state.toolarea.unreadMessages +
|
||||
state.toolarea.unreadFiles
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setToolTab : stateActions.setToolTab,
|
||||
toggleToolArea : stateActions.toggleToolArea,
|
||||
closeToolArea : stateActions.closeToolArea
|
||||
};
|
||||
|
||||
const ToolAreaContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ToolArea);
|
||||
|
||||
export default ToolAreaContainer;
|
||||
63
app/lib/components/ToolArea/scrollToBottom.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
|
||||
/**
|
||||
* A higher order component which scrolls the user to the bottom of the
|
||||
* wrapped component, provided that the user already was at the bottom
|
||||
* of the wrapped component. Useful for chats and similar use cases.
|
||||
* @param {number} treshold The required distance from the bottom required.
|
||||
*/
|
||||
const scrollToBottom = (treshold = 0) => (WrappedComponent) =>
|
||||
{
|
||||
return class AutoScroller extends Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.ref = React.createRef();
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate()
|
||||
{
|
||||
// Check if the user has scrolled close enough to the bottom for
|
||||
// us to scroll to the bottom or not.
|
||||
return this.elem.scrollHeight - this.elem.scrollTop <=
|
||||
this.elem.clientHeight - treshold;
|
||||
}
|
||||
|
||||
scrollToBottom = () =>
|
||||
{
|
||||
// Scroll the user to the bottom of the wrapped element.
|
||||
this.elem.scrollTop = this.elem.scrollHeight;
|
||||
};
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
// eslint-disable-next-line react/no-find-dom-node
|
||||
this.elem = findDOMNode(this.ref.current);
|
||||
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, atBottom)
|
||||
{
|
||||
if (atBottom)
|
||||
{
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (
|
||||
<WrappedComponent
|
||||
ref={this.ref}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default scrollToBottom;
|
||||
95
app/lib/components/VideoContainers/FullScreenView.jsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import FullView from './FullView';
|
||||
|
||||
const FullScreenView = (props) =>
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
consumer,
|
||||
toggleConsumerFullscreen,
|
||||
toolbarsVisible
|
||||
} = props;
|
||||
|
||||
if (!consumer)
|
||||
return null;
|
||||
|
||||
const consumerVisible = (
|
||||
Boolean(consumer) &&
|
||||
!consumer.locallyPaused &&
|
||||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<div data-component='FullScreenView'>
|
||||
<If condition={consumerVisible && !consumer.supported}>
|
||||
<div className='incompatible-video'>
|
||||
<p>incompatible video</p>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div className='controls'>
|
||||
<div
|
||||
className={classnames('button', 'exitfullscreen', 'room-controls', {
|
||||
visible : toolbarsVisible
|
||||
})}
|
||||
onClick={(e) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
toggleConsumerFullscreen(consumer);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FullView
|
||||
advancedMode={advancedMode}
|
||||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FullScreenView.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
consumer : appPropTypes.Consumer,
|
||||
toggleConsumerFullscreen : PropTypes.func.isRequired,
|
||||
toolbarsVisible : PropTypes.bool
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
consumer : state.consumers[state.room.fullScreenConsumer],
|
||||
toolbarsVisible : state.room.toolbarsVisible
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerFullscreen : (consumer) =>
|
||||
{
|
||||
if (consumer)
|
||||
dispatch(stateActions.toggleConsumerFullscreen(consumer.id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const FullScreenViewContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FullScreenView);
|
||||
|
||||
export default FullScreenViewContainer;
|
||||
83
app/lib/components/VideoContainers/FullView.jsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class FullView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
||||
this.video = React.createRef();
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
videoVisible,
|
||||
videoProfile
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div data-component='FullView'>
|
||||
<video
|
||||
ref={this.video}
|
||||
className={classnames({
|
||||
hidden : !videoVisible,
|
||||
loading : videoProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={Boolean(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { videoTrack } = this.props;
|
||||
|
||||
this._setTracks(videoTrack);
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
const { videoTrack } = this.props;
|
||||
|
||||
this._setTracks(videoTrack);
|
||||
}
|
||||
|
||||
_setTracks(videoTrack)
|
||||
{
|
||||
if (this._videoTrack === videoTrack)
|
||||
return;
|
||||
|
||||
this._videoTrack = videoTrack;
|
||||
|
||||
const video = this.video.current;
|
||||
|
||||
if (videoTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
stream.addTrack(videoTrack);
|
||||
video.srcObject = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FullView.propTypes =
|
||||
{
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool,
|
||||
videoProfile : PropTypes.string
|
||||
};
|
||||
222
app/lib/components/VideoContainers/PeerView.jsx
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import EditableInput from '../Controls/EditableInput';
|
||||
|
||||
export default class PeerView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
volume : 0, // Integer from 0 to 10.,
|
||||
videoWidth : null,
|
||||
videoHeight : null
|
||||
};
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._audioTrack = null;
|
||||
|
||||
// Latest received video track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._videoTrack = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._videoResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
peer,
|
||||
volume,
|
||||
advancedMode,
|
||||
videoVisible,
|
||||
videoProfile,
|
||||
audioCodec,
|
||||
videoCodec,
|
||||
onChangeDisplayName
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
videoWidth,
|
||||
videoHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='PeerView'>
|
||||
<div className='info'>
|
||||
<If condition={advancedMode}>
|
||||
<div className={classnames('media', { 'is-me': isMe })}>
|
||||
<div className='box'>
|
||||
<If condition={audioCodec}>
|
||||
<p className='codec'>{audioCodec}</p>
|
||||
</If>
|
||||
|
||||
<If condition={videoCodec}>
|
||||
<p className='codec'>{videoCodec} {videoProfile}</p>
|
||||
</If>
|
||||
|
||||
<If condition={(videoVisible && videoWidth !== null)}>
|
||||
<p className='resolution'>{videoWidth}x{videoHeight}</p>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div className={classnames('peer', { 'is-me': isMe })}>
|
||||
<Choose>
|
||||
<When condition={isMe}>
|
||||
<EditableInput
|
||||
value={peer.displayName}
|
||||
propName='displayName'
|
||||
className='display-name editable'
|
||||
classLoading='loading'
|
||||
classInvalid='invalid'
|
||||
shouldBlockWhileLoading
|
||||
editProps={{
|
||||
maxLength : 20,
|
||||
autoCorrect : false,
|
||||
spellCheck : false
|
||||
}}
|
||||
onChange={({ displayName }) => onChangeDisplayName(displayName)}
|
||||
/>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<span className='display-name'>
|
||||
{peer.displayName}
|
||||
</span>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<If condition={advancedMode}>
|
||||
<div className='row'>
|
||||
<span
|
||||
className={classnames('device-icon', peer.device.flag)}
|
||||
/>
|
||||
<span className='device-version'>
|
||||
{peer.device.name} {Math.floor(peer.device.version) || null}
|
||||
</span>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({
|
||||
hidden : !videoVisible,
|
||||
'is-me' : isMe,
|
||||
loading : videoProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={isMe}
|
||||
/>
|
||||
|
||||
<div className='volume-container'>
|
||||
<div className={classnames('bar', `level${volume}`)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { audioTrack, videoTrack } = this.props;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { audioTrack, videoTrack } = nextProps;
|
||||
|
||||
this._setTracks(audioTrack, videoTrack);
|
||||
|
||||
}
|
||||
|
||||
_setTracks(audioTrack, videoTrack)
|
||||
{
|
||||
if (this._audioTrack === audioTrack && this._videoTrack === videoTrack)
|
||||
return;
|
||||
|
||||
this._audioTrack = audioTrack;
|
||||
this._videoTrack = videoTrack;
|
||||
|
||||
clearInterval(this._videoResolutionTimer);
|
||||
this._hideVideoResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (audioTrack || videoTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (audioTrack)
|
||||
stream.addTrack(audioTrack);
|
||||
|
||||
if (videoTrack)
|
||||
stream.addTrack(videoTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (videoTrack)
|
||||
this._showVideoResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_showVideoResolution()
|
||||
{
|
||||
this._videoResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { videoWidth, videoHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === videoWidth && video.videoHeight === videoHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
videoWidth : video.videoWidth,
|
||||
videoHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideVideoResolution()
|
||||
{
|
||||
this.setState({ videoWidth: null, videoHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
PeerView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
peer : PropTypes.oneOfType(
|
||||
[ appPropTypes.Me, appPropTypes.Peer ]).isRequired,
|
||||
advancedMode : PropTypes.bool,
|
||||
audioTrack : PropTypes.any,
|
||||
volume : PropTypes.number,
|
||||
videoTrack : PropTypes.any,
|
||||
videoVisible : PropTypes.bool.isRequired,
|
||||
videoProfile : PropTypes.string,
|
||||
audioCodec : PropTypes.string,
|
||||
videoCodec : PropTypes.string,
|
||||
onChangeDisplayName : PropTypes.func
|
||||
};
|
||||
157
app/lib/components/VideoContainers/ScreenView.jsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class ScreenView extends React.Component
|
||||
{
|
||||
constructor(props)
|
||||
{
|
||||
super(props);
|
||||
|
||||
this.state =
|
||||
{
|
||||
screenWidth : null,
|
||||
screenHeight : null
|
||||
};
|
||||
|
||||
// Latest received screen track.
|
||||
// @type {MediaStreamTrack}
|
||||
this._screenTrack = null;
|
||||
|
||||
// Periodic timer for showing video resolution.
|
||||
this._screenResolutionTimer = null;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
isMe,
|
||||
advancedMode,
|
||||
screenVisible,
|
||||
screenProfile,
|
||||
screenCodec
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
screenWidth,
|
||||
screenHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div data-component='ScreenView'>
|
||||
<div className='info'>
|
||||
<If condition={advancedMode}>
|
||||
<div className={classnames('media', { 'is-me': isMe })}>
|
||||
<If condition={screenVisible}>
|
||||
<div className='box'>
|
||||
<If condition={screenCodec}>
|
||||
<p className='codec'>{screenCodec} {screenProfile}</p>
|
||||
</If>
|
||||
|
||||
<If condition={(screenVisible && screenWidth !== null)}>
|
||||
<p className='resolution'>{screenWidth}x{screenHeight}</p>
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<video
|
||||
ref='video'
|
||||
className={classnames({
|
||||
hidden : !screenVisible,
|
||||
'is-me' : isMe,
|
||||
loading : screenProfile === 'none'
|
||||
})}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={Boolean(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
const { screenTrack } = this.props;
|
||||
|
||||
this._setTracks(screenTrack);
|
||||
}
|
||||
|
||||
componentWillUnmount()
|
||||
{
|
||||
clearInterval(this._screenResolutionTimer);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps)
|
||||
{
|
||||
const { screenTrack } = nextProps;
|
||||
|
||||
this._setTracks(screenTrack);
|
||||
}
|
||||
|
||||
_setTracks(screenTrack)
|
||||
{
|
||||
if (this._screenTrack === screenTrack)
|
||||
return;
|
||||
|
||||
this._screenTrack = screenTrack;
|
||||
|
||||
clearInterval(this._screenResolutionTimer);
|
||||
this._hideScreenResolution();
|
||||
|
||||
const { video } = this.refs;
|
||||
|
||||
if (screenTrack)
|
||||
{
|
||||
const stream = new MediaStream;
|
||||
|
||||
if (screenTrack)
|
||||
stream.addTrack(screenTrack);
|
||||
|
||||
video.srcObject = stream;
|
||||
|
||||
if (screenTrack)
|
||||
this._showScreenResolution();
|
||||
}
|
||||
else
|
||||
{
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
_showScreenResolution()
|
||||
{
|
||||
this._screenResolutionTimer = setInterval(() =>
|
||||
{
|
||||
const { screenWidth, screenHeight } = this.state;
|
||||
const { video } = this.refs;
|
||||
|
||||
// Don't re-render if nothing changed.
|
||||
if (video.videoWidth === screenWidth && video.videoHeight === screenHeight)
|
||||
return;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
screenWidth : video.videoWidth,
|
||||
screenHeight : video.videoHeight
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
_hideScreenResolution()
|
||||
{
|
||||
this.setState({ screenWidth: null, screenHeight: null });
|
||||
}
|
||||
}
|
||||
|
||||
ScreenView.propTypes =
|
||||
{
|
||||
isMe : PropTypes.bool,
|
||||
advancedMode : PropTypes.bool,
|
||||
screenTrack : PropTypes.any,
|
||||
screenVisible : PropTypes.bool,
|
||||
screenProfile : PropTypes.string,
|
||||
screenCodec : PropTypes.string
|
||||
};
|
||||
|
|
@ -1,91 +1,16 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import classnames from 'classnames';
|
||||
import FullScreen from '../FullScreen';
|
||||
import FullScreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullScreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
|
||||
const styles = (theme) =>
|
||||
({
|
||||
root :
|
||||
{
|
||||
position : 'absolute',
|
||||
top : 0,
|
||||
left : 0,
|
||||
height : '100%',
|
||||
width : '100%',
|
||||
zIndex : 20000
|
||||
},
|
||||
controls :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 20020,
|
||||
right : 0,
|
||||
top : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'row',
|
||||
justifyContent : 'flex-start',
|
||||
alignItems : 'center',
|
||||
padding : theme.spacing(1),
|
||||
'&.hide' :
|
||||
{
|
||||
transition : 'opacity 0.1s ease-in-out',
|
||||
opacity : 0
|
||||
},
|
||||
'&.hover' :
|
||||
{
|
||||
opacity : 1
|
||||
}
|
||||
},
|
||||
button :
|
||||
{
|
||||
flex : '0 0 auto',
|
||||
margin : '0.2vmin',
|
||||
borderRadius : 2,
|
||||
backgroundColor : 'rgba(255, 255, 255, 0.7)',
|
||||
cursor : 'pointer',
|
||||
transitionProperty : 'opacity, background-color',
|
||||
transitionDuration : '0.15s',
|
||||
width : '5vmin',
|
||||
height : '5vmin'
|
||||
},
|
||||
icon :
|
||||
{
|
||||
fontSize : '5vmin'
|
||||
},
|
||||
incompatibleVideo :
|
||||
{
|
||||
position : 'absolute',
|
||||
zIndex : 20010,
|
||||
top : 0,
|
||||
bottom : 0,
|
||||
left : 0,
|
||||
right : 0,
|
||||
display : 'flex',
|
||||
flexDirection : 'column',
|
||||
justifyContent : 'center',
|
||||
alignItems : 'center',
|
||||
'& p' :
|
||||
{
|
||||
padding : '6px 12px',
|
||||
borderRadius : 6,
|
||||
userSelect : 'none',
|
||||
pointerEvents : 'none',
|
||||
fontSize : 15,
|
||||
color : 'rgba(255, 255, 255, 0.55)'
|
||||
}
|
||||
}
|
||||
});
|
||||
import classnames from 'classnames';
|
||||
|
||||
class NewWindow extends React.PureComponent
|
||||
{
|
||||
static defaultProps =
|
||||
{
|
||||
url : '',
|
||||
name : 'edumeet',
|
||||
title : 'edumeet',
|
||||
name : '',
|
||||
title : '',
|
||||
features : { width: '800px', height: '600px' },
|
||||
onBlock : null,
|
||||
onUnload : null,
|
||||
|
|
@ -124,67 +49,32 @@ class NewWindow extends React.PureComponent
|
|||
|
||||
this.state = {
|
||||
mounted : false,
|
||||
fullscreen : false,
|
||||
hover : false
|
||||
fullscreen : false
|
||||
};
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const {
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
if (!this.state.mounted)
|
||||
return null;
|
||||
|
||||
let touchTimeout = null;
|
||||
|
||||
return ReactDOM.createPortal([
|
||||
<div key='newwindow' className={classes.root}
|
||||
onMouseOver={() => this.setState({ hover: true })}
|
||||
onMouseOut={() => this.setState({ hover: false })}
|
||||
onTouchStart={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
this.setState({ hover: true });
|
||||
}}
|
||||
onTouchEnd={() =>
|
||||
{
|
||||
if (touchTimeout)
|
||||
clearTimeout(touchTimeout);
|
||||
|
||||
touchTimeout = setTimeout(() =>
|
||||
{
|
||||
this.setState({ hover: false });
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<div className={classnames(
|
||||
classes.controls,
|
||||
'hide',
|
||||
this.state.hover ? 'hover' : null
|
||||
)}
|
||||
>
|
||||
{ this.fullscreen.fullscreenEnabled &&
|
||||
<div key='newwindow' data-component='FullScreenView'>
|
||||
<div className='controls'>
|
||||
{this.fullscreen.fullscreenEnabled && (
|
||||
<div
|
||||
className={classes.button}
|
||||
className={classnames('button', {
|
||||
fullscreen : !this.state.fullscreen,
|
||||
exitFullscreen : this.state.fullscreen
|
||||
})}
|
||||
onClick={this.handleToggleFullscreen}
|
||||
data-tip='Fullscreen'
|
||||
data-place='right'
|
||||
data-type='dark'
|
||||
>
|
||||
{ this.state.fullscreen ?
|
||||
<FullScreenExitIcon className={classes.icon} />
|
||||
:
|
||||
<FullScreenIcon className={classes.icon} />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{ this.props.children }
|
||||
{this.props.children}
|
||||
</div>
|
||||
], this.container);
|
||||
}
|
||||
|
|
@ -209,15 +99,11 @@ class NewWindow extends React.PureComponent
|
|||
url,
|
||||
title,
|
||||
name,
|
||||
aspectRatio,
|
||||
features,
|
||||
onBlock,
|
||||
center
|
||||
} = this.props;
|
||||
|
||||
features.width = '800px';
|
||||
features.height = `${800 / aspectRatio}px`;
|
||||
|
||||
if (center === 'parent')
|
||||
{
|
||||
features.left =
|
||||
|
|
@ -228,20 +114,20 @@ class NewWindow extends React.PureComponent
|
|||
else if (center === 'screen')
|
||||
{
|
||||
const screenLeft =
|
||||
window.screenLeft !== undefined ? window.screenLeft : window.screen.left;
|
||||
window.screenLeft !== undefined ? window.screenLeft : screen.left;
|
||||
const screenTop =
|
||||
window.screenTop !== undefined ? window.screenTop : window.screen.top;
|
||||
window.screenTop !== undefined ? window.screenTop : screen.top;
|
||||
|
||||
const width = window.innerWidth
|
||||
? window.innerWidth
|
||||
: document.documentElement.clientWidth
|
||||
? document.documentElement.clientWidth
|
||||
: window.screen.width;
|
||||
: screen.width;
|
||||
const height = window.innerHeight
|
||||
? window.innerHeight
|
||||
: document.documentElement.clientHeight
|
||||
? document.documentElement.clientHeight
|
||||
: window.screen.height;
|
||||
: screen.height;
|
||||
|
||||
features.left = (width / 2) - (features.width / 2) + screenLeft;
|
||||
features.top = (height / 2) - (features.height / 2) + screenTop;
|
||||
|
|
@ -309,17 +195,15 @@ class NewWindow extends React.PureComponent
|
|||
}
|
||||
|
||||
NewWindow.propTypes = {
|
||||
children : PropTypes.node,
|
||||
url : PropTypes.string,
|
||||
name : PropTypes.string,
|
||||
title : PropTypes.string,
|
||||
aspectRatio : PropTypes.number,
|
||||
features : PropTypes.object,
|
||||
onUnload : PropTypes.func,
|
||||
onBlock : PropTypes.func,
|
||||
center : PropTypes.oneOf([ 'parent', 'screen' ]),
|
||||
copyStyles : PropTypes.bool,
|
||||
classes : PropTypes.object.isRequired
|
||||
children : PropTypes.node,
|
||||
url : PropTypes.string,
|
||||
name : PropTypes.string,
|
||||
title : PropTypes.string,
|
||||
features : PropTypes.object,
|
||||
onUnload : PropTypes.func,
|
||||
onBlock : PropTypes.func,
|
||||
center : PropTypes.oneOf([ 'parent', 'screen' ]),
|
||||
copyStyles : PropTypes.bool
|
||||
};
|
||||
|
||||
function copyStyles(source, target)
|
||||
|
|
@ -399,4 +283,4 @@ function toWindowFeatures(obj)
|
|||
.join(',');
|
||||
}
|
||||
|
||||
export default withStyles(styles)(NewWindow);
|
||||
export default NewWindow;
|
||||
72
app/lib/components/VideoWindow/VideoWindow.jsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import NewWindow from './NewWindow';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as appPropTypes from '../appPropTypes';
|
||||
import * as stateActions from '../../redux/stateActions';
|
||||
import FullView from '../VideoContainers/FullView';
|
||||
|
||||
const VideoWindow = (props) =>
|
||||
{
|
||||
const {
|
||||
advancedMode,
|
||||
consumer,
|
||||
toggleConsumerWindow
|
||||
} = props;
|
||||
|
||||
if (!consumer)
|
||||
return null;
|
||||
|
||||
const consumerVisible = (
|
||||
Boolean(consumer) &&
|
||||
!consumer.locallyPaused &&
|
||||
!consumer.remotelyPaused
|
||||
);
|
||||
|
||||
let consumerProfile;
|
||||
|
||||
if (consumer)
|
||||
consumerProfile = consumer.profile;
|
||||
|
||||
return (
|
||||
<NewWindow onUnload={toggleConsumerWindow}>
|
||||
<FullView
|
||||
advancedMode={advancedMode}
|
||||
videoTrack={consumer ? consumer.track : null}
|
||||
videoVisible={consumerVisible}
|
||||
videoProfile={consumerProfile}
|
||||
/>
|
||||
</NewWindow>
|
||||
);
|
||||
};
|
||||
|
||||
VideoWindow.propTypes =
|
||||
{
|
||||
advancedMode : PropTypes.bool,
|
||||
consumer : appPropTypes.Consumer,
|
||||
toggleConsumerWindow : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
{
|
||||
return {
|
||||
consumer : state.consumers[state.room.windowConsumer]
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
{
|
||||
return {
|
||||
toggleConsumerWindow : () =>
|
||||
{
|
||||
dispatch(stateActions.toggleConsumerWindow());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const VideoWindowContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(VideoWindow);
|
||||
|
||||
export default VideoWindowContainer;
|
||||
91
app/lib/components/appPropTypes.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Room = PropTypes.shape(
|
||||
{
|
||||
url : PropTypes.string.isRequired,
|
||||
state : PropTypes.oneOf(
|
||||
[ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
|
||||
activeSpeakerName : PropTypes.string
|
||||
});
|
||||
|
||||
export const Device = PropTypes.shape(
|
||||
{
|
||||
flag : PropTypes.string.isRequired,
|
||||
name : PropTypes.string.isRequired,
|
||||
version : PropTypes.string
|
||||
});
|
||||
|
||||
export const Me = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
displayNameSet : PropTypes.bool.isRequired,
|
||||
device : Device.isRequired,
|
||||
canSendMic : PropTypes.bool.isRequired,
|
||||
canSendWebcam : PropTypes.bool.isRequired,
|
||||
webcamInProgress : PropTypes.bool.isRequired,
|
||||
audioOnly : PropTypes.bool.isRequired,
|
||||
audioOnlyInProgress : PropTypes.bool.isRequired,
|
||||
restartIceInProgress : PropTypes.bool.isRequired
|
||||
});
|
||||
|
||||
export const Producer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
deviceLabel : PropTypes.string,
|
||||
type : PropTypes.oneOf([ 'front', 'back', 'screen' ]),
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
export const Peer = PropTypes.shape(
|
||||
{
|
||||
name : PropTypes.string.isRequired,
|
||||
displayName : PropTypes.string,
|
||||
device : Device.isRequired,
|
||||
consumers : PropTypes.arrayOf(PropTypes.number).isRequired
|
||||
});
|
||||
|
||||
export const Consumer = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.number.isRequired,
|
||||
peerName : PropTypes.string.isRequired,
|
||||
source : PropTypes.oneOf([ 'mic', 'webcam', 'screen' ]).isRequired,
|
||||
supported : PropTypes.bool.isRequired,
|
||||
locallyPaused : PropTypes.bool.isRequired,
|
||||
remotelyPaused : PropTypes.bool.isRequired,
|
||||
profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
|
||||
track : PropTypes.any,
|
||||
codec : PropTypes.string
|
||||
});
|
||||
|
||||
export const Notification = PropTypes.shape(
|
||||
{
|
||||
id : PropTypes.string.isRequired,
|
||||
type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
|
||||
timeout : PropTypes.number
|
||||
});
|
||||
|
||||
export const Message = PropTypes.shape(
|
||||
{
|
||||
type : PropTypes.string,
|
||||
component : PropTypes.string,
|
||||
text : PropTypes.string,
|
||||
sender : PropTypes.string
|
||||
});
|
||||
|
||||
export const FileEntryProps = PropTypes.shape(
|
||||
{
|
||||
data : PropTypes.shape({
|
||||
name : PropTypes.string.isRequired,
|
||||
picture : PropTypes.string,
|
||||
file : PropTypes.shape({
|
||||
magnet : PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
me : PropTypes.bool
|
||||
}).isRequired,
|
||||
notify : PropTypes.func.isRequired
|
||||
});
|
||||
22
app/lib/components/transitions.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
const Appear = ({ duration, children }) => (
|
||||
<CSSTransition
|
||||
in
|
||||
classNames='Appear'
|
||||
timeout={duration || 1000}
|
||||
appear
|
||||
>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
Appear.propTypes =
|
||||
{
|
||||
duration : PropTypes.number,
|
||||
children : PropTypes.any
|
||||
};
|
||||
|
||||
export { Appear };
|
||||
34
app/lib/cookiesManager.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import jsCookie from 'js-cookie';
|
||||
|
||||
const USER_COOKIE = 'multiparty-meeting.user';
|
||||
const DEVICES_COOKIE = 'multiparty-meeting.devices';
|
||||
|
||||
export function getUser()
|
||||
{
|
||||
return jsCookie.getJSON(USER_COOKIE);
|
||||
}
|
||||
|
||||
export function setUser({ displayName })
|
||||
{
|
||||
jsCookie.set(USER_COOKIE, { displayName });
|
||||
}
|
||||
|
||||
export function getDevices()
|
||||
{
|
||||
return jsCookie.getJSON(DEVICES_COOKIE);
|
||||
}
|
||||
|
||||
export function setDevices({ webcamEnabled })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { webcamEnabled });
|
||||
}
|
||||
|
||||
export function setAudioDevice({ audioDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { audioDeviceId });
|
||||
}
|
||||
|
||||
export function setVideoDevice({ videoDeviceId })
|
||||
{
|
||||
jsCookie.set(DEVICES_COOKIE, { videoDeviceId });
|
||||
}
|
||||
157
app/lib/index.jsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import domready from 'domready';
|
||||
import UrlParse from 'url-parse';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { getDeviceInfo } from 'mediasoup-client';
|
||||
import randomString from 'random-string';
|
||||
import Logger from './Logger';
|
||||
import * as utils from './utils';
|
||||
import RoomClient from './RoomClient';
|
||||
import RoomContext from './RoomContext';
|
||||
import * as cookiesManager from './cookiesManager';
|
||||
import * as stateActions from './redux/stateActions';
|
||||
import Room from './components/Room';
|
||||
import { loginEnabled } from '../config/config';
|
||||
import { store } from './store';
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
let roomClient;
|
||||
|
||||
RoomClient.init({ store });
|
||||
|
||||
domready(() =>
|
||||
{
|
||||
logger.debug('DOM ready');
|
||||
|
||||
// Load stuff and run
|
||||
utils.initialize()
|
||||
.then(run);
|
||||
});
|
||||
|
||||
function run()
|
||||
{
|
||||
logger.debug('run() [environment:%s]', process.env.NODE_ENV);
|
||||
|
||||
const peerName = randomString({ length: 8 }).toLowerCase();
|
||||
const urlParser = new UrlParse(window.location.href, true);
|
||||
let roomId = (urlParser.pathname).substr(1)
|
||||
? (urlParser.pathname).substr(1).toLowerCase() : urlParser.query.roomId.toLowerCase();
|
||||
const produce = urlParser.query.produce !== 'false';
|
||||
let displayName = urlParser.query.displayName;
|
||||
const isSipEndpoint = urlParser.query.sipEndpoint === 'true';
|
||||
const useSimulcast = urlParser.query.simulcast === 'true';
|
||||
|
||||
if (!roomId)
|
||||
{
|
||||
roomId = randomString({ length: 8 }).toLowerCase();
|
||||
|
||||
urlParser.query.roomId = roomId;
|
||||
window.history.pushState('', '', urlParser.toString());
|
||||
}
|
||||
|
||||
// Get the effective/shareable Room URL.
|
||||
const roomUrlParser = new UrlParse(window.location.href, true);
|
||||
|
||||
for (const key of Object.keys(roomUrlParser.query))
|
||||
{
|
||||
// Don't keep some custom params.
|
||||
switch (key)
|
||||
{
|
||||
case 'roomId':
|
||||
case 'simulcast':
|
||||
break;
|
||||
default:
|
||||
delete roomUrlParser.query[key];
|
||||
}
|
||||
}
|
||||
delete roomUrlParser.hash;
|
||||
|
||||
const roomUrl = roomUrlParser.toString();
|
||||
|
||||
// Get displayName from cookie (if not already given as param).
|
||||
const userCookie = cookiesManager.getUser() || {};
|
||||
let displayNameSet;
|
||||
|
||||
if (!displayName)
|
||||
displayName = userCookie.displayName;
|
||||
|
||||
if (displayName)
|
||||
{
|
||||
displayNameSet = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
displayName = 'Guest';
|
||||
displayNameSet = false;
|
||||
}
|
||||
|
||||
// Get current device.
|
||||
const device = getDeviceInfo();
|
||||
|
||||
// If a SIP endpoint mangle device info.
|
||||
if (isSipEndpoint)
|
||||
{
|
||||
device.flag = 'sipendpoint';
|
||||
device.name = 'SIP Endpoint';
|
||||
device.version = undefined;
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setRoomUrl(roomUrl));
|
||||
|
||||
store.dispatch(
|
||||
stateActions.setMe({ peerName, displayName, displayNameSet, device, loginEnabled }));
|
||||
|
||||
roomClient = new RoomClient(
|
||||
{ roomId, peerName, displayName, device, useSimulcast, produce });
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<RoomContext.Provider value={roomClient}>
|
||||
<Room />
|
||||
</RoomContext.Provider>
|
||||
</Provider>,
|
||||
document.getElementById('multiparty-meeting')
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Debugging stuff.
|
||||
global.CLIENT = roomClient;
|
||||
|
||||
/* setInterval(() =>
|
||||
{
|
||||
if (!roomClient._room.peers[0])
|
||||
{
|
||||
delete global.CONSUMER;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const peer = roomClient._room.peers[0];
|
||||
|
||||
global.CONSUMER = peer.consumers[peer.consumers.length - 1];
|
||||
}, 2000);
|
||||
*/
|
||||
global.sendSdp = function()
|
||||
{
|
||||
logger.debug('---------- SEND_TRANSPORT LOCAL SDP OFFER:');
|
||||
logger.debug(
|
||||
roomClient._sendTransport._handler._pc.localDescription.sdp);
|
||||
|
||||
logger.debug('---------- SEND_TRANSPORT REMOTE SDP ANSWER:');
|
||||
logger.debug(
|
||||
roomClient._sendTransport._handler._pc.remoteDescription.sdp);
|
||||
};
|
||||
|
||||
global.recvSdp = function()
|
||||
{
|
||||
logger.debug('---------- RECV_TRANSPORT REMOTE SDP OFFER:');
|
||||
logger.debug(
|
||||
roomClient._recvTransport._handler._pc.remoteDescription.sdp);
|
||||
|
||||
logger.debug('---------- RECV_TRANSPORT LOCAL SDP ANSWER:');
|
||||
logger.debug(
|
||||
roomClient._recvTransport._handler._pc.localDescription.sdp);
|
||||
};
|
||||
101
app/lib/redux/STATE.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# APP STATE
|
||||
|
||||
```js
|
||||
{
|
||||
peerWidth : 200,
|
||||
peerHeight : 150,
|
||||
room :
|
||||
{
|
||||
url : 'https://example.io/?&roomId=d0el8y34',
|
||||
state : 'connected', // new/connecting/connected/closed
|
||||
activeSpeakerName : 'alice'
|
||||
},
|
||||
me :
|
||||
{
|
||||
name : 'bob',
|
||||
displayName : 'Bob McFLower',
|
||||
displayNameSet : false, // true if got from cookie or manually set.
|
||||
device : { flag: 'firefox', name: 'Firefox', version: '61' },
|
||||
canSendMic : true,
|
||||
canSendWebcam : true,
|
||||
webcamInProgress : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
restartIceInProgress : false
|
||||
},
|
||||
producers :
|
||||
{
|
||||
1111 :
|
||||
{
|
||||
id : 1111,
|
||||
source : 'mic', // mic/webcam,
|
||||
locallyPaused : true,
|
||||
remotelyPaused : false,
|
||||
track : MediaStreamTrack,
|
||||
codec : 'opus'
|
||||
},
|
||||
1112 :
|
||||
{
|
||||
id : 1112,
|
||||
source : 'webcam', // mic/webcam
|
||||
deviceLabel : 'Macbook Webcam',
|
||||
type : 'front', // front/back
|
||||
locallyPaused : false,
|
||||
remotelyPaused : false,
|
||||
track : MediaStreamTrack,
|
||||
codec : 'vp8',
|
||||
}
|
||||
},
|
||||
peers :
|
||||
{
|
||||
'alice' :
|
||||
{
|
||||
name : 'alice',
|
||||
displayName : 'Alice Thomsom',
|
||||
raiseHandState : false,
|
||||
device : { flag: 'chrome', name: 'Chrome', version: '58' },
|
||||
consumers : [ 5551, 5552 ]
|
||||
}
|
||||
},
|
||||
consumers :
|
||||
{
|
||||
5551 :
|
||||
{
|
||||
id : 5551,
|
||||
peerName : 'alice',
|
||||
source : 'mic', // mic/webcam
|
||||
supported : true,
|
||||
locallyPaused : false,
|
||||
remotelyPaused : false,
|
||||
profile : 'default',
|
||||
track : MediaStreamTrack,
|
||||
codec : 'opus'
|
||||
},
|
||||
5552 :
|
||||
{
|
||||
id : 5552,
|
||||
peerName : 'alice',
|
||||
source : 'webcam',
|
||||
supported : false,
|
||||
locallyPaused : false,
|
||||
remotelyPaused : true,
|
||||
profile : 'medium',
|
||||
track : null,
|
||||
codec : 'h264'
|
||||
}
|
||||
},
|
||||
notifications :
|
||||
[
|
||||
{
|
||||
id : 'qweasdw43we',
|
||||
type : 'info' // info/error
|
||||
text : 'You joined the room'
|
||||
},
|
||||
{
|
||||
id : 'j7sdhkjjkcc',
|
||||
type : 'error'
|
||||
text : 'Could not add webcam'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
43
app/lib/redux/reducers/chatmessages.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import
|
||||
{
|
||||
createNewMessage
|
||||
} from './helper';
|
||||
|
||||
const chatmessages = (state = [], action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_NEW_USER_MESSAGE':
|
||||
{
|
||||
const { text } = action.payload;
|
||||
|
||||
const message = createNewMessage(text, 'client', 'Me', undefined);
|
||||
|
||||
return [ ...state, message ];
|
||||
}
|
||||
|
||||
case 'ADD_NEW_RESPONSE_MESSAGE':
|
||||
{
|
||||
const { message } = action.payload;
|
||||
|
||||
return [ ...state, message ];
|
||||
}
|
||||
|
||||
case 'ADD_CHAT_HISTORY':
|
||||
{
|
||||
const { chatHistory } = action.payload;
|
||||
|
||||
return [ ...state, ...chatHistory ];
|
||||
}
|
||||
|
||||
case 'DROP_MESSAGES':
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default chatmessages;
|
||||
|
|
@ -25,7 +25,6 @@ const consumers = (state = initialState, action) =>
|
|||
{
|
||||
const { consumerId, originator } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
|
||||
let newConsumer;
|
||||
|
||||
if (originator === 'local')
|
||||
|
|
@ -36,11 +35,19 @@ const consumers = (state = initialState, action) =>
|
|||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_VOLUME':
|
||||
{
|
||||
const { consumerId, volume } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, volume };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_RESUMED':
|
||||
{
|
||||
const { consumerId, originator } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
|
||||
let newConsumer;
|
||||
|
||||
if (originator === 'local')
|
||||
|
|
@ -51,39 +58,11 @@ const consumers = (state = initialState, action) =>
|
|||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_CURRENT_LAYERS':
|
||||
case 'SET_CONSUMER_EFFECTIVE_PROFILE':
|
||||
{
|
||||
const { consumerId, spatialLayer, temporalLayer } = action.payload;
|
||||
const { consumerId, profile } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer =
|
||||
{
|
||||
...consumer,
|
||||
currentSpatialLayer : spatialLayer,
|
||||
currentTemporalLayer : temporalLayer
|
||||
};
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_PREFERRED_LAYERS':
|
||||
{
|
||||
const { consumerId, spatialLayer, temporalLayer } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer =
|
||||
{
|
||||
...consumer,
|
||||
preferredSpatialLayer : spatialLayer,
|
||||
preferredTemporalLayer : temporalLayer
|
||||
};
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_PRIORITY':
|
||||
{
|
||||
const { consumerId, priority } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, priority };
|
||||
const newConsumer = { ...consumer, profile };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
|
@ -97,46 +76,6 @@ const consumers = (state = initialState, action) =>
|
|||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_AUDIO_GAIN':
|
||||
{
|
||||
const { consumerId, audioGain } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer = { ...consumer, audioGain };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_SCORE':
|
||||
{
|
||||
const { consumerId, score } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
|
||||
if (!consumer)
|
||||
return state;
|
||||
|
||||
const newConsumer = { ...consumer, score };
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
case 'CLEAR_CONSUMERS':
|
||||
{
|
||||
return initialState;
|
||||
}
|
||||
|
||||
case 'SET_CONSUMER_OPUS_CONFIG':
|
||||
{
|
||||
const { consumerId, opusConfig } = action.payload;
|
||||
const consumer = state[consumerId];
|
||||
const newConsumer =
|
||||
{
|
||||
...consumer,
|
||||
opusConfig
|
||||
};
|
||||
|
||||
return { ...state, [consumerId]: newConsumer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
99
app/lib/redux/reducers/files.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
const files = (state = {}, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_FILE':
|
||||
{
|
||||
const { file } = action.payload;
|
||||
|
||||
const newFile = {
|
||||
active : false,
|
||||
progress : 0,
|
||||
files : null,
|
||||
me : false,
|
||||
...file
|
||||
};
|
||||
|
||||
return { ...state, [file.magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'ADD_FILE_HISTORY':
|
||||
{
|
||||
const { fileHistory } = action.payload;
|
||||
const newFileHistory = {};
|
||||
|
||||
fileHistory.map((file) =>
|
||||
{
|
||||
const newFile = {
|
||||
active : false,
|
||||
progress : 0,
|
||||
files : null,
|
||||
me : false,
|
||||
...file
|
||||
};
|
||||
|
||||
newFileHistory[file.magnetUri] = newFile;
|
||||
});
|
||||
|
||||
return { ...state, ...newFileHistory };
|
||||
}
|
||||
|
||||
case 'SET_FILE_ACTIVE':
|
||||
{
|
||||
const { magnetUri } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = { ...file, active: true };
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'SET_FILE_INACTIVE':
|
||||
{
|
||||
const { magnetUri } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = { ...file, active: false };
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'SET_FILE_PROGRESS':
|
||||
{
|
||||
const { magnetUri, progress } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = { ...file, progress: progress };
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'SET_FILE_DONE':
|
||||
{
|
||||
const { magnetUri, sharedFiles } = action.payload;
|
||||
const file = state[magnetUri];
|
||||
|
||||
const newFile = {
|
||||
...file,
|
||||
files : sharedFiles,
|
||||
progress : 1,
|
||||
active : false,
|
||||
timeout : false
|
||||
};
|
||||
|
||||
return { ...state, [magnetUri]: newFile };
|
||||
}
|
||||
|
||||
case 'REMOVE_FILE':
|
||||
{
|
||||
const { magnetUri } = action.payload;
|
||||
|
||||
return state.filter((file) => file.magnetUri !== magnetUri);
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default files;
|
||||
11
app/lib/redux/reducers/helper.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export function createNewMessage(text, sender, name, picture)
|
||||
{
|
||||
return {
|
||||
type : 'message',
|
||||
text,
|
||||
time : Date.now(),
|
||||
name,
|
||||
sender,
|
||||
picture
|
||||
};
|
||||
}
|
||||
25
app/lib/redux/reducers/index.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import room from './room';
|
||||
import me from './me';
|
||||
import producers from './producers';
|
||||
import peers from './peers';
|
||||
import consumers from './consumers';
|
||||
import notifications from './notifications';
|
||||
import chatmessages from './chatmessages';
|
||||
import toolarea from './toolarea';
|
||||
import files from './files';
|
||||
|
||||
const reducers = combineReducers(
|
||||
{
|
||||
room,
|
||||
me,
|
||||
producers,
|
||||
peers,
|
||||
consumers,
|
||||
notifications,
|
||||
chatmessages,
|
||||
toolarea,
|
||||
files
|
||||
});
|
||||
|
||||
export default reducers;
|
||||
189
app/lib/redux/reducers/me.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
const initialState =
|
||||
{
|
||||
name : null,
|
||||
displayName : null,
|
||||
displayNameSet : false,
|
||||
device : null,
|
||||
canSendMic : false,
|
||||
canSendWebcam : false,
|
||||
canShareScreen : false,
|
||||
needExtension : false,
|
||||
canChangeAudioDevice : false,
|
||||
audioDevices : null,
|
||||
canChangeWebcam : false,
|
||||
webcamDevices : null,
|
||||
webcamInProgress : false,
|
||||
audioInProgress : false,
|
||||
screenShareInProgress : false,
|
||||
loginEnabled : false,
|
||||
audioOnly : false,
|
||||
audioOnlyInProgress : false,
|
||||
raiseHand : false,
|
||||
raiseHandInProgress : false,
|
||||
restartIceInProgress : false,
|
||||
picture : null,
|
||||
selectedWebcam : null,
|
||||
selectedAudioDevice : null,
|
||||
loggedIn : false
|
||||
};
|
||||
|
||||
const me = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'SET_ME':
|
||||
{
|
||||
const {
|
||||
peerName,
|
||||
displayName,
|
||||
displayNameSet,
|
||||
device,
|
||||
loginEnabled
|
||||
} = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
name : peerName,
|
||||
displayName,
|
||||
displayNameSet,
|
||||
device,
|
||||
loginEnabled
|
||||
};
|
||||
}
|
||||
|
||||
case 'LOGGED_IN':
|
||||
return { ...state, loggedIn: true };
|
||||
|
||||
case 'USER_LOGOUT':
|
||||
return { ...state, loggedIn: false };
|
||||
|
||||
case 'CHANGE_WEBCAM':
|
||||
{
|
||||
return { ...state, selectedWebcam: action.payload.deviceId };
|
||||
}
|
||||
|
||||
case 'CHANGE_AUDIO_DEVICE':
|
||||
{
|
||||
return { ...state, selectedAudioDevice: action.payload.deviceId };
|
||||
}
|
||||
|
||||
case 'SET_MEDIA_CAPABILITIES':
|
||||
{
|
||||
const { canSendMic, canSendWebcam } = action.payload;
|
||||
|
||||
return { ...state, canSendMic, canSendWebcam };
|
||||
}
|
||||
|
||||
case 'SET_SCREEN_CAPABILITIES':
|
||||
{
|
||||
const { canShareScreen, needExtension } = action.payload;
|
||||
|
||||
return { ...state, canShareScreen, needExtension };
|
||||
}
|
||||
|
||||
case 'SET_CAN_CHANGE_AUDIO_DEVICE':
|
||||
{
|
||||
const canChangeAudioDevice = action.payload;
|
||||
|
||||
return { ...state, canChangeAudioDevice };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_DEVICES':
|
||||
{
|
||||
const { devices } = action.payload;
|
||||
|
||||
return { ...state, audioDevices: devices };
|
||||
}
|
||||
|
||||
case 'SET_CAN_CHANGE_WEBCAM':
|
||||
{
|
||||
const canChangeWebcam = action.payload;
|
||||
|
||||
return { ...state, canChangeWebcam };
|
||||
}
|
||||
|
||||
case 'SET_WEBCAM_DEVICES':
|
||||
{
|
||||
const { devices } = action.payload;
|
||||
|
||||
return { ...state, webcamDevices: devices };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, audioInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_WEBCAM_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, webcamInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_SCREEN_SHARE_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, screenShareInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_DISPLAY_NAME':
|
||||
{
|
||||
let { displayName } = action.payload;
|
||||
|
||||
// Be ready for undefined displayName (so keep previous one).
|
||||
if (!displayName)
|
||||
displayName = state.displayName;
|
||||
|
||||
return { ...state, displayName, displayNameSet: true };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_ONLY_STATE':
|
||||
{
|
||||
const { enabled } = action.payload;
|
||||
|
||||
return { ...state, audioOnly: enabled };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_ONLY_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, audioOnlyInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_MY_RAISE_HAND_STATE':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, raiseHand: flag };
|
||||
}
|
||||
|
||||
case 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, raiseHandInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_RESTART_ICE_IN_PROGRESS':
|
||||
{
|
||||
const { flag } = action.payload;
|
||||
|
||||
return { ...state, restartIceInProgress: flag };
|
||||
}
|
||||
|
||||
case 'SET_PICTURE':
|
||||
{
|
||||
return { ...state, picture: action.payload.picture };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default me;
|
||||
|
|
@ -6,17 +6,6 @@ const notifications = (state = [], action) =>
|
|||
{
|
||||
const { notification } = action.payload;
|
||||
|
||||
notification.toBeClosed=false;
|
||||
|
||||
return [ ...state, notification ];
|
||||
}
|
||||
|
||||
case 'ADD_CONSENT_NOTIFICATION':
|
||||
{
|
||||
const { notification } = action.payload;
|
||||
|
||||
notification.toBeClosed=false;
|
||||
|
||||
return [ ...state, notification ];
|
||||
}
|
||||
|
||||
|
|
@ -32,21 +21,6 @@ const notifications = (state = [], action) =>
|
|||
return [];
|
||||
}
|
||||
|
||||
case 'CLOSE_NOTIFICATION':
|
||||
{
|
||||
const { notificationId } = action.payload;
|
||||
|
||||
return (state.map((e) =>
|
||||
{
|
||||
if (e.id === notificationId)
|
||||
{
|
||||
e.toBeClosed=true;
|
||||
}
|
||||
|
||||
return e;
|
||||
}));
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
98
app/lib/redux/reducers/peers.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import omit from 'lodash/omit';
|
||||
|
||||
const peer = (state = {}, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_PEER':
|
||||
return action.payload.peer;
|
||||
|
||||
case 'SET_PEER_DISPLAY_NAME':
|
||||
return { ...state, displayName: action.payload.displayName };
|
||||
|
||||
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||
return { ...state, peerVideoInProgress: action.payload.flag };
|
||||
|
||||
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||
return { ...state, peerAudioInProgress: action.payload.flag };
|
||||
|
||||
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||
return { ...state, peerScreenInProgress: action.payload.flag };
|
||||
|
||||
case 'SET_PEER_RAISE_HAND_STATE':
|
||||
return { ...state, raiseHandState: action.payload.raiseHandState };
|
||||
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const consumers = [ ...state.consumers, action.payload.consumer.id ];
|
||||
|
||||
return { ...state, consumers };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const consumers = state.consumers.filter((consumer) =>
|
||||
consumer !== action.payload.consumerId);
|
||||
|
||||
return { ...state, consumers };
|
||||
}
|
||||
|
||||
case 'SET_PEER_PICTURE':
|
||||
{
|
||||
return { ...state, picture: action.payload.picture };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const peers = (state = {}, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'ADD_PEER':
|
||||
{
|
||||
return { ...state, [action.payload.peer.name]: peer(undefined, action) };
|
||||
}
|
||||
|
||||
case 'REMOVE_PEER':
|
||||
{
|
||||
return omit(state, [ action.payload.peerName ]);
|
||||
}
|
||||
|
||||
case 'SET_PEER_DISPLAY_NAME':
|
||||
case 'SET_PEER_VIDEO_IN_PROGRESS':
|
||||
case 'SET_PEER_AUDIO_IN_PROGRESS':
|
||||
case 'SET_PEER_SCREEN_IN_PROGRESS':
|
||||
case 'SET_PEER_RAISE_HAND_STATE':
|
||||
case 'SET_PEER_PICTURE':
|
||||
case 'ADD_CONSUMER':
|
||||
{
|
||||
const oldPeer = state[action.payload.peerName];
|
||||
|
||||
if (!oldPeer)
|
||||
{
|
||||
throw new Error('no Peer found');
|
||||
}
|
||||
|
||||
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||
}
|
||||
|
||||
case 'REMOVE_CONSUMER':
|
||||
{
|
||||
const oldPeer = state[action.payload.peerName];
|
||||
|
||||
// NOTE: This means that the Peer was closed before, so it's ok.
|
||||
if (!oldPeer)
|
||||
return state;
|
||||
|
||||
return { ...state, [oldPeer.name]: peer(oldPeer, action) };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default peers;
|
||||
|
|
@ -25,7 +25,6 @@ const producers = (state = initialState, action) =>
|
|||
{
|
||||
const { producerId, originator } = action.payload;
|
||||
const producer = state[producerId];
|
||||
|
||||
let newProducer;
|
||||
|
||||
if (originator === 'local')
|
||||
|
|
@ -36,11 +35,19 @@ const producers = (state = initialState, action) =>
|
|||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_VOLUME':
|
||||
{
|
||||
const { producerId, volume } = action.payload;
|
||||
const producer = state[producerId];
|
||||
const newProducer = { ...producer, volume };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_RESUMED':
|
||||
{
|
||||
const { producerId, originator } = action.payload;
|
||||
const producer = state[producerId];
|
||||
|
||||
let newProducer;
|
||||
|
||||
if (originator === 'local')
|
||||
|
|
@ -60,17 +67,6 @@ const producers = (state = initialState, action) =>
|
|||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
case 'SET_PRODUCER_SCORE':
|
||||
{
|
||||
const { producerId, score } = action.payload;
|
||||
|
||||
const producer = state[producerId];
|
||||
|
||||
const newProducer = { ...producer, score };
|
||||
|
||||
return { ...state, [producerId]: newProducer };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
144
app/lib/redux/reducers/room.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
const initialState =
|
||||
{
|
||||
url : null,
|
||||
state : 'new', // new/connecting/connected/disconnected/closed,
|
||||
locked : false,
|
||||
lockedOut : false,
|
||||
audioSuspended : false,
|
||||
activeSpeakerName : null,
|
||||
torrentSupport : false,
|
||||
showSettings : false,
|
||||
advancedMode : false,
|
||||
fullScreenConsumer : null, // ConsumerID
|
||||
windowConsumer : null, // ConsumerID
|
||||
toolbarsVisible : true,
|
||||
mode : 'democratic',
|
||||
selectedPeerName : null,
|
||||
spotlights : []
|
||||
};
|
||||
|
||||
const room = (state = initialState, action) =>
|
||||
{
|
||||
switch (action.type)
|
||||
{
|
||||
case 'SET_ROOM_URL':
|
||||
{
|
||||
const { url } = action.payload;
|
||||
|
||||
return { ...state, url };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_STATE':
|
||||
{
|
||||
const roomState = action.payload.state;
|
||||
|
||||
if (roomState == 'connected')
|
||||
return { ...state, state: roomState };
|
||||
else
|
||||
return { ...state, state: roomState, activeSpeakerName: null };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_LOCKED':
|
||||
{
|
||||
return { ...state, locked: true };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_UNLOCKED':
|
||||
{
|
||||
return { ...state, locked: false };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_LOCKED_OUT':
|
||||
{
|
||||
return { ...state, lockedOut: true };
|
||||
}
|
||||
|
||||
case 'SET_AUDIO_SUSPENDED':
|
||||
{
|
||||
const { audioSuspended } = action.payload;
|
||||
|
||||
return { ...state, audioSuspended };
|
||||
}
|
||||
|
||||
case 'SET_ROOM_ACTIVE_SPEAKER':
|
||||
{
|
||||
const { peerName } = action.payload;
|
||||
|
||||
return { ...state, activeSpeakerName: peerName };
|
||||
}
|
||||
|
||||
case 'FILE_SHARING_SUPPORTED':
|
||||
{
|
||||
const { supported } = action.payload;
|
||||
|
||||
return { ...state, torrentSupport: supported };
|
||||
}
|
||||
|
||||
case 'TOGGLE_SETTINGS':
|
||||
{
|
||||
const showSettings = !state.showSettings;
|
||||
|
||||
return { ...state, showSettings };
|
||||
}
|
||||
|
||||
case 'TOGGLE_ADVANCED_MODE':
|
||||
{
|
||||
const advancedMode = !state.advancedMode;
|
||||
|
||||
return { ...state, advancedMode };
|
||||
}
|
||||
|
||||
case 'TOGGLE_FULLSCREEN_CONSUMER':
|
||||
{
|
||||
const { consumerId } = action.payload;
|
||||
const currentConsumer = state.fullScreenConsumer;
|
||||
|
||||
return { ...state, fullScreenConsumer: currentConsumer ? null : consumerId };
|
||||
}
|
||||
|
||||
case 'TOGGLE_WINDOW_CONSUMER':
|
||||
{
|
||||
const { consumerId } = action.payload;
|
||||
const currentConsumer = state.windowConsumer;
|
||||
|
||||
if (currentConsumer === consumerId)
|
||||
return { ...state, windowConsumer: null };
|
||||
else
|
||||
return { ...state, windowConsumer: consumerId };
|
||||
}
|
||||
|
||||
case 'SET_TOOLBARS_VISIBLE':
|
||||
{
|
||||
const { toolbarsVisible } = action.payload;
|
||||
|
||||
return { ...state, toolbarsVisible };
|
||||
}
|
||||
|
||||
case 'SET_DISPLAY_MODE':
|
||||
return { ...state, mode: action.payload.mode };
|
||||
|
||||
case 'SET_SELECTED_PEER':
|
||||
{
|
||||
const { selectedPeerName } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
selectedPeerName : state.selectedPeerName === selectedPeerName ?
|
||||
null : selectedPeerName
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_SPOTLIGHTS':
|
||||
{
|
||||
const { spotlights } = action.payload;
|
||||
|
||||
return { ...state, spotlights };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default room;
|
||||
|
|
@ -14,7 +14,7 @@ const toolarea = (state = initialState, action) =>
|
|||
{
|
||||
const toolAreaOpen = !state.toolAreaOpen;
|
||||
const unreadMessages = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||
const unreadFiles = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadFiles;
|
||||
const unreadFiles = toolAreaOpen && state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||
|
||||
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ const toolarea = (state = initialState, action) =>
|
|||
{
|
||||
const toolAreaOpen = true;
|
||||
const unreadMessages = state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
|
||||
const unreadFiles = state.currentToolTab === 'chat' ? 0 : state.unreadFiles;
|
||||
const unreadFiles = state.currentToolTab === 'files' ? 0 : state.unreadFiles;
|
||||
|
||||
return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
|
@ -39,12 +39,12 @@ const toolarea = (state = initialState, action) =>
|
|||
{
|
||||
const { toolTab } = action.payload;
|
||||
const unreadMessages = toolTab === 'chat' ? 0 : state.unreadMessages;
|
||||
const unreadFiles = toolTab === 'chat' ? 0 : state.unreadFiles;
|
||||
const unreadFiles = toolTab === 'files' ? 0 : state.unreadFiles;
|
||||
|
||||
return { ...state, currentToolTab: toolTab, unreadMessages, unreadFiles };
|
||||
}
|
||||
|
||||
case 'ADD_MESSAGE':
|
||||
case 'ADD_NEW_RESPONSE_MESSAGE':
|
||||
{
|
||||
if (state.toolAreaOpen && state.currentToolTab === 'chat')
|
||||
{
|
||||
|
|
@ -56,7 +56,7 @@ const toolarea = (state = initialState, action) =>
|
|||
|
||||
case 'ADD_FILE':
|
||||
{
|
||||
if (state.toolAreaOpen && state.currentToolTab === 'chat')
|
||||
if (state.toolAreaOpen && state.currentToolTab === 'files')
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import randomString from 'random-string';
|
||||
import * as notificationActions from './notificationActions';
|
||||
import * as stateActions from './stateActions';
|
||||
|
||||
// This returns a redux-thunk action (a function).
|
||||
export const notify = ({ type = 'info', text, timeout }) =>
|
||||
|
|
@ -14,9 +14,6 @@ export const notify = ({ type = 'info', text, timeout }) =>
|
|||
case 'error':
|
||||
timeout = 5000;
|
||||
break;
|
||||
default:
|
||||
timeout = 3000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -30,11 +27,11 @@ export const notify = ({ type = 'info', text, timeout }) =>
|
|||
|
||||
return (dispatch) =>
|
||||
{
|
||||
dispatch(notificationActions.addNotification(notification));
|
||||
dispatch(stateActions.addNotification(notification));
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
dispatch(notificationActions.removeNotification(notification.id));
|
||||
dispatch(stateActions.removeNotification(notification.id));
|
||||
}, timeout);
|
||||
};
|
||||
};
|
||||
570
app/lib/redux/stateActions.js
Normal file
|
|
@ -0,0 +1,570 @@
|
|||
export const setRoomUrl = (url) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_URL',
|
||||
payload : { url }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomState = (state) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_STATE',
|
||||
payload : { state }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomActiveSpeaker = (peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_ACTIVE_SPEAKER',
|
||||
payload : { peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomLocked = () =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_LOCKED'
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomUnLocked = () =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_UNLOCKED'
|
||||
};
|
||||
};
|
||||
|
||||
export const setRoomLockedOut = () =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ROOM_LOCKED_OUT'
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioSuspended = ({ audioSuspended }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_SUSPENDED',
|
||||
payload : { audioSuspended }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMe = ({ peerName, displayName, displayNameSet, device, loginEnabled }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_ME',
|
||||
payload : { peerName, displayName, displayNameSet, device, loginEnabled }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMediaCapabilities = ({ canSendMic, canSendWebcam }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MEDIA_CAPABILITIES',
|
||||
payload : { canSendMic, canSendWebcam }
|
||||
};
|
||||
};
|
||||
|
||||
export const setScreenCapabilities = ({ canShareScreen, needExtension }) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_SCREEN_CAPABILITIES',
|
||||
payload : { canShareScreen, needExtension }
|
||||
};
|
||||
};
|
||||
|
||||
export const setCanChangeAudioDevice = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CAN_CHANGE_AUDIO_DEVICE',
|
||||
payload : flag
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioDevices = (devices) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_DEVICES',
|
||||
payload : { devices }
|
||||
};
|
||||
};
|
||||
|
||||
export const setWebcamDevices = (devices) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_WEBCAM_DEVICES',
|
||||
payload : { devices }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileSharingSupported = (supported) =>
|
||||
{
|
||||
return {
|
||||
type : 'FILE_SHARING_SUPPORTED',
|
||||
payload : { supported }
|
||||
};
|
||||
};
|
||||
|
||||
export const setDisplayName = (displayName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_DISPLAY_NAME',
|
||||
payload : { displayName }
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleAdvancedMode = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_ADVANCED_MODE'
|
||||
};
|
||||
};
|
||||
|
||||
export const setDisplayMode = (mode) =>
|
||||
({
|
||||
type : 'SET_DISPLAY_MODE',
|
||||
payload : { mode }
|
||||
});
|
||||
|
||||
export const setAudioOnlyState = (enabled) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_ONLY_STATE',
|
||||
payload : { enabled }
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioOnlyInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_ONLY_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerVideoInProgress = (peerName, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_VIDEO_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerAudioInProgress = (peerName, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_AUDIO_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerScreenInProgress = (peerName, flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_SCREEN_IN_PROGRESS',
|
||||
payload : { peerName, flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMyRaiseHandState = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MY_RAISE_HAND_STATE',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleSettings = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_SETTINGS'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleToolArea = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_TOOL_AREA'
|
||||
};
|
||||
};
|
||||
|
||||
export const openToolArea = () =>
|
||||
{
|
||||
return {
|
||||
type : 'OPEN_TOOL_AREA'
|
||||
};
|
||||
};
|
||||
|
||||
export const closeToolArea = () =>
|
||||
{
|
||||
return {
|
||||
type : 'CLOSE_TOOL_AREA'
|
||||
};
|
||||
};
|
||||
|
||||
export const setToolTab = (toolTab) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_TOOL_TAB',
|
||||
payload : { toolTab }
|
||||
};
|
||||
};
|
||||
|
||||
export const setMyRaiseHandStateInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_MY_RAISE_HAND_STATE_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerRaiseHandState = (peerName, raiseHandState) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_RAISE_HAND_STATE',
|
||||
payload : { peerName, raiseHandState }
|
||||
};
|
||||
};
|
||||
|
||||
export const setRestartIceInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_RESTART_ICE_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const addProducer = (producer) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_PRODUCER',
|
||||
payload : { producer }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeProducer = (producerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_PRODUCER',
|
||||
payload : { producerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerPaused = (producerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_PAUSED',
|
||||
payload : { producerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerResumed = (producerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_RESUMED',
|
||||
payload : { producerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerTrack = (producerId, track) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_TRACK',
|
||||
payload : { producerId, track }
|
||||
};
|
||||
};
|
||||
|
||||
export const setAudioInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_AUDIO_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setWebcamInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_WEBCAM_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const setScreenShareInProgress = (flag) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_SCREEN_SHARE_IN_PROGRESS',
|
||||
payload : { flag }
|
||||
};
|
||||
};
|
||||
|
||||
export const addPeer = (peer) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_PEER',
|
||||
payload : { peer }
|
||||
};
|
||||
};
|
||||
|
||||
export const removePeer = (peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_PEER',
|
||||
payload : { peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPeerDisplayName = (displayName, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PEER_DISPLAY_NAME',
|
||||
payload : { displayName, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const addConsumer = (consumer, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_CONSUMER',
|
||||
payload : { consumer, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeConsumer = (consumerId, peerName) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_CONSUMER',
|
||||
payload : { consumerId, peerName }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerPaused = (consumerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_PAUSED',
|
||||
payload : { consumerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerResumed = (consumerId, originator) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_RESUMED',
|
||||
payload : { consumerId, originator }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerEffectiveProfile = (consumerId, profile) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_EFFECTIVE_PROFILE',
|
||||
payload : { consumerId, profile }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerTrack = (consumerId, track) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_TRACK',
|
||||
payload : { consumerId, track }
|
||||
};
|
||||
};
|
||||
|
||||
export const setConsumerVolume = (consumerId, volume) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_CONSUMER_VOLUME',
|
||||
payload : { consumerId, volume }
|
||||
};
|
||||
};
|
||||
|
||||
export const setProducerVolume = (producerId, volume) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_PRODUCER_VOLUME',
|
||||
payload : { producerId, volume }
|
||||
};
|
||||
};
|
||||
|
||||
export const addNotification = (notification) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NOTIFICATION',
|
||||
payload : { notification }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeNotification = (notificationId) =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_NOTIFICATION',
|
||||
payload : { notificationId }
|
||||
};
|
||||
};
|
||||
|
||||
export const removeAllNotifications = () =>
|
||||
{
|
||||
return {
|
||||
type : 'REMOVE_ALL_NOTIFICATIONS'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleChat = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_CHAT'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleConsumerFullscreen = (consumerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_FULLSCREEN_CONSUMER',
|
||||
payload : { consumerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleConsumerWindow = (consumerId) =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_WINDOW_CONSUMER',
|
||||
payload : { consumerId }
|
||||
};
|
||||
};
|
||||
|
||||
export const setToolbarsVisible = (toolbarsVisible) => ({
|
||||
type : 'SET_TOOLBARS_VISIBLE',
|
||||
payload : { toolbarsVisible }
|
||||
});
|
||||
|
||||
export const increaseBadge = () =>
|
||||
{
|
||||
return {
|
||||
type : 'INCREASE_BADGE'
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleInputDisabled = () =>
|
||||
{
|
||||
return {
|
||||
type : 'TOGGLE_INPUT_DISABLED'
|
||||
};
|
||||
};
|
||||
|
||||
export const addUserMessage = (text) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NEW_USER_MESSAGE',
|
||||
payload : { text }
|
||||
};
|
||||
};
|
||||
|
||||
export const addUserFile = (file) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NEW_USER_FILE',
|
||||
payload : { file }
|
||||
};
|
||||
};
|
||||
|
||||
export const addResponseMessage = (message) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_NEW_RESPONSE_MESSAGE',
|
||||
payload : { message }
|
||||
};
|
||||
};
|
||||
|
||||
export const addChatHistory = (chatHistory) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_CHAT_HISTORY',
|
||||
payload : { chatHistory }
|
||||
};
|
||||
};
|
||||
|
||||
export const dropMessages = () =>
|
||||
{
|
||||
return {
|
||||
type : 'DROP_MESSAGES'
|
||||
};
|
||||
};
|
||||
|
||||
export const addFile = (file) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_FILE',
|
||||
payload : { file }
|
||||
};
|
||||
};
|
||||
|
||||
export const addFileHistory = (fileHistory) =>
|
||||
{
|
||||
return {
|
||||
type : 'ADD_FILE_HISTORY',
|
||||
payload : { fileHistory }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileActive = (magnetUri) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_ACTIVE',
|
||||
payload : { magnetUri }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileInActive = (magnetUri) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_INACTIVE',
|
||||
payload : { magnetUri }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileProgress = (magnetUri, progress) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_PROGRESS',
|
||||
payload : { magnetUri, progress }
|
||||
};
|
||||
};
|
||||
|
||||
export const setFileDone = (magnetUri, sharedFiles) =>
|
||||
{
|
||||
return {
|
||||
type : 'SET_FILE_DONE',
|
||||
payload : { magnetUri, sharedFiles }
|
||||
};
|
||||
};
|
||||
|
||||
export const setPicture = (picture) =>
|
||||
({
|
||||
type : 'SET_PICTURE',
|
||||
payload : { picture }
|
||||
});
|
||||
|
||||
export const setPeerPicture = (peerName, picture) =>
|
||||
({
|
||||
type : 'SET_PEER_PICTURE',
|
||||
payload : { peerName, picture }
|
||||
});
|
||||
|
||||
export const loggedIn = () =>
|
||||
({
|
||||
type : 'LOGGED_IN'
|
||||
});
|
||||
|
||||
export const setSelectedPeer = (selectedPeerName) =>
|
||||
({
|
||||
type : 'SET_SELECTED_PEER',
|
||||
payload : { selectedPeerName }
|
||||
});
|
||||
|
||||
export const setSpotlights = (spotlights) =>
|
||||
({
|
||||
type : 'SET_SPOTLIGHTS',
|
||||
payload : { spotlights }
|
||||
});
|
||||
47
app/lib/store.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
applyMiddleware,
|
||||
createStore,
|
||||
compose
|
||||
} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import reducers from './redux/reducers';
|
||||
|
||||
const reduxMiddlewares =
|
||||
[
|
||||
thunk
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development')
|
||||
{
|
||||
const reduxLogger = createLogger(
|
||||
{
|
||||
// filter VOLUME level actions from log
|
||||
predicate : (getState, action) => ! (action.type == 'SET_PRODUCER_VOLUME'
|
||||
|| action.type == 'SET_CONSUMER_VOLUME'),
|
||||
duration : true,
|
||||
timestamp : false,
|
||||
level : 'log',
|
||||
logErrors : true
|
||||
});
|
||||
|
||||
reduxMiddlewares.push(reduxLogger);
|
||||
}
|
||||
|
||||
const composeEnhancers =
|
||||
typeof window === 'object' &&
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
|
||||
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
||||
}) : compose;
|
||||
|
||||
const enhancer = composeEnhancers(
|
||||
applyMiddleware(...reduxMiddlewares)
|
||||
// other store enhancers if any
|
||||
);
|
||||
|
||||
export const store = createStore(
|
||||
reducers,
|
||||
undefined,
|
||||
enhancer
|
||||
);
|
||||
8
app/lib/urlFactory.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export function getSignalingUrl(peerName, roomId)
|
||||
{
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
const url = `wss://${hostname}:${port}/?peerName=${peerName}&roomId=${roomId}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
40
app/lib/utils.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
let mediaQueryDetectorElem;
|
||||
|
||||
export function initialize()
|
||||
{
|
||||
// Media query detector stuff.
|
||||
mediaQueryDetectorElem =
|
||||
document.getElementById('multiparty-meeting-media-query-detector');
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export function isDesktop()
|
||||
{
|
||||
return Boolean(mediaQueryDetectorElem.offsetParent);
|
||||
}
|
||||
|
||||
export function isMobile()
|
||||
{
|
||||
return !mediaQueryDetectorElem.offsetParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a function which will call the callback function
|
||||
* after the given amount of milliseconds has passed since
|
||||
* the last time the callback function was called.
|
||||
*/
|
||||
export const idle = (callback, delay) =>
|
||||
{
|
||||
let handle;
|
||||
|
||||
return () =>
|
||||
{
|
||||
if (handle)
|
||||
{
|
||||
clearTimeout(handle);
|
||||
}
|
||||
|
||||
handle = setTimeout(callback, delay);
|
||||
};
|
||||
};
|
||||
194
app/package.json
|
|
@ -1,116 +1,82 @@
|
|||
{
|
||||
"name": "edumeet",
|
||||
"version": "3.5.5",
|
||||
"private": true,
|
||||
"description": "edumeet meeting service",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
"contributors": [
|
||||
"Stefan Otto",
|
||||
"Mészáros Mihály",
|
||||
"Roman Drozd",
|
||||
"Rémai Gábor László",
|
||||
"Piotr Pawałowski"
|
||||
],
|
||||
"license": "MIT",
|
||||
"homepage": "./",
|
||||
"main": "src/electron-starter.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@react-hook/window-size": "^3.0.7",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"bowser": "^2.7.0",
|
||||
"chroma-js": "^2.1.1",
|
||||
"classnames": "^2.2.6",
|
||||
"convict": "^6.0.1",
|
||||
"convict-format-with-validator": "^6.0.1",
|
||||
"create-torrent": "^4.4.1",
|
||||
"deep-object-diff": "^1.1.0",
|
||||
"dompurify": "^2.0.7",
|
||||
"domready": "^1.0.8",
|
||||
"draft-js": "^0.11.7",
|
||||
"draft-js-export-html": "^1.4.1",
|
||||
"draft-js-plugins-editor": "^3.0.0",
|
||||
"draft-js-single-line-plugin": "^2.0.5",
|
||||
"end-of-stream": "1.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"hark": "^1.2.3",
|
||||
"idb": "^6.0.0",
|
||||
"is-electron": "^2.2.0",
|
||||
"marked": "^0.8.0",
|
||||
"material-ui-popup-state": "^1.8.0",
|
||||
"mediasoup-client": "^3.6.55",
|
||||
"notistack": "^0.9.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^16.10.2",
|
||||
"react-cookie-consent": "^2.5.0",
|
||||
"react-dom": "^16.10.2",
|
||||
"react-flip-toolkit": "^7.0.9",
|
||||
"react-image-file-resizer": "^0.3.8",
|
||||
"react-images-upload": "^1.2.0",
|
||||
"react-intl": "^3.4.0",
|
||||
"react-intl-redux": "^2.2.0",
|
||||
"react-redux": "^7.2.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-wakelock-react16": "0.0.7",
|
||||
"redux": "^4.0.4",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-persist-transform-filter": "0.0.20",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"riek": "^1.1.0",
|
||||
"socket.io-client": "^2.4.0",
|
||||
"source-map-explorer": "^2.1.0",
|
||||
"streamsaver": "^2.0.5",
|
||||
"typescript": "^4.2.4",
|
||||
"universal-cookie": "4.0.4",
|
||||
"web-streams-polyfill": "^3.0.2",
|
||||
"webtorrent": "^0.108.1"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer build/static/js/*",
|
||||
"start": "HTTPS=true PORT=4443 react-scripts start",
|
||||
"build": "react-scripts build && rm -rf ../server/public && DEST='../server/dist/public' && rm -rf $DEST && mkdir -p $DEST && mv -T build/ $DEST",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"electron": "electron --no-sandbox .",
|
||||
"dev": "nf start -p 3000",
|
||||
"lint": "eslint ./ --ext .js,.jsx,.ts,.tsx; exit 0",
|
||||
"lint-fix": "eslint ./ --fix --ext .js,.jsx,.ts,.tsx; exit 0",
|
||||
"gen-config-docs": "node --loader ts-node/esm src/config.ts && eslint -c .eslintrc.json public/config/config.example.js --fix"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie > 0",
|
||||
"not op_mini all"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/convict": "^6.0.1",
|
||||
"@types/convict-format-with-validator": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.20.0",
|
||||
"@typescript-eslint/parser": "^4.20.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"electron": "^12.0.0",
|
||||
"eslint-config-react-app": "^6.0.0",
|
||||
"eslint-import-resolver-typescript": "^2.4.0",
|
||||
"eslint-plugin-flowtype": "^5.4.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.3.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.23.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-webpack-plugin": "^2.5.3",
|
||||
"foreman": "^3.0.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
"name": "multiparty-meeting",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"description": "multiparty meeting service",
|
||||
"author": "Håvar Aambø Fosstveit <h@fosstveit.net>",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.jsx",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"classnames": "^2.2.6",
|
||||
"create-torrent": "^3.32.1",
|
||||
"debug": "^4.1.0",
|
||||
"domready": "^1.0.8",
|
||||
"drag-drop": "^4.2.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"hark": "^1.2.2",
|
||||
"js-cookie": "^2.2.0",
|
||||
"magnet-uri": "^5.2.3",
|
||||
"marked": "^0.5.1",
|
||||
"mediasoup-client": "^2.4.9",
|
||||
"prop-types": "^15.6.2",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^16.5.2",
|
||||
"react-cookie-consent": "^1.9.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-draggable": "^3.0.5",
|
||||
"react-dropdown": "^1.5.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-spinner": "^0.2.7",
|
||||
"react-tooltip": "^3.9.0",
|
||||
"react-transition-group": "^2.5.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"riek": "^1.1.0",
|
||||
"socket.io-client": "^2.1.1",
|
||||
"url-parse": "^1.4.3",
|
||||
"webtorrent": "^0.102.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
|
||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-plugin-jsx-control-statements": "^3.2.8",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babelify": "^10.0.0",
|
||||
"browser-sync": "^2.26.3",
|
||||
"browserify": "^16.2.3",
|
||||
"del": "^3.0.0",
|
||||
"envify": "^4.1.0",
|
||||
"eslint": "^5.7.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jsx-control-statements": "^2.2.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-change": "^1.0.0",
|
||||
"gulp-css-base64": "^1.3.4",
|
||||
"gulp-eslint": "^5.0.0",
|
||||
"gulp-header": "^2.0.5",
|
||||
"gulp-if": "^2.0.2",
|
||||
"gulp-plumber": "^1.2.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-stylus": "^2.7.0",
|
||||
"gulp-touch-cmd": "0.0.1",
|
||||
"gulp-uglify-es": "^1.0.4",
|
||||
"gulp-util": "^3.0.8",
|
||||
"lodash": "^4.17.10",
|
||||
"mkdirp": "^0.5.1",
|
||||
"ncp": "^2.0.0",
|
||||
"nib": "^1.1.2",
|
||||
"supports-color": "^5.5.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"watchify": "^3.11.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
#  App Configuration properties list:
|
||||
|
||||
| Name | Description | Format | Default value |
|
||||
| :--- | :---------- | :----- | :------------ |
|
||||
| loginEnabled | If the login is enabled. | `"boolean"` | ``false`` |
|
||||
| developmentPort | The development server listening port. | `"port"` | ``3443`` |
|
||||
| productionPort | The production server listening port. | `"port"` | ``443`` |
|
||||
| serverHostname | If the server component runs on a different host than the app you can specify the host name. | `"string"` | ``""`` |
|
||||
| supportedBrowsers | Supported browsers version in bowser satisfy format. | `"object"` | ``{ "windows": { "internet explorer": ">12", "microsoft edge": ">18" }, "microsoft edge": ">18", "safari": ">12", "firefox": ">=60", "chrome": ">=74", "chromium": ">=74", "opera": ">=62", "samsung internet for android": ">=11.1.1.52"}`` |
|
||||
| networkPriorities | Network priorities. | `"object"` | ``{ "audio": "high", "mainVideo": "high", "additionalVideos": "medium", "screenShare": "medium"}`` |
|
||||
| aspectRatio | The aspect ratio of the videos as shown on the screen. This value must match exactly one of the values defined in aspectRatios. | `"float"` | ``1.777`` |
|
||||
| aspectRatios | The selectable aspect ratios in the user settings. | `"array"` | ``[ { "value": 1.333, "label": "4 : 3" }, { "value": 1.777, "label": "16 : 9" }]`` |
|
||||
| resolution | The default video camera capture resolution. | `[ "low", "medium", "high", "veryhigh", "ultra"]` | ``"medium"`` |
|
||||
| frameRate | The default video camera capture framerate. | `"nat"` | ``15`` |
|
||||
| screenResolution | The default screen sharing resolution. | `[ "low", "medium", "high", "veryhigh", "ultra"]` | ``"veryhigh"`` |
|
||||
| screenSharingFrameRate | The default screen sharing framerate. | `"nat"` | ``5`` |
|
||||
| simulcast | Enable or disable simulcast for webcam video. | `"boolean"` | ``true`` |
|
||||
| simulcastSharing | Enable or disable simulcast for screen sharing video. | `"boolean"` | ``false`` |
|
||||
| simulcastProfiles | Define different encodings for various resolutions of the video. | `"object"` | ``{ "320": [ { "scaleResolutionDownBy": 1, "maxBitRate": 150000 } ], "640": [ { "scaleResolutionDownBy": 2, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 500000 } ], "1280": [ { "scaleResolutionDownBy": 4, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 2, "maxBitRate": 500000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 1200000 } ], "1920": [ { "scaleResolutionDownBy": 6, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 3, "maxBitRate": 500000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 3500000 } ], "3840": [ { "scaleResolutionDownBy": 12, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 6, "maxBitRate": 500000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 10000000 } ]}`` |
|
||||
| adaptiveScalingFactor | The adaptive spatial layer selection scaling factor in the range [0.5, 1.0]. | | ``0.75`` |
|
||||
| localRecordingEnabled | If set to true Local Recording feature will be enabled. | `"boolean"` | ``false`` |
|
||||
| audioOutputSupportedBrowsers | White listing browsers that support audio output device selection. | `"array"` | ``[ "chrome", "opera"]`` |
|
||||
| requestTimeout | The Socket.io request timeout. | `"nat"` | ``20000`` |
|
||||
| requestRetries | The Socket.io request maximum retries. | `"nat"` | ``3`` |
|
||||
| transportOptions | The Mediasoup transport options. | `"object"` | ``{ "tcp": true}`` |
|
||||
| autoGainControl | Auto gain control enabled. | `"boolean"` | ``true`` |
|
||||
| echoCancellation | Echo cancellation enabled. | `"boolean"` | ``true`` |
|
||||
| noiseSuppression | Noise suppression enabled. | `"boolean"` | ``true`` |
|
||||
| voiceActivatedUnmute | Automatically unmute speaking above noiseThreshold. | `"boolean"` | ``false`` |
|
||||
| noiseThreshold | This is only for voiceActivatedUnmute and audio-indicator. | `"int"` | ``-60`` |
|
||||
| sampleRate | The audio sample rate. | `[ 8000, 16000, 24000, 44100, 48000]` | ``48000`` |
|
||||
| channelCount | The audio channels count. | `[ 1, 2]` | ``1`` |
|
||||
| sampleSize | The audio sample size count. | `[ 8, 16, 24, 32]` | ``16`` |
|
||||
| opusStereo | If OPUS FEC stereo be enabled. | `"boolean"` | ``false`` |
|
||||
| opusDtx | If OPUS DTX should be enabled. | `"boolean"` | ``true`` |
|
||||
| opusFec | If OPUS FEC should be enabled. | `"boolean"` | ``true`` |
|
||||
| opusPtime | The OPUS packet time. | `[ 3, 5, 10, 20, 30, 40, 50, 60]` | ``20`` |
|
||||
| opusMaxPlaybackRate | The OPUS playback rate. | `[ 8000, 16000, 24000, 44100, 48000]` | ``48000`` |
|
||||
| audioPreset | The audio preset | `"string"` | ``"conference"`` |
|
||||
| audioPresets | The audio presets. | `"object"` | ``{ "conference": { "name": "Conference audio", "autoGainControl": true, "echoCancellation": true, "noiseSuppression": true, "voiceActivatedUnmute": false, "noiseThreshold": -60, "sampleRate": 48000, "channelCount": 1, "sampleSize": 16, "opusStereo": false, "opusDtx": true, "opusFec": true, "opusPtime": 20, "opusMaxPlaybackRate": 48000 }, "hifi": { "name": "HiFi streaming", "autoGainControl": false, "echoCancellation": false, "noiseSuppression": false, "voiceActivatedUnmute": false, "noiseThreshold": -60, "sampleRate": 48000, "channelCount": 2, "sampleSize": 16, "opusStereo": true, "opusDtx": false, "opusFec": true, "opusPtime": 60, "opusMaxPlaybackRate": 48000 }}`` |
|
||||
| autoMuteThreshold | It sets the maximum number of participants in one room that can join unmuted. The next participant will join automatically muted. Set it to 0 to auto mute all. Set it to negative (-1) to never automatically auto mute but use it with caution, full mesh audio strongly decrease room capacity! | `"nat"` | ``4`` |
|
||||
| background | The page background image URL | `"string"` | ``"images/background.jpg"`` |
|
||||
| defaultLayout | The default layout. | `[ "democratic", "filmstrip"]` | ``"democratic"`` |
|
||||
| buttonControlBar | If true, the media control buttons will be shown in separate control bar, not in the ME container. | `"boolean"` | ``false`` |
|
||||
| drawerOverlayed | If false, will push videos away to make room for side drawer. If true, will overlay side drawer over videos. | `"boolean"` | ``true`` |
|
||||
| notificationPosition | The position of the notifications. | `[ "left", "right"]` | ``"right"`` |
|
||||
| notificationSounds | It sets the notifications sounds. Valid keys are: 'parkedPeer', 'parkedPeers', 'raisedHand', 'chatMessage', 'sendFile', 'newPeer' and 'default'. Not defining a key is equivalent to using the default notification sound. Setting 'play' to null disables the sound notification. | `"object"` | ``{ "chatMessage": { "play": "/sounds/notify-chat.mp3" }, "raisedHand": { "play": "/sounds/notify-hand.mp3" }, "default": { "delay": 5000, "play": "/sounds/notify.mp3" }}`` |
|
||||
| hideTimeout | Timeout for auto hiding the topbar and the buttons control bar. | `"int"` | ``3000`` |
|
||||
| lastN | The maximum number of participants that will be visible in as speaker. | `"nat"` | ``4`` |
|
||||
| mobileLastN | The maximum number of participants that will be visible in as speaker for mobile users. | `"nat"` | ``1`` |
|
||||
| maxLastN | The highest number of lastN the user can select manually in the user interface. | `"nat"` | ``5`` |
|
||||
| lockLastN | If true, the users can not change the number of visible speakers. | `"boolean"` | ``false`` |
|
||||
| logo | If not null, it shows the logo loaded from the specified URL, otherwise it shows the title. | `"url"` | ``"images/logo.edumeet.svg"`` |
|
||||
| title | The title to show if the logo is not specified. | `"string"` | ``"edumeet"`` |
|
||||
| infoTooltipText | Informative text on the join page. If empty hidden. | `"string"` | ``""`` |
|
||||
| infoTooltipLink | Informative text link site on the join page. If empty hidden. | `"string"` | ``""`` |
|
||||
| infoTooltipDesc | Informative text description on the join page. If empty hidden. | `"string"` | ``""`` |
|
||||
| supportUrl | The service & Support URL; if `null`, it will be not displayed on the about dialogs. | `"url"` | ``"https://support.example.com"`` |
|
||||
| privacyUrl | The privacy and data protection external URL or local HTML path. | `"string"` | ``"privacy/privacy.html"`` |
|
||||
| theme | UI theme elements colors. | `"object"` | ``{ "palette": { "primary": { "main": "#313131" } }, "overrides": { "MuiAppBar": { "colorPrimary": { "backgroundColor": "#313131" } }, "MuiButton": { "containedPrimary": { "backgroundColor": "#5F9B2D", "&:hover": { "backgroundColor": "#5F9B2D" } }, "containedSecondary": { "backgroundColor": "#f50057", "&:hover": { "backgroundColor": "#f50057" } } }, "MuiFab": { "primary": { "backgroundColor": "#518029", "&:hover": { "backgroundColor": "#518029" }, "&:disabled": { "color": "#999898", "backgroundColor": "#323131" } }, "secondary": { "backgroundColor": "#f50057", "&:hover": { "backgroundColor": "#f50057" }, "&:disabled": { "color": "#999898", "backgroundColor": "#323131" } } }, "MuiBadge": { "colorPrimary": { "backgroundColor": "#5F9B2D", "&:hover": { "backgroundColor": "#518029" } } } }, "typography": { "useNextVariants": true }}`` |
|
||||
|
||||
|
||||
---
|
||||
|
||||
*Document generated with:* `yarn gen-config-docs` *from:* [config.ts](src/config.ts).
|
||||
|
|
@ -1,386 +0,0 @@
|
|||
/**
|
||||
* Edumeet App Configuration
|
||||
*
|
||||
* The configuration documentation is available also:
|
||||
* - in the app/README.md file in the source tree
|
||||
* - visiting the /?config=true page in a running instance
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line
|
||||
var config = {
|
||||
|
||||
// If the login is enabled.
|
||||
loginEnabled : false,
|
||||
|
||||
// The development server listening port.
|
||||
developmentPort : 3443,
|
||||
|
||||
// The production server listening port.
|
||||
productionPort : 443,
|
||||
|
||||
// If the server component runs on a different host than the app you can specify the host name.
|
||||
serverHostname : '',
|
||||
|
||||
// Supported browsers version in bowser satisfy format.
|
||||
supportedBrowsers : {
|
||||
'windows' : {
|
||||
'internet explorer' : '>12',
|
||||
'microsoft edge' : '>18'
|
||||
},
|
||||
'microsoft edge' : '>18',
|
||||
'safari' : '>12',
|
||||
'firefox' : '>=60',
|
||||
'chrome' : '>=74',
|
||||
'chromium' : '>=74',
|
||||
'opera' : '>=62',
|
||||
'samsung internet for android' : '>=11.1.1.52'
|
||||
},
|
||||
|
||||
// Network priorities.
|
||||
networkPriorities : {
|
||||
'audio' : 'high',
|
||||
'mainVideo' : 'high',
|
||||
'additionalVideos' : 'medium',
|
||||
'screenShare' : 'medium'
|
||||
},
|
||||
|
||||
// The aspect ratio of the videos as shown on the screen.
|
||||
// This value must match exactly one of the values defined in aspectRatios.
|
||||
aspectRatio : 1.777,
|
||||
|
||||
// The selectable aspect ratios in the user settings.
|
||||
aspectRatios : [
|
||||
{
|
||||
'value' : 1.333,
|
||||
'label' : '4 : 3'
|
||||
},
|
||||
{
|
||||
'value' : 1.777,
|
||||
'label' : '16 : 9'
|
||||
}
|
||||
],
|
||||
|
||||
// The default video camera capture resolution.
|
||||
resolution : 'medium',
|
||||
|
||||
// The default video camera capture framerate.
|
||||
frameRate : 15,
|
||||
|
||||
// The default screen sharing resolution.
|
||||
screenResolution : 'veryhigh',
|
||||
|
||||
// The default screen sharing framerate.
|
||||
screenSharingFrameRate : 5,
|
||||
|
||||
// Enable or disable simulcast for webcam video.
|
||||
simulcast : true,
|
||||
|
||||
// Enable or disable simulcast for screen sharing video.
|
||||
simulcastSharing : false,
|
||||
|
||||
// Define different encodings for various resolutions of the video.
|
||||
simulcastProfiles : {
|
||||
'320' : [
|
||||
{
|
||||
'scaleResolutionDownBy' : 1,
|
||||
'maxBitRate' : 150000
|
||||
}
|
||||
],
|
||||
'640' : [
|
||||
{
|
||||
'scaleResolutionDownBy' : 2,
|
||||
'maxBitRate' : 150000
|
||||
},
|
||||
{
|
||||
'scaleResolutionDownBy' : 1,
|
||||
'maxBitRate' : 500000
|
||||
}
|
||||
],
|
||||
'1280' : [
|
||||
{
|
||||
'scaleResolutionDownBy' : 4,
|
||||
'maxBitRate' : 150000
|
||||
},
|
||||
{
|
||||
'scaleResolutionDownBy' : 2,
|
||||
'maxBitRate' : 500000
|
||||
},
|
||||
{
|
||||
'scaleResolutionDownBy' : 1,
|
||||
'maxBitRate' : 1200000
|
||||
}
|
||||
],
|
||||
'1920' : [
|
||||
{
|
||||
'scaleResolutionDownBy' : 6,
|
||||
'maxBitRate' : 150000
|
||||
},
|
||||
{
|
||||
'scaleResolutionDownBy' : 3,
|
||||
'maxBitRate' : 500000
|
||||
},
|
||||
{
|
||||
'scaleResolutionDownBy' : 1,
|
||||
'maxBitRate' : 3500000
|
||||
}
|
||||
],
|
||||
'3840' : [
|
||||
{
|
||||
'scaleResolutionDownBy' : 12,
|
||||
'maxBitRate' : 150000
|
||||
},
|
||||
{
|
||||
'scaleResolutionDownBy' : 6,
|
||||
'maxBitRate' : 500000
|
||||
},
|
||||
{
|
||||
'scaleResolutionDownBy' : 1,
|
||||
'maxBitRate' : 10000000
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// The adaptive spatial layer selection scaling factor in the range [0.5, 1.0].
|
||||
adaptiveScalingFactor : 0.75,
|
||||
|
||||
// If set to true Local Recording feature will be enabled.
|
||||
localRecordingEnabled : false,
|
||||
|
||||
// White listing browsers that support audio output device selection.
|
||||
audioOutputSupportedBrowsers : [
|
||||
'chrome',
|
||||
'opera'
|
||||
],
|
||||
|
||||
// The Socket.io request timeout.
|
||||
requestTimeout : 20000,
|
||||
|
||||
// The Socket.io request maximum retries.
|
||||
requestRetries : 3,
|
||||
|
||||
// The Mediasoup transport options.
|
||||
transportOptions : {
|
||||
'tcp' : true
|
||||
},
|
||||
|
||||
// Auto gain control enabled.
|
||||
autoGainControl : true,
|
||||
|
||||
// Echo cancellation enabled.
|
||||
echoCancellation : true,
|
||||
|
||||
// Noise suppression enabled.
|
||||
noiseSuppression : true,
|
||||
|
||||
// Automatically unmute speaking above noiseThreshold.
|
||||
voiceActivatedUnmute : false,
|
||||
|
||||
// This is only for voiceActivatedUnmute and audio-indicator.
|
||||
noiseThreshold : -60,
|
||||
|
||||
// The audio sample rate.
|
||||
sampleRate : 48000,
|
||||
|
||||
// The audio channels count.
|
||||
channelCount : 1,
|
||||
|
||||
// The audio sample size count.
|
||||
sampleSize : 16,
|
||||
|
||||
// If OPUS FEC stereo be enabled.
|
||||
opusStereo : false,
|
||||
|
||||
// If OPUS DTX should be enabled.
|
||||
opusDtx : true,
|
||||
|
||||
// If OPUS FEC should be enabled.
|
||||
opusFec : true,
|
||||
|
||||
// The OPUS packet time.
|
||||
opusPtime : 20,
|
||||
|
||||
// The OPUS playback rate.
|
||||
opusMaxPlaybackRate : 48000,
|
||||
|
||||
// The audio preset
|
||||
audioPreset : 'conference',
|
||||
|
||||
// The audio presets.
|
||||
audioPresets : {
|
||||
'conference' : {
|
||||
'name' : 'Conference audio',
|
||||
'autoGainControl' : true,
|
||||
'echoCancellation' : true,
|
||||
'noiseSuppression' : true,
|
||||
'voiceActivatedUnmute' : false,
|
||||
'noiseThreshold' : -60,
|
||||
'sampleRate' : 48000,
|
||||
'channelCount' : 1,
|
||||
'sampleSize' : 16,
|
||||
'opusStereo' : false,
|
||||
'opusDtx' : true,
|
||||
'opusFec' : true,
|
||||
'opusPtime' : 20,
|
||||
'opusMaxPlaybackRate' : 48000
|
||||
},
|
||||
'hifi' : {
|
||||
'name' : 'HiFi streaming',
|
||||
'autoGainControl' : false,
|
||||
'echoCancellation' : false,
|
||||
'noiseSuppression' : false,
|
||||
'voiceActivatedUnmute' : false,
|
||||
'noiseThreshold' : -60,
|
||||
'sampleRate' : 48000,
|
||||
'channelCount' : 2,
|
||||
'sampleSize' : 16,
|
||||
'opusStereo' : true,
|
||||
'opusDtx' : false,
|
||||
'opusFec' : true,
|
||||
'opusPtime' : 60,
|
||||
'opusMaxPlaybackRate' : 48000
|
||||
}
|
||||
},
|
||||
|
||||
// It sets the maximum number of participants in one room that can join unmuted.
|
||||
// The next participant will join automatically muted.
|
||||
// Set it to 0 to auto mute all.
|
||||
// Set it to negative (-1) to never automatically auto mute but use it with caution,
|
||||
// full mesh audio strongly decrease room capacity!
|
||||
autoMuteThreshold : 4,
|
||||
|
||||
// The page background image URL
|
||||
background : 'images/background.jpg',
|
||||
|
||||
// The default layout.
|
||||
defaultLayout : 'democratic',
|
||||
|
||||
// If true, the media control buttons will be shown in separate control bar, not in the ME container.
|
||||
buttonControlBar : false,
|
||||
|
||||
// If false, will push videos away to make room for side drawer.
|
||||
// If true, will overlay side drawer over videos.
|
||||
drawerOverlayed : true,
|
||||
|
||||
// The position of the notifications.
|
||||
notificationPosition : 'right',
|
||||
|
||||
// It sets the notifications sounds.
|
||||
// Valid keys are: 'parkedPeer', 'parkedPeers', 'raisedHand',
|
||||
// 'chatMessage', 'sendFile', 'newPeer' and 'default'.
|
||||
// Not defining a key is equivalent to using the default notification sound.
|
||||
// Setting 'play' to null disables the sound notification.
|
||||
//
|
||||
notificationSounds : {
|
||||
'chatMessage' : {
|
||||
'play' : '/sounds/notify-chat.mp3'
|
||||
},
|
||||
'raisedHand' : {
|
||||
'play' : '/sounds/notify-hand.mp3'
|
||||
},
|
||||
'default' : {
|
||||
'delay' : 5000,
|
||||
'play' : '/sounds/notify.mp3'
|
||||
}
|
||||
},
|
||||
|
||||
// Timeout for auto hiding the topbar and the buttons control bar.
|
||||
hideTimeout : 3000,
|
||||
|
||||
// The maximum number of participants that will be visible in as speaker.
|
||||
lastN : 4,
|
||||
|
||||
// The maximum number of participants that will be visible in as speaker for mobile users.
|
||||
mobileLastN : 1,
|
||||
|
||||
// The highest number of lastN the user can select manually in the user interface.
|
||||
maxLastN : 5,
|
||||
|
||||
// If true, the users can not change the number of visible speakers.
|
||||
lockLastN : false,
|
||||
|
||||
// If not null, it shows the logo loaded from the specified URL, otherwise it shows the title.
|
||||
logo : 'images/logo.edumeet.svg',
|
||||
|
||||
// The title to show if the logo is not specified.
|
||||
title : 'edumeet',
|
||||
|
||||
// Informative text on the join page. If empty hidden.
|
||||
infoTooltipText : '',
|
||||
|
||||
// Informative text link site on the join page. If empty hidden.
|
||||
infoTooltipLink : '',
|
||||
|
||||
// Informative text description on the join page. If empty hidden.
|
||||
infoTooltipDesc : '',
|
||||
|
||||
// The service & Support URL; if `null`, it will be not displayed on the about dialogs.
|
||||
supportUrl : 'https://support.example.com',
|
||||
|
||||
// The privacy and data protection external URL or local HTML path.
|
||||
privacyUrl : 'privacy/privacy.html',
|
||||
|
||||
// UI theme elements colors.
|
||||
theme : {
|
||||
'palette' : {
|
||||
'primary' : {
|
||||
'main' : '#313131'
|
||||
}
|
||||
},
|
||||
'overrides' : {
|
||||
'MuiAppBar' : {
|
||||
'colorPrimary' : {
|
||||
'backgroundColor' : '#313131'
|
||||
}
|
||||
},
|
||||
'MuiButton' : {
|
||||
'containedPrimary' : {
|
||||
'backgroundColor' : '#5F9B2D',
|
||||
'&:hover' : {
|
||||
'backgroundColor' : '#5F9B2D'
|
||||
}
|
||||
},
|
||||
'containedSecondary' : {
|
||||
'backgroundColor' : '#f50057',
|
||||
'&:hover' : {
|
||||
'backgroundColor' : '#f50057'
|
||||
}
|
||||
}
|
||||
},
|
||||
'MuiFab' : {
|
||||
'primary' : {
|
||||
'backgroundColor' : '#518029',
|
||||
'&:hover' : {
|
||||
'backgroundColor' : '#518029'
|
||||
},
|
||||
'&:disabled' : {
|
||||
'color' : '#999898',
|
||||
'backgroundColor' : '#323131'
|
||||
}
|
||||
},
|
||||
'secondary' : {
|
||||
'backgroundColor' : '#f50057',
|
||||
'&:hover' : {
|
||||
'backgroundColor' : '#f50057'
|
||||
},
|
||||
'&:disabled' : {
|
||||
'color' : '#999898',
|
||||
'backgroundColor' : '#323131'
|
||||
}
|
||||
}
|
||||
},
|
||||
'MuiBadge' : {
|
||||
'colorPrimary' : {
|
||||
'backgroundColor' : '#5F9B2D',
|
||||
'&:hover' : {
|
||||
'backgroundColor' : '#518029'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'typography' : {
|
||||
'useNextVariants' : true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generated with: `yarn gen-config-docs` from app/src/config.ts
|
||||
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
|
@ -1,88 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
xml:space="preserve"
|
||||
width="139"
|
||||
height="30"
|
||||
viewBox="0 0 799.66669 172.29333"
|
||||
sodipodi:docname="edumeet_logo.eps"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
|
||||
id="metadata8"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs6" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1014"
|
||||
id="namedview4"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.53772404"
|
||||
inkscape:cx="399.83334"
|
||||
inkscape:cy="86.146667"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g10" /><g
|
||||
id="g10"
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="ink_ext_XXXXXX"
|
||||
transform="matrix(1.3333333,0,0,-1.3333333,0,172.29333)"><g
|
||||
id="g12"
|
||||
transform="scale(0.1)"><path
|
||||
d="m 1667.64,373.02 c 0,0 -193.65,11.343 -394.11,119.109 72.23,293.047 -69.56,605.841 -353.264,738.851 C 597.211,1382.37 212.637,1243.22 61.2031,920.25 -90.1719,597.238 48.9063,212.57 371.965,61.1602 659.195,-73.4609 995.113,21.8086 1172.46,271.719 c 248.85,5.543 495.18,101.301 495.18,101.301"
|
||||
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path14"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 2241.9,497.863 c 1.02,45.887 -19.37,121.34 -102.98,121.34 -77.5,0 -110.14,-70.355 -115.23,-121.34 z m -218.21,-87.687 c 3.06,-89.742 73.42,-128.485 152.96,-128.485 58.12,0 99.91,8.161 137.64,22.434 l 18.37,-86.676 c -42.83,-17.34 -101.98,-30.59 -173.35,-30.59 -161.11,0 -255.95,98.922 -255.95,250.844 0,137.652 83.62,267.16 242.68,267.16 161.12,0 214.15,-132.562 214.15,-241.672 0,-23.453 -2.05,-41.8 -4.09,-53.015 h -332.41"
|
||||
style="fill:#004d7b;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path16"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 2793.57,486.648 c 0,10.196 -1.01,22.434 -3.06,32.629 -11.2,48.946 -50.97,88.711 -108.08,88.711 -80.55,0 -125.42,-71.375 -125.42,-164.168 0,-90.75 44.87,-157.031 124.41,-157.031 50.98,0 95.85,34.672 108.08,88.715 3.06,11.215 4.07,23.449 4.07,36.703 z m 125.42,434.379 V 336.754 c 0,-50.973 2.05,-106.043 4.09,-139.703 h -112.17 l -5.1,78.527 h -2.05 c -29.55,-55.066 -89.71,-89.738 -162.11,-89.738 -118.29,0 -212.1,100.949 -212.1,253.898 -1.01,166.211 102.98,265.125 222.29,265.125 68.32,0 117.27,-28.554 139.69,-65.261 h 2.04 v 281.425 h 125.42"
|
||||
style="fill:#004d7b;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path18"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 3498.2,345.93 c 0,-59.141 2.04,-108.078 4.09,-148.879 h -110.13 l -6.12,75.461 h -2.05 c -21.4,-35.684 -70.36,-86.672 -159.06,-86.672 -90.75,0 -173.35,54.051 -173.35,216.172 V 693.641 H 3177 V 423.422 c 0,-82.586 26.52,-135.613 92.79,-135.613 49.98,0 82.6,35.691 95.85,67.3 4.09,11.211 7.14,24.469 7.14,38.746 V 693.641 H 3498.2 V 345.93"
|
||||
style="fill:#004d7b;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path20"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 4219.13,477.465 c -4.09,88.715 -9.19,195.781 -8.15,288.57 h -3.06 c -22.44,-83.609 -51,-176.398 -81.58,-263.07 l -99.93,-297.754 h -96.87 l -91.78,293.672 c -26.5,87.687 -50.97,181.5 -69.34,267.152 h -2.02 c -3.08,-89.726 -7.14,-198.836 -12.24,-292.648 L 3738.87,197.051 H 3621.6 l 45.88,687.273 h 165.19 l 89.73,-276.336 c 24.48,-80.558 45.88,-162.129 63.23,-238.609 h 3.04 c 18.37,74.441 41.82,159.07 68.32,239.629 l 94.84,275.316 h 163.14 l 39.78,-687.273 h -122.38 l -13.24,280.414"
|
||||
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path22"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="M 4866.67,501.941 H 4607.68 V 300.043 h 289.58 V 197.051 h -415 v 687.273 h 399.7 V 781.336 H 4607.68 V 603.91 h 258.99 V 501.941"
|
||||
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path24"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 5391.83,501.941 h -259 V 300.043 h 289.59 V 197.051 h -415.01 v 687.273 h 399.71 V 781.336 H 5132.83 V 603.91 h 259 V 501.941"
|
||||
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path26"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 5674.31,779.297 h -195.78 v 105.027 h 519.02 V 779.297 H 5799.73 V 197.051 h -125.42 v 582.246"
|
||||
style="fill:#3871c1;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path28"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 804.438,984.711 c -187.032,87.649 -409.68,7.086 -497.36,-179.902 -55.531,-118.496 -43.469,-251.282 20.344,-355 -108.164,-67.911 -203.207,-92.024 -203.207,-92.024 0,0 143.82,-26.316 302.43,-14.781 18.66,-13.457 38.761,-25.453 60.351,-35.578 186.984,-87.625 409.707,-7.102 497.359,179.906 87.675,187.047 7.114,409.691 -179.917,497.379"
|
||||
style="fill:#f9a72b;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path30"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="M 831.895,558.988 C 783.809,456.395 661.621,412.215 559.043,460.293 456.438,508.387 412.262,630.559 460.344,733.152 c 48.094,102.582 170.242,146.778 272.847,98.696 102.61,-48.106 146.797,-170.246 98.704,-272.86"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||
id="path32"
|
||||
inkscape:connector-curvature="0" /></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 6.2 KiB |
|
|
@ -1,63 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta
|
||||
name='viewport'
|
||||
content='width=device-width, initial-scale=1, shrink-to-fit=no'
|
||||
/>
|
||||
<meta name='description' content='edumeet - Simple web meetings'>
|
||||
<meta name="keywords" content="edumeet,letsmeet,multiparty,videoconference,meet,WebRTC,room,rooms,geant,video conferencing, video conference, online meetings, web meeting, video meeting, cloud meeting, cloud video, group video call, group video chat, screen share, application share, mobility, mobile collaboration, desktop share, video collaboration">
|
||||
<meta name='theme-color' content='#000000' />
|
||||
|
||||
<link rel='preconnect' href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
|
||||
<link rel='shortcut icon' href='%PUBLIC_URL%/favicon.png' />
|
||||
<link rel='manifest' href='%PUBLIC_URL%/manifest.json' />
|
||||
|
||||
<title>edumeet</title>
|
||||
|
||||
<script src='%PUBLIC_URL%/config/config.js' type='text/javascript'></script>
|
||||
|
||||
<!-- Show an error page to IE browsers -->
|
||||
<script type='text/javascript'>
|
||||
var fallback = '<style type="text/css">body{margin:40px auto;max-width:650px;line-height:1.6;font-size:18px;color:#444;padding:0 10px}h1,h2,h3{line-height:1.2}</style><header><h1>Your browser is not supported</h1><aside>You need to change to a different browser.</aside></header><h3>Supported browsers</h3><ul><li>Google Chrome/Chromium 55 +</li><li>Microsoft Edge 18 +</li><li>Mozilla Firefox 60 +</li><li>Apple Safari 12 +</li><li>Opera 62 +</li><li>Samsung Internet 11.1.1.52 +</li></ul>';
|
||||
|
||||
var fallbackCall = function() {
|
||||
document.body.innerHTML = fallback;
|
||||
};
|
||||
|
||||
if(navigator.userAgent.indexOf('MSIE') !== -1)
|
||||
{
|
||||
document.attachEvent('onreadystatechange', function() {
|
||||
if (document.readyState === 'complete')
|
||||
{
|
||||
document.detachEvent('onreadystatechange', arguments.callee);
|
||||
|
||||
fallbackCall();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (navigator.appVersion.indexOf('Trident/') > -1)
|
||||
{
|
||||
if (
|
||||
document.readyState === 'complete' ||
|
||||
(document.readyState !== 'loading' && !document.documentElement.doScroll)
|
||||
)
|
||||
{
|
||||
document.removeEventListener('DOMContentLoaded', fallbackCall);
|
||||
|
||||
fallbackCall();
|
||||
}
|
||||
else
|
||||
{
|
||||
document.addEventListener('DOMContentLoaded', fallbackCall);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id='edumeet'></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"short_name": "edumeet",
|
||||
"name": "edumeet - Simple web meetings",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<title>Placeholder for Privacy Statement / Policy, AUP</title>
|
||||
<style type='text/css'>body{margin:40px auto;max-width:650px;line-height:1.6;font-size:18px;color:#444;padding:0 10px}h1,h2,h3{line-height:1.2}</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><h1>Privacy Statement</h1></header>
|
||||
<h2>Privacy Policy</h2>
|
||||
<ul>
|
||||
<li>User consent</li>
|
||||
<li>Data deletion</li>
|
||||
</ul>
|
||||
<h2>Acceptable use policy (AUP)</h2>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Disallow:
|
||||