├── compose
├── .gitignore
├── config
│ ├── edumeet-app-config.js
│ ├── grafana-dashboards.yml
│ ├── edumeet-server-config.yaml
│ ├── grafana-prometheus-datasource.yml
│ ├── prometheus.yml
│ ├── nginx.conf
│ └── edumeet-server-config.js
├── edumeet
│ └── Dockerfile
├── README.md
└── docker-compose.yml
├── server
├── .eslintignore
├── connect.js
├── utils
│ ├── password_encode.js
│ └── gen-config-docs.ts
├── tsconfig.json
├── lib
│ ├── access
│ │ ├── access.ts
│ │ ├── roles.js
│ │ └── perms.js
│ ├── helpers
│ │ ├── errors.js
│ │ └── httpHelper.js
│ ├── interactive
│ │ └── Client.js
│ ├── logger
│ │ └── Logger.ts
│ └── stats
│ │ └── promExporter.js
├── config
│ ├── config.example.toml
│ ├── config.example.yaml
│ └── config.example.json
├── certs
│ ├── edumeet-demo-cert.pem
│ └── edumeet-demo-key.pem
└── package.json
├── munin
├── mm-plugin-conf
├── munin.md
└── mm-plugin
├── app
├── .eslintignore
├── Procfile
├── src
│ ├── react-app-env.d.ts
│ ├── images
│ │ ├── background.jpg
│ │ ├── avatar-empty.jpeg
│ │ ├── pin-icon-baseline.svg
│ │ ├── pin-icon-outline.svg
│ │ └── buddy.svg
│ ├── store
│ │ ├── actions
│ │ │ ├── peerVolumeActions.js
│ │ │ ├── transportActions.js
│ │ │ ├── intlActions.js
│ │ │ ├── recorderActions.js
│ │ │ ├── toolareaActions.js
│ │ │ ├── notificationActions.js
│ │ │ ├── chatActions.js
│ │ │ ├── lobbyPeerActions.js
│ │ │ ├── producerActions.js
│ │ │ ├── requestActions.js
│ │ │ ├── fileActions.js
│ │ │ ├── consumerActions.js
│ │ │ ├── meActions.js
│ │ │ └── peerActions.js
│ │ ├── reducers
│ │ │ ├── intl.js
│ │ │ ├── transports.js
│ │ │ ├── config.js
│ │ │ ├── peerVolumes.js
│ │ │ ├── recorder.js
│ │ │ ├── rootReducer.js
│ │ │ ├── notifications.js
│ │ │ ├── lobbyPeers.js
│ │ │ ├── chat.js
│ │ │ ├── producers.js
│ │ │ ├── toolarea.js
│ │ │ ├── files.js
│ │ │ ├── consumers.js
│ │ │ ├── me.js
│ │ │ └── peers.js
│ │ └── store.js
│ ├── components
│ │ ├── Loader
│ │ │ ├── LazyPreload.js
│ │ │ └── LoadingView.js
│ │ ├── MeetingDrawer
│ │ │ ├── Chat
│ │ │ │ ├── Chat.js
│ │ │ │ └── Menu
│ │ │ │ │ └── Moderator.js
│ │ │ └── ParticipantList
│ │ │ │ ├── ListMe.js
│ │ │ │ └── ListModerator.js
│ │ ├── Controls
│ │ │ ├── EditableInput.js
│ │ │ └── ExtraVideo.js
│ │ ├── App.js
│ │ ├── PeerAudio
│ │ │ ├── AudioPeers.js
│ │ │ └── PeerAudio.js
│ │ ├── ConfigError.tsx
│ │ ├── appPropTypes.js
│ │ ├── FullScreen.js
│ │ ├── VideoWindow
│ │ │ └── VideoWindow.js
│ │ ├── VideoContainers
│ │ │ └── FullView.js
│ │ ├── ConfigDocumentation.tsx
│ │ ├── AccessControl
│ │ │ ├── LockDialog
│ │ │ │ └── ListLobbyPeer.js
│ │ │ └── LoginDialog.js
│ │ ├── Settings
│ │ │ ├── AdvancedSettings.js
│ │ │ └── Settings.js
│ │ ├── UnsupportedBrowser.js
│ │ ├── Containers
│ │ │ └── Volume.js
│ │ └── LeaveDialog.js
│ ├── __tests__
│ │ ├── RoomClient.spec.js
│ │ ├── App.spec.js
│ │ └── Room.spec.js
│ ├── RoomContext.js
│ ├── urlFactory.js
│ ├── utils.js
│ ├── electron-wait-react.js
│ ├── Logger.js
│ ├── deviceInfo.js
│ ├── index.css
│ ├── electron-starter.js
│ ├── permissions.js
│ ├── serviceWorker.js
│ ├── intl
│ │ └── locales.ts
│ └── transforms
│ │ └── receiver.ts
├── .env
├── public
│ ├── robots.txt
│ ├── favicon.png
│ ├── sounds
│ │ ├── notify.mp3
│ │ ├── notify-chat.mp3
│ │ └── notify-hand.mp3
│ ├── images
│ │ └── background.jpg
│ ├── manifest.json
│ ├── privacy
│ │ └── privacy.html
│ └── index.html
├── tsconfig.json
└── package.json
├── LTI
├── lti1.png
├── lti2.png
├── lti3.png
├── lti4.png
└── LTI.md
├── CONTRIBUTING.md
├── .gitignore
├── docs
├── README.md
├── HAproxy.md
└── SCALING_AND_HARDWARE.md
├── edumeet.service
├── LICENSE.md
├── prom.md
└── .github
└── workflows
└── develop-deb.yml
/compose/.gitignore:
--------------------------------------------------------------------------------
1 | data
2 |
--------------------------------------------------------------------------------
/server/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/munin/mm-plugin-conf:
--------------------------------------------------------------------------------
1 | [mm]
2 | user root
3 |
--------------------------------------------------------------------------------
/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .eslintrc.js
2 | src/react-app-env.d.ts
--------------------------------------------------------------------------------
/app/Procfile:
--------------------------------------------------------------------------------
1 | react: npm start
2 | electron: node src/electron-wait-react
--------------------------------------------------------------------------------
/app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/LTI/lti1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/LTI/lti1.png
--------------------------------------------------------------------------------
/LTI/lti2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/LTI/lti2.png
--------------------------------------------------------------------------------
/LTI/lti3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/LTI/lti3.png
--------------------------------------------------------------------------------
/LTI/lti4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/LTI/lti4.png
--------------------------------------------------------------------------------
/app/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_VERSION=$npm_package_version
2 | REACT_APP_NAME=$npm_package_name
--------------------------------------------------------------------------------
/app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # Allow crawling of all content
2 | User-agent: *
3 | Disallow:
--------------------------------------------------------------------------------
/app/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/app/public/favicon.png
--------------------------------------------------------------------------------
/app/public/sounds/notify.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/app/public/sounds/notify.mp3
--------------------------------------------------------------------------------
/app/src/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/app/src/images/background.jpg
--------------------------------------------------------------------------------
/app/public/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/app/public/images/background.jpg
--------------------------------------------------------------------------------
/app/public/sounds/notify-chat.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/app/public/sounds/notify-chat.mp3
--------------------------------------------------------------------------------
/app/public/sounds/notify-hand.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/app/public/sounds/notify-hand.mp3
--------------------------------------------------------------------------------
/app/src/images/avatar-empty.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/muaz-khan/edumeet/master/app/src/images/avatar-empty.jpeg
--------------------------------------------------------------------------------
/server/connect.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const interactiveClient = require('./lib/interactive/Client');
4 |
5 | interactiveClient();
6 |
--------------------------------------------------------------------------------
/compose/config/edumeet-app-config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | var config =
3 | {
4 | developmentPort : 8443,
5 | productionPort : 3443
6 | };
7 |
--------------------------------------------------------------------------------
/compose/config/grafana-dashboards.yml:
--------------------------------------------------------------------------------
1 | - name: 'default'
2 | org_id: 1
3 | folder: ''
4 | type: 'file'
5 | options:
6 | folder: '/var/lib/grafana/dashboards'
7 |
--------------------------------------------------------------------------------
/app/src/store/actions/peerVolumeActions.js:
--------------------------------------------------------------------------------
1 | export const setPeerVolume = (peerId, volume) =>
2 | ({
3 | type : 'SET_PEER_VOLUME',
4 | payload : { peerId, volume }
5 | });
6 |
--------------------------------------------------------------------------------
/app/src/store/actions/transportActions.js:
--------------------------------------------------------------------------------
1 | export const addTransportStats = (transport, type) =>
2 | ({
3 | type : 'ADD_TRANSPORT_STATS',
4 | payload : { transport, type }
5 | });
--------------------------------------------------------------------------------
/app/src/images/pin-icon-baseline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Source code contributions should pass static code analysis as performed by `npm run lint` in `server` and `app` respectively.
2 |
3 | Please contribute by creating your pull requests against the `develop` branch.
4 |
--------------------------------------------------------------------------------
/app/src/images/pin-icon-outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/store/actions/intlActions.js:
--------------------------------------------------------------------------------
1 | import { UPDATE } from 'react-intl-redux';
2 |
3 | export const updateIntl = ({ locale, formats, messages, list }) =>
4 | ({
5 | type : UPDATE,
6 | payload : { locale, formats, messages, list }
7 | });
8 |
--------------------------------------------------------------------------------
/compose/config/edumeet-server-config.yaml:
--------------------------------------------------------------------------------
1 | redisOptions:
2 | host: redis
3 | port: 6379
4 |
5 | listeningPort: 3443
6 | listeningRedirectPort: 0
7 | httpOnly: false
8 | trustProxy: ''
9 |
10 | prometheus:
11 | enabled: true
12 | listen: 0.0.0.0
13 |
--------------------------------------------------------------------------------
/app/src/components/Loader/LazyPreload.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const LazyPreload = (importStatement) =>
4 | {
5 | const Component = React.lazy(importStatement);
6 |
7 | Component.preload = importStatement;
8 |
9 | return Component;
10 | };
--------------------------------------------------------------------------------
/app/src/__tests__/RoomClient.spec.js:
--------------------------------------------------------------------------------
1 | import RoomClient from '../RoomClient';
2 |
3 | describe('new RoomClient() without parameters throws Error', () =>
4 | {
5 | test('Matches the snapshot', () =>
6 | {
7 | expect(() => new RoomClient()).toThrow(Error);
8 | });
9 | });
--------------------------------------------------------------------------------
/server/utils/password_encode.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcrypt');
2 | const saltRounds=10;
3 |
4 | if (process.argv.length == 3)
5 | {
6 | const cleartextPassword = process.argv[2];
7 |
8 | // eslint-disable-next-line no-console
9 | console.log(bcrypt.hashSync(cleartextPassword, saltRounds));
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/store/actions/recorderActions.js:
--------------------------------------------------------------------------------
1 | export const setLocalRecordingState = (status) =>
2 | ({
3 | type : 'SET_LOCAL_RECORDING_STATE',
4 | payload : { status }
5 | });
6 | export const setLocalRecordingConsent = (agreed) =>
7 | ({
8 | type : 'SET_LOCAL_RECORDING_CONSENT',
9 | payload : { agreed }
10 | });
11 |
--------------------------------------------------------------------------------
/app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "edumeet",
3 | "name": "edumeet - Simple web meetings",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/store/reducers/intl.js:
--------------------------------------------------------------------------------
1 | import { UPDATE } from 'react-intl-redux';
2 |
3 | const initialState = {
4 | locale : null,
5 | messages : null
6 | };
7 |
8 | const intlReducer = (state = initialState, action) =>
9 | {
10 | if (action.type !== UPDATE)
11 | {
12 | return state;
13 | }
14 |
15 | return { ...state, ...action.payload };
16 | };
17 |
18 | export default intlReducer;
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | /app/build/
4 | /app/public/config/config.js
5 | /app/public/images/logo.*
6 | !/app/public/images/logo.edumeet.svg
7 | /server/config/
8 | !/server/config/config.example.*
9 | !/server/config/README.md
10 | /server/public/
11 | /server/certs/
12 | /server/dist/
13 | !/server/certs/mediasoup-demo.localhost.*
14 | .vscode
15 | /app/public/config/*.pem
16 | yarn-error.log
17 |
--------------------------------------------------------------------------------
/app/src/RoomContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const RoomContext = React.createContext();
4 |
5 | export default RoomContext;
6 |
7 | export function withRoomContext(Component)
8 | {
9 | return (props) => ( // eslint-disable-line react/display-name
10 |
11 | {(roomClient) => }
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/app/src/store/reducers/transports.js:
--------------------------------------------------------------------------------
1 | const initialState = {};
2 |
3 | const transports = (state = initialState, action) =>
4 | {
5 | switch (action.type)
6 | {
7 | case 'ADD_TRANSPORT_STATS':
8 | {
9 | const { transport, type } = action.payload;
10 |
11 | return { ...state, [type]: transport[0] };
12 | }
13 |
14 | default:
15 | return state;
16 | }
17 | };
18 |
19 | export default transports;
20 |
--------------------------------------------------------------------------------
/app/src/store/reducers/config.js:
--------------------------------------------------------------------------------
1 | import { config as defaultConfig } from '../../config';
2 |
3 | const initialState =
4 | {
5 | ...defaultConfig
6 | };
7 |
8 | const config = (state = initialState, action) =>
9 | {
10 | switch (action.type)
11 | {
12 | case 'CONFIG_SET':
13 | {
14 | return { ...action.payload };
15 | }
16 |
17 | default:
18 | return state;
19 | }
20 | };
21 |
22 | export default config;
23 |
--------------------------------------------------------------------------------
/app/src/store/actions/toolareaActions.js:
--------------------------------------------------------------------------------
1 | export const toggleToolArea = () =>
2 | ({
3 | type : 'TOGGLE_TOOL_AREA'
4 | });
5 |
6 | export const openToolArea = () =>
7 | ({
8 | type : 'OPEN_TOOL_AREA'
9 | });
10 |
11 | export const closeToolArea = () =>
12 | ({
13 | type : 'CLOSE_TOOL_AREA'
14 | });
15 |
16 | export const setToolTab = (toolTab) =>
17 | ({
18 | type : 'SET_TOOL_TAB',
19 | payload : { toolTab }
20 | });
--------------------------------------------------------------------------------
/app/src/urlFactory.js:
--------------------------------------------------------------------------------
1 | import { config } from './config';
2 |
3 | export function getSignalingUrl(peerId, roomId)
4 | {
5 | const hostname = config.serverHostname || window.location.hostname;
6 | const port =
7 | process.env.NODE_ENV !== 'production' ?
8 | config.developmentPort
9 | :
10 | config.productionPort;
11 |
12 | const url = `wss://${hostname}:${port}/?peerId=${peerId}&roomId=${roomId}`;
13 |
14 | return url;
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/components/Loader/LoadingView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withStyles } from '@material-ui/core/styles';
3 |
4 | const styles = (/* theme */) =>
5 | ({
6 | root :
7 | {
8 | height : '100%',
9 | width : '100%'
10 | }
11 | });
12 |
13 | const LoadingView = ({
14 | classes
15 | }) =>
16 | {
17 | return (
18 |
19 | );
20 | };
21 |
22 | export default withStyles(styles)(LoadingView);
23 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 
2 | # Documentation / Table Of Contents
3 | ### [Overview eduMEET](/README.md)
4 | ### [Documentation of configuration for client/app](/app/public/config/README.md)
5 | ### [Documentation of configuration for server](/server/config/README.md)
6 | ### [Documentation of Development enviroment with Docker](/compose/README.md)
7 | ### [Setup HAproxy / load balancing edumeet](HAproxy.md)
8 | ### [Scaling and recommended Hardware](SCALING_AND_HARDWARE.md)
9 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "allowSyntheticDefaultImports": true,
7 | "allowJs": true,
8 | "moduleResolution": "node",
9 | "sourceMap": true,
10 | "outDir": "dist",
11 | "rootDir": "."
12 | },
13 | "include": [
14 | "**/*.ts",
15 | "**/*.js"
16 | ],
17 | "exclude": [
18 | "./public",
19 | "./dist"
20 | ]
21 | }
--------------------------------------------------------------------------------
/compose/edumeet/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14-buster-slim
2 | RUN apt-get update && \
3 | apt-get install -y git build-essential python pkg-config libssl-dev && \
4 | apt-get clean
5 | WORKDIR /edumeet
6 | ENV DEBUG=edumeet*,mediasoup*
7 | RUN npm install -g nodemon && \
8 | npm install -g concurrently
9 | RUN touch /.yarnrc && mkdir -p /.yarn /.cache/yarn && chmod -R 775 /.yarn /.yarnrc /.cache
10 | CMD concurrently --names "server,app" \
11 | "cd server && yarn && yarn dev" \
12 | "cd app && yarn && yarn build && yarn start"
13 |
--------------------------------------------------------------------------------
/server/lib/access/access.ts:
--------------------------------------------------------------------------------
1 | // The role(s) will gain access to the room
2 | // even if it is locked (!)
3 | export const BYPASS_ROOM_LOCK = 'BYPASS_ROOM_LOCK';
4 |
5 | // The role(s) will gain access to the room without
6 | // going into the lobby. If you want to restrict access to your
7 | // server to only directly allow authenticated users, you could
8 | // add the userRoles.AUTHENTICATED to the user in the userMapping
9 | // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ]
10 | export const BYPASS_LOBBY = 'BYPASS_LOBBY';
11 |
--------------------------------------------------------------------------------
/app/src/store/actions/notificationActions.js:
--------------------------------------------------------------------------------
1 | export const addNotification = (notification) =>
2 | ({
3 | type : 'ADD_NOTIFICATION',
4 | payload : { notification }
5 | });
6 |
7 | export const removeNotification = (notificationId) =>
8 | ({
9 | type : 'REMOVE_NOTIFICATION',
10 | payload : { notificationId }
11 | });
12 |
13 | export const removeAllNotifications = () =>
14 | ({
15 | type : 'REMOVE_ALL_NOTIFICATIONS'
16 | });
17 |
18 | export const closeNotification = (notificationId) =>
19 | ({
20 | type : 'CLOSE_NOTIFICATION',
21 | payload : { notificationId }
22 | });
23 |
--------------------------------------------------------------------------------
/compose/config/grafana-prometheus-datasource.yml:
--------------------------------------------------------------------------------
1 | apiVersion: 1
2 |
3 | deleteDatasources:
4 | - name: Prometheus
5 | orgId: 1
6 |
7 | datasources:
8 | - name: Prometheus
9 | type: prometheus
10 | access: proxy
11 | url: http://prometheus:9090
12 | password:
13 | user:
14 | database: prometheus
15 | basicAuth: false
16 | basicAuthUser:
17 | basicAuthPassword:
18 | withCredentials:
19 | isDefault: true
20 | jsonData:
21 | tlsAuth: false
22 | tlsAuthWithCACert: false
23 | secureJsonData:
24 | tlsCACert: ""
25 | tlsClientCert: ""
26 | tlsClientKey: ""
27 | version: 1
28 | editable: true
--------------------------------------------------------------------------------
/app/public/privacy/privacy.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Placeholder for Privacy Statement / Policy, AUP
7 |
8 |
9 |
10 |
11 | Privacy Policy
12 |
13 | - User consent
14 | - Data deletion
15 |
16 | Acceptable use policy (AUP)
17 |
18 |
--------------------------------------------------------------------------------
/edumeet.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=edumeet is a audio / video meeting service running in the browser and powered by webRTC
3 | After=network.target
4 |
5 | [Service]
6 | # modify the paths accordingly with your installation
7 | ExecStart=/usr/local/src/edumeet/server/dist/server.js
8 | WorkingDirectory=/usr/local/src/edumeet/server
9 | Restart=always
10 | RestartSec=1
11 | User=nobody
12 | Group=nogroup
13 | Environment=PATH=/usr/bin:/usr/local/bin
14 | Environment=NODE_ENV=production
15 | Environment=DEBUG="*ERROR*,*WARN*,*INFO*"
16 | StandardOutput=syslog
17 | StandardError=syslog
18 | AmbientCapabilities=CAP_NET_BIND_SERVICE
19 |
20 | [Install]
21 | WantedBy=multi-user.target
22 |
--------------------------------------------------------------------------------
/server/lib/helpers/errors.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Error produced when a socket request has a timeout.
3 | */
4 | class SocketTimeoutError extends Error
5 | {
6 | constructor(message)
7 | {
8 | super(message);
9 |
10 | this.name = 'SocketTimeoutError';
11 |
12 | if (Error.hasOwnProperty('captureStackTrace')) // Just in V8.
13 | Error.captureStackTrace(this, SocketTimeoutError);
14 | else
15 | this.stack = (new Error(message)).stack;
16 | }
17 | }
18 |
19 | class NotFoundInMediasoupError extends Error
20 | {
21 | constructor(message)
22 | {
23 | super(message);
24 |
25 | this.name = 'NotFoundInMediasoupError';
26 | }
27 | }
28 |
29 | module.exports =
30 | {
31 | SocketTimeoutError,
32 | NotFoundInMediasoupError
33 | };
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "downlevelIteration": true
23 | },
24 | "include": ["src/**/*.ts", "src/**/*.tsx"],
25 | "exclude": ["node_modules", "**/*.spec.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/store/actions/chatActions.js:
--------------------------------------------------------------------------------
1 | export const addMessage = (message) =>
2 | ({
3 | type : 'ADD_MESSAGE',
4 | payload : { message }
5 | });
6 |
7 | export const addChatHistory = (chatHistory) =>
8 | ({
9 | type : 'ADD_CHAT_HISTORY',
10 | payload : { chatHistory }
11 | });
12 |
13 | export const clearChat = () =>
14 | ({
15 | type : 'CLEAR_CHAT'
16 | });
17 |
18 | export const sortChat = (order) =>
19 | ({
20 | type : 'SORT_CHAT',
21 | payload : { order }
22 | });
23 |
24 | export const setIsScrollEnd = (flag) =>
25 | ({
26 | type : 'SET_IS_SCROLL_END',
27 | payload : { flag }
28 | });
29 |
30 | export const setIsMessageRead = (id, isRead) =>
31 | {
32 | return ({
33 | type : 'SET_IS_MESSAGE_READ',
34 | payload : { id, isRead }
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/app/src/store/actions/lobbyPeerActions.js:
--------------------------------------------------------------------------------
1 | export const addLobbyPeer = (peerId) =>
2 | ({
3 | type : 'ADD_LOBBY_PEER',
4 | payload : { peerId }
5 | });
6 |
7 | export const removeLobbyPeer = (peerId) =>
8 | ({
9 | type : 'REMOVE_LOBBY_PEER',
10 | payload : { peerId }
11 | });
12 |
13 | export const setLobbyPeerDisplayName = (displayName, peerId) =>
14 | ({
15 | type : 'SET_LOBBY_PEER_DISPLAY_NAME',
16 | payload : { displayName, peerId }
17 | });
18 |
19 | export const setLobbyPeerPicture = (picture, peerId) =>
20 | ({
21 | type : 'SET_LOBBY_PEER_PICTURE',
22 | payload : { picture, peerId }
23 | });
24 |
25 | export const setLobbyPeerPromotionInProgress = (peerId, flag) =>
26 | ({
27 | type : 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS',
28 | payload : { peerId, flag }
29 | });
--------------------------------------------------------------------------------
/server/config/config.example.toml:
--------------------------------------------------------------------------------
1 | listeningPort = "443"
2 | listeningHost = "host.domain.tld"
3 | fileTracker = "wss://tracker.openwebtorrent.com"
4 | turnAPIKey = "Your API key"
5 | turnAPIURI = "https://host.domain.tld/turn"
6 |
7 | [tls]
8 | cert = "./certs/edumeet-demo-cert.pem"
9 | key = "./certs/edumeet-demo-key.pem"
10 |
11 | [backupTurnServers]
12 | urls = [ "turn:host.domain.tld:443?transport=tcp" ]
13 | username = "Your username"
14 | credential = "Your's credential"
15 |
16 | [redisOptions]
17 | host = "127.0.0.1"
18 | port = "6379"
19 | password = "_REDIS_PASSWORD_"
20 |
21 | [prometheus]
22 | enabled = "true"
23 | deidentify = "true"
24 | numeric = "true"
25 | listen = "host.domain.tld"
26 |
27 | [[mediasoup.webRtcTransport.listenIps]]
28 | ip = "PUBLIC_IP_ADDRESS"
29 | announcedIp = ""
30 |
31 |
--------------------------------------------------------------------------------
/server/lib/interactive/Client.js:
--------------------------------------------------------------------------------
1 | const net = require('net');
2 | const os = require('os');
3 | const path = require('path');
4 |
5 | const SOCKET_PATH_UNIX = '/tmp/edumeet-server.sock';
6 | const SOCKET_PATH_WIN = path.join('\\\\?\\pipe', process.cwd(), 'edumeet-server');
7 | const SOCKET_PATH = os.platform() === 'win32'? SOCKET_PATH_WIN : SOCKET_PATH_UNIX;
8 |
9 | module.exports = async function()
10 | {
11 | const socket = net.connect(SOCKET_PATH);
12 |
13 | process.stdin.pipe(socket);
14 | socket.pipe(process.stdout);
15 |
16 | socket.on('connect', () => process.stdin.setRawMode(true));
17 |
18 | socket.on('close', () => process.exit(0));
19 | socket.on('exit', () => socket.end());
20 |
21 | if (process.argv && process.argv[2] === '--stats')
22 | {
23 | await socket.write('stats\n');
24 |
25 | socket.end();
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a function which will call the callback function
3 | * after the given amount of milliseconds has passed since
4 | * the last time the callback function was called.
5 | */
6 | export const idle = (callback, delay) =>
7 | {
8 | let handle;
9 |
10 | return () =>
11 | {
12 | if (handle)
13 | {
14 | clearTimeout(handle);
15 | }
16 |
17 | handle = setTimeout(callback, delay);
18 | };
19 | };
20 |
21 | /**
22 | * Error produced when a socket request has a timeout.
23 | */
24 | export class SocketTimeoutError extends Error
25 | {
26 | constructor(message)
27 | {
28 | super(message);
29 |
30 | this.name = 'SocketTimeoutError';
31 |
32 | if (Error.hasOwnProperty('captureStackTrace')) // Just in V8.
33 | Error.captureStackTrace(this, SocketTimeoutError);
34 | else
35 | this.stack = (new Error(message)).stack;
36 | }
37 | }
--------------------------------------------------------------------------------
/server/lib/helpers/httpHelper.js:
--------------------------------------------------------------------------------
1 | exports.loginHelper = function(data)
2 | {
3 | const html = `
4 |
5 |
6 |
7 | edumeet
8 |
9 |
10 |
17 |
18 | `;
19 |
20 | return html;
21 | };
22 |
23 | exports.logoutHelper = function()
24 | {
25 | const html = `
26 |
27 |
28 |
29 | edumeet
30 |
31 |
32 |
37 |
38 | `;
39 |
40 | return html;
41 | };
--------------------------------------------------------------------------------
/server/lib/access/roles.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // These can be changed, id must be unique.
3 |
4 | // A person can give other peers any role that is promotable: true
5 | // with a level up to and including their own highest role.
6 | // Example: A MODERATOR can give other peers PRESENTER and MODERATOR
7 | // roles (all peers always have NORMAL)
8 | ADMIN : { id: 2529, label: 'admin', level: 50, promotable: true },
9 | MODERATOR : { id: 5337, label: 'moderator', level: 40, promotable: true },
10 | PRESENTER : { id: 9583, label: 'presenter', level: 30, promotable: true },
11 | AUTHENTICATED : { id: 5714, label: 'authenticated', level: 20, promotable: false },
12 | // Don't change anything after this point
13 |
14 | // All users have this role by default, do not change or remove this role
15 | NORMAL : { id: 4261, label: 'normal', level: 10, promotable: false }
16 | };
--------------------------------------------------------------------------------
/app/src/components/MeetingDrawer/Chat/Chat.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import Paper from '@material-ui/core/Paper';
5 | import Moderator from './Menu/Moderator';
6 | import List from './List/List';
7 | import Input from './Menu/Input';
8 |
9 | const styles = () =>
10 | ({
11 | root :
12 | {
13 | display : 'flex',
14 | flexDirection : 'column',
15 | width : '100%',
16 | height : '100%',
17 | overflowY : 'auto'
18 | }
19 | });
20 |
21 | const Chat = (props) =>
22 | {
23 | const {
24 | classes
25 | } = props;
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | Chat.propTypes =
37 | {
38 | classes : PropTypes.object.isRequired
39 | };
40 |
41 | export default withStyles(styles)(Chat);
--------------------------------------------------------------------------------
/app/src/store/actions/producerActions.js:
--------------------------------------------------------------------------------
1 | export const addProducer = (producer) =>
2 | ({
3 | type : 'ADD_PRODUCER',
4 | payload : { producer }
5 | });
6 |
7 | export const removeProducer = (producerId) =>
8 | ({
9 | type : 'REMOVE_PRODUCER',
10 | payload : { producerId }
11 | });
12 |
13 | export const setProducerPaused = (producerId, originator) =>
14 | ({
15 | type : 'SET_PRODUCER_PAUSED',
16 | payload : { producerId, originator }
17 | });
18 |
19 | export const setProducerResumed = (producerId, originator) =>
20 | ({
21 | type : 'SET_PRODUCER_RESUMED',
22 | payload : { producerId, originator }
23 | });
24 |
25 | export const setProducerTrack = (producerId, track) =>
26 | ({
27 | type : 'SET_PRODUCER_TRACK',
28 | payload : { producerId, track }
29 | });
30 |
31 | export const setProducerScore = (producerId, score) =>
32 | ({
33 | type : 'SET_PRODUCER_SCORE',
34 | payload : { producerId, score }
35 | });
--------------------------------------------------------------------------------
/app/src/store/actions/requestActions.js:
--------------------------------------------------------------------------------
1 | import randomString from 'random-string';
2 | import * as notificationActions from './notificationActions';
3 |
4 | // This returns a redux-thunk action (a function).
5 | export const notify = ({ type = 'info', text, timeout }) =>
6 | {
7 | if (!timeout)
8 | {
9 | switch (type)
10 | {
11 | case 'info':
12 | timeout = 3000;
13 | break;
14 | case 'error':
15 | timeout = 5000;
16 | break;
17 | default:
18 | timeout = 3000;
19 | break;
20 | }
21 | }
22 |
23 | const notification =
24 | {
25 | id : randomString({ length: 6 }).toLowerCase(),
26 | type : type,
27 | text : text,
28 | timeout : timeout
29 | };
30 |
31 | return (dispatch) =>
32 | {
33 | dispatch(notificationActions.addNotification(notification));
34 |
35 | setTimeout(() =>
36 | {
37 | dispatch(notificationActions.removeNotification(notification.id));
38 | }, timeout);
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/app/src/store/reducers/peerVolumes.js:
--------------------------------------------------------------------------------
1 | const initialState = {};
2 |
3 | const peerVolumes = (state = initialState, action) =>
4 | {
5 | switch (action.type)
6 | {
7 | case 'SET_ME':
8 | {
9 | const {
10 | peerId
11 | } = action.payload;
12 |
13 | return { ...state, [peerId]: -100 };
14 | }
15 | case 'ADD_PEER':
16 | {
17 | const { peer } = action.payload;
18 |
19 | return { ...state, [peer.id]: -100 };
20 | }
21 |
22 | case 'REMOVE_PEER':
23 | {
24 | const { peerId } = action.payload;
25 | const newState = { ...state };
26 |
27 | delete newState[peerId];
28 |
29 | return newState;
30 | }
31 |
32 | case 'SET_PEER_VOLUME':
33 | {
34 | const { peerId } = action.payload;
35 | const dBs = action.payload.volume < -100 ? -100 : action.payload.volume;
36 |
37 | return { ...state, [peerId]: dBs };
38 | }
39 |
40 | default:
41 | return state;
42 | }
43 | };
44 |
45 | export default peerVolumes;
46 |
--------------------------------------------------------------------------------
/app/src/electron-wait-react.js:
--------------------------------------------------------------------------------
1 | const net = require('net');
2 | const port = process.env.PORT ? (process.env.PORT - 100) : 3000;
3 |
4 | process.env.ELECTRON_START_URL = `http://localhost:${port}`;
5 |
6 | const client = new net.Socket();
7 |
8 | let startedElectron = false;
9 |
10 | const tryConnection = () =>
11 | client.connect({ port: port }, () =>
12 | {
13 | client.end();
14 |
15 | if (!startedElectron)
16 | {
17 | // eslint-disable-next-line no-console
18 | console.log('starting electron');
19 |
20 | startedElectron = true;
21 |
22 | const exec = require('child_process').exec;
23 |
24 | const electron = exec('npm run electron');
25 |
26 | electron.stdout.on('data', (data) =>
27 | {
28 | // eslint-disable-next-line no-console
29 | console.log(`stdout: ${data.toString()}`);
30 | });
31 | }
32 | });
33 |
34 | tryConnection();
35 |
36 | client.on('error', () =>
37 | {
38 | setTimeout(tryConnection, 1000);
39 | });
40 |
--------------------------------------------------------------------------------
/app/src/store/actions/fileActions.js:
--------------------------------------------------------------------------------
1 | export const addFile = (file) =>
2 | ({
3 | type : 'ADD_FILE',
4 | payload : { ...file }
5 | });
6 |
7 | export const addFileHistory = (fileHistory) =>
8 | ({
9 | type : 'ADD_FILE_HISTORY',
10 | payload : { fileHistory }
11 | });
12 |
13 | export const setFileActive = (magnetUri) =>
14 | ({
15 | type : 'SET_FILE_ACTIVE',
16 | payload : { magnetUri }
17 | });
18 |
19 | export const setFileInActive = (magnetUri) =>
20 | ({
21 | type : 'SET_FILE_INACTIVE',
22 | payload : { magnetUri }
23 | });
24 |
25 | export const setFileProgress = (magnetUri, progress) =>
26 | ({
27 | type : 'SET_FILE_PROGRESS',
28 | payload : { magnetUri, progress }
29 | });
30 |
31 | export const setFileDone = (magnetUri, sharedFiles) =>
32 | ({
33 | type : 'SET_FILE_DONE',
34 | payload : { magnetUri, sharedFiles }
35 | });
36 |
37 | export const clearFiles = () =>
38 | ({
39 | type : 'CLEAR_FILES'
40 | });
--------------------------------------------------------------------------------
/app/src/Logger.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | const APP_NAME = 'edumeet';
4 |
5 | export default class Logger
6 | {
7 | constructor(prefix)
8 | {
9 | if (prefix)
10 | {
11 | this._debug = debug(`${APP_NAME}:${prefix}`);
12 | this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
13 | this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
14 | }
15 | else
16 | {
17 | this._debug = debug(APP_NAME);
18 | this._warn = debug(`${APP_NAME}:WARN`);
19 | this._error = debug(`${APP_NAME}:ERROR`);
20 | }
21 |
22 | /* eslint-disable no-console */
23 | this._debug.log = console.info.bind(console);
24 | this._warn.log = console.warn.bind(console);
25 | this._error.log = console.error.bind(console);
26 | /* eslint-enable no-console */
27 | }
28 |
29 | get debug()
30 | {
31 | return this._debug;
32 | }
33 |
34 | get warn()
35 | {
36 | return this._warn;
37 | }
38 |
39 | get error()
40 | {
41 | return this._error;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/deviceInfo.js:
--------------------------------------------------------------------------------
1 | import bowser from 'bowser';
2 |
3 | window.BB = bowser;
4 |
5 | export default function deviceInfo()
6 | {
7 | const ua = navigator.userAgent;
8 | const browser = bowser.getParser(ua);
9 |
10 | let flag;
11 |
12 | if (browser.satisfies({ chrome: '>=0', chromium: '>=0' }))
13 | flag = 'chrome';
14 | else if (browser.satisfies({ firefox: '>=0' }))
15 | flag = 'firefox';
16 | else if (browser.satisfies({ safari: '>=0' }))
17 | flag = 'safari';
18 | else if (browser.satisfies({ opera: '>=0' }))
19 | flag = 'opera';
20 | else if (browser.satisfies({ 'microsoft edge': '>=0' }))
21 | flag = 'edge';
22 | else
23 | flag = 'unknown';
24 |
25 | return {
26 | flag,
27 | os : browser.getOSName(true), // ios, android, linux...
28 | platform : browser.getPlatformType(true), // mobile, desktop, tablet
29 | name : browser.getBrowserName(true),
30 | version : browser.getBrowserVersion(),
31 | bowser : browser
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/store/reducers/recorder.js:
--------------------------------------------------------------------------------
1 | const initialState = {
2 | localRecordingState : {
3 | status : 'init',
4 | consent : 'init'
5 | }
6 | }
7 |
8 | ;
9 |
10 | const recorder = (state = initialState, action) =>
11 | {
12 | switch (action.type)
13 | {
14 | case 'SET_LOCAL_RECORDING_STATE':
15 | {
16 | const { status } = action.payload;
17 |
18 | const localRecordingState = state['localRecordingState'];
19 |
20 | localRecordingState.status = status;
21 |
22 | return { ...state,
23 | localRecordingState : localRecordingState
24 | };
25 | }
26 |
27 | case 'SET_LOCAL_RECORDING_CONSENT':
28 | {
29 | const { agreed } = action.payload;
30 | const localRecordingState = state['localRecordingState'];
31 |
32 | localRecordingState.consent = agreed;
33 |
34 | return { ...state,
35 | localRecordingState : localRecordingState
36 | };
37 | }
38 |
39 | default:
40 | return state;
41 | }
42 | };
43 |
44 | export default recorder;
45 |
--------------------------------------------------------------------------------
/app/src/store/reducers/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import room from './room';
3 | import me from './me';
4 | import producers from './producers';
5 | import consumers from './consumers';
6 | import transports from './transports';
7 | import peers from './peers';
8 | import lobbyPeers from './lobbyPeers';
9 | import peerVolumes from './peerVolumes';
10 | import notifications from './notifications';
11 | import toolarea from './toolarea';
12 | import chat from './chat';
13 | import files from './files';
14 | import recorder from './recorder';
15 | import settings from './settings';
16 | import config from './config';
17 | import intl from './intl';
18 |
19 | export default combineReducers({
20 | // intl : intlReducer,
21 | room,
22 | me,
23 | producers,
24 | consumers,
25 | transports,
26 | peers,
27 | lobbyPeers,
28 | peerVolumes,
29 | notifications,
30 | toolarea,
31 | chat,
32 | files,
33 | recorder,
34 | settings,
35 | config,
36 | intl
37 | });
38 |
--------------------------------------------------------------------------------
/server/config/config.example.yaml:
--------------------------------------------------------------------------------
1 | listeningPort: 443
2 | listeningHost: host.domain.tld
3 |
4 | fileTracker: "wss://tracker.openwebtorrent.com"
5 |
6 | tls:
7 | cert: ./certs/edumeet-demo-cert.pem
8 | key: ./certs/edumeet-demo-key.pem
9 |
10 | turnAPIURI: "https://host.domain.tld/turn"
11 | turnAPIKey: "Your API key"
12 |
13 | backupTurnServers:
14 | - urls:
15 | - "turn:host.domain.tld:443?transport=tcp"
16 | username: "Your username"
17 | credential: "Your's credential"
18 |
19 | redisOptions:
20 | host: "127.0.0.1"
21 | port: "6379"
22 | password: "_REDIS_PASSWORD_"
23 |
24 | prometheus:
25 | enabled: true
26 | deidentify: true
27 | numeric: true
28 | listen: host.domain.tld
29 |
30 | mediasoup:
31 | webRtcTransport:
32 | listenIps:
33 | - ip: "PUBLIC_IP_ADDRESS"
34 | announcedIp: ""
35 |
--------------------------------------------------------------------------------
/app/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background-color: rgba(114, 119, 143, 1.0);
3 |
4 | --peer-shadow: 0px /* 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12) */;
5 | --peer-border: 0px /* solid rgba(49, 49, 49, 0.9) */;
6 | --peer-empty-avatar: url('./images/buddy.svg');
7 | --peer-bg-color: rgba(49, 49, 49, 0.9);
8 | --peer-video-bg-color: rgba(19, 19, 19, 1);
9 |
10 | --active-speaker-border-color: rgba(255, 255, 255, 1.0);
11 | --selected-peer-border-color: rgba(55, 126, 255, 1.0);
12 | --active-speaker-shadow: 0px 0px 8px rgba(255, 255, 255, 0.9);
13 | }
14 |
15 | html
16 | {
17 | height: 100%;
18 | width: 100%;
19 | font-family: 'Roboto';
20 | font-weight: 300;
21 | margin : 0;
22 | box-sizing: border-box;
23 | }
24 |
25 | *, *:before, *:after {
26 | box-sizing: inherit;
27 | }
28 |
29 | body
30 | {
31 | height: 100%;
32 | width: 100%;
33 | font-size: 16px;
34 | margin: 0;
35 | }
36 |
37 | #edumeet
38 | {
39 | height: 100%;
40 | width: 100%;
41 | }
--------------------------------------------------------------------------------
/munin/munin.md:
--------------------------------------------------------------------------------
1 | # Install a munin plugin, as a very basic monitoring
2 |
3 | ## munin-node
4 |
5 | * install on your docker host munin-node on mm.example.com
6 |
7 | ```bash
8 | apt install munin-node
9 | ```
10 |
11 | * Copy mm-plugin from this directory to plugins dir as mm
12 |
13 | cp mm-plugin /usr/share/munin/plugins/mm
14 |
15 | ```bash
16 | sudo ln -s /usr/share/munin/plugins/mm /etc/munin/plugins/mm
17 | ```
18 |
19 | * Copy mm-plugin-conf from this directory to munin plugins conf dir as mm
20 |
21 | ```bash
22 | cp mm-plugin-conf /etc/munin/plugin-conf.d/mm
23 | ```
24 |
25 | * Restart munin
26 |
27 | ```bash
28 | systemctl restart munin-node
29 | ```
30 |
31 | ## munin master
32 |
33 | * Install a munin master on different host if you don't have munin already.
34 |
35 | ```bash
36 | apt install munin
37 | ```
38 |
39 | * On your munin master configure the new node
40 | edit and add to /etc/munin.conf
41 |
42 | ```bash
43 | [mm]
44 | mm.example.com
45 | ```
46 |
--------------------------------------------------------------------------------
/compose/config/prometheus.yml:
--------------------------------------------------------------------------------
1 | # global config
2 | global:
3 | scrape_interval: 120s # By default, scrape targets every 15 seconds.
4 | evaluation_interval: 120s # By default, scrape targets every 15 seconds.
5 | # scrape_timeout is set to the global default (10s).
6 | # Attach these labels to any time series or alerts when communicating with
7 | # external systems (federation, remote storage, Alertmanager).
8 | external_labels:
9 | monitor: 'edumeet'
10 |
11 | # Load and evaluate rules in this file every 'evaluation_interval' seconds.
12 | rule_files:
13 | # - "alert.rules"
14 | # - "first.rules"
15 | # - "second.rules"
16 |
17 | scrape_configs:
18 | - job_name: 'prometheus'
19 | scrape_interval: 15s
20 | static_configs:
21 | - targets: ['localhost:9090','node-exporter:9100']
22 | - job_name: 'edumeet'
23 | scrape_interval: 15s
24 | metrics_path: /metrics
25 | scheme: http
26 | # authorization:
27 | # type: Bearer
28 | # credentials: "prometheus-secret"
29 | # tls_config:
30 | # insecure_skip_verify: true
31 | static_configs:
32 | - targets: ['edumeet:8889']
33 |
--------------------------------------------------------------------------------
/server/config/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "listeningPort" : "443",
3 | "listeningHost" : "host.domain.tld",
4 |
5 | "fileTracker" : "wss://tracker.openwebtorrent.com",
6 |
7 | "tls" : {
8 | "cert" : "./certs/edumeet-demo-cert.pem",
9 | "key" : "./certs/edumeet-demo-key.pem"
10 | },
11 |
12 | "turnAPIKey" : "Your API key",
13 | "turnAPIURI" : "https://host.domain.tld/turn",
14 |
15 | "backupTurnServers" : {
16 | "urls": [ "turn:host.domain.tld:443?transport=tcp" ],
17 | "username" : "Your username",
18 | "credential" : "Your's credential"
19 | },
20 |
21 | "redisOptions": {
22 | "host" : "127.0.0.1",
23 | "port" : "6379",
24 | "password" : "_REDIS_PASSWORD_"
25 | },
26 |
27 | "prometheus" : {
28 | "enabled" : "true",
29 | "deidentify" : "true",
30 | "numeric" : "true",
31 | "listen" : "host.domain.tld"
32 | },
33 |
34 | "mediasoup" : {
35 | "webRtcTransport" : {
36 | "listenIps" : [
37 | {
38 | "ip": "PUBLIC_IP_ADDRESS",
39 | "announcedIp" : ""
40 | }
41 | ]
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/electron-starter.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 |
3 | const app = electron.app;
4 |
5 | const BrowserWindow = electron.BrowserWindow;
6 | const Menu = electron.Menu;
7 |
8 | const path = require('path');
9 | const url = require('url');
10 |
11 | let mainWindow;
12 |
13 | function createWindow()
14 | {
15 | mainWindow = new BrowserWindow({
16 | width : 1280,
17 | height : 720,
18 | webPreferences : { nodeIntegration: true }
19 | });
20 |
21 | Menu.setApplicationMenu(null);
22 |
23 | const startUrl = process.env.ELECTRON_START_URL || url.format({
24 | pathname : path.join(__dirname, '/../build/index.html'),
25 | protocol : 'file:',
26 | slashes : true
27 | });
28 |
29 | mainWindow.loadURL(startUrl);
30 |
31 | mainWindow.on('closed', () =>
32 | {
33 | mainWindow = null;
34 | });
35 | }
36 |
37 | app.on('ready', createWindow);
38 |
39 | app.on('window-all-closed', () =>
40 | {
41 | if (process.platform !== 'darwin')
42 | {
43 | app.quit();
44 | }
45 | });
46 |
47 | app.on('activate', () =>
48 | {
49 | if (mainWindow === null)
50 | {
51 | createWindow();
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 GÉANT Association
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/store/reducers/notifications.js:
--------------------------------------------------------------------------------
1 | const notifications = (state = [], action) =>
2 | {
3 | switch (action.type)
4 | {
5 | case 'ADD_NOTIFICATION':
6 | {
7 | const { notification } = action.payload;
8 |
9 | notification.toBeClosed=false;
10 |
11 | return [ ...state, notification ];
12 | }
13 |
14 | case 'ADD_CONSENT_NOTIFICATION':
15 | {
16 | const { notification } = action.payload;
17 |
18 | notification.toBeClosed=false;
19 |
20 | return [ ...state, notification ];
21 | }
22 |
23 | case 'REMOVE_NOTIFICATION':
24 | {
25 | const { notificationId } = action.payload;
26 |
27 | return state.filter((notification) => notification.id !== notificationId);
28 | }
29 |
30 | case 'REMOVE_ALL_NOTIFICATIONS':
31 | {
32 | return [];
33 | }
34 |
35 | case 'CLOSE_NOTIFICATION':
36 | {
37 | const { notificationId } = action.payload;
38 |
39 | return (state.map((e) =>
40 | {
41 | if (e.id === notificationId)
42 | {
43 | e.toBeClosed=true;
44 | }
45 |
46 | return e;
47 | }));
48 | }
49 |
50 | default:
51 | return state;
52 | }
53 | };
54 |
55 | export default notifications;
56 |
--------------------------------------------------------------------------------
/app/src/components/Controls/EditableInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { RIEInput } from 'riek';
4 |
5 | export default class EditableInput extends React.Component
6 | {
7 | render()
8 | {
9 | const {
10 | value,
11 | propName,
12 | className,
13 | classLoading,
14 | classInvalid,
15 | editProps,
16 | onChange
17 | } = this.props;
18 |
19 | return (
20 | onChange(data)}
29 | />
30 | );
31 | }
32 |
33 | shouldComponentUpdate(nextProps)
34 | {
35 | if (nextProps.value === this.props.value)
36 | return false;
37 |
38 | return true;
39 | }
40 | }
41 |
42 | EditableInput.propTypes =
43 | {
44 | value : PropTypes.string,
45 | propName : PropTypes.string.isRequired,
46 | className : PropTypes.string,
47 | classLoading : PropTypes.string,
48 | classInvalid : PropTypes.string,
49 | editProps : PropTypes.any,
50 | onChange : PropTypes.func.isRequired
51 | };
52 |
--------------------------------------------------------------------------------
/app/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, Suspense } from 'react';
2 | import { useParams } from 'react-router';
3 | import { connect } from 'react-redux';
4 | import PropTypes from 'prop-types';
5 | import JoinDialog from './JoinDialog';
6 | import LoadingView from './Loader/LoadingView';
7 | import { LazyPreload } from './Loader/LazyPreload';
8 |
9 | const Room = LazyPreload(() => import(/* webpackChunkName: "room" */ './Room'));
10 |
11 | const App = (props) =>
12 | {
13 | const {
14 | room
15 | } = props;
16 |
17 | const id = useParams().id.toLowerCase();
18 |
19 | useEffect(() =>
20 | {
21 | Room.preload();
22 |
23 | return;
24 | }, []);
25 |
26 | if (!room.joined)
27 | {
28 | return (
29 |
30 | );
31 | }
32 | else
33 | {
34 | return (
35 | }>
36 |
37 |
38 | );
39 | }
40 | };
41 |
42 | App.propTypes =
43 | {
44 | room : PropTypes.object.isRequired
45 | };
46 |
47 | const mapStateToProps = (state) =>
48 | ({
49 | room : state.room
50 | });
51 |
52 | export default connect(
53 | mapStateToProps,
54 | null,
55 | null,
56 | {
57 | areStatesEqual : (next, prev) =>
58 | {
59 | return (
60 | prev.room === next.room
61 | );
62 | }
63 | }
64 | )(App);
--------------------------------------------------------------------------------
/server/utils/gen-config-docs.ts:
--------------------------------------------------------------------------------
1 | import { configDocs } from '../lib/config/config';
2 | import { writeFile } from 'fs/promises';
3 |
4 | function formatJson(data)
5 | {
6 | return `\`${data.replace(/\n/g, '')}\``;
7 | }
8 |
9 | let data = `#  server configuration properties list:
10 |
11 | | Name | Description | Format | Default value |
12 | | :--- | :---------- | :----- | :------------ |
13 | `;
14 |
15 | Object.entries(configDocs).forEach((entry: [string, any]) =>
16 | {
17 | const [ name, value ] = entry;
18 |
19 | // escape dynamically created default values
20 | switch (name)
21 | {
22 | case 'mediasoup.webRtcTransport.listenIps':
23 | value.default = '[ { "ip": "0.0.0.0", "announcedIp": null } ]';
24 | break;
25 | case 'mediasoup.numWorkers':
26 | value.default = '4';
27 | break;
28 | }
29 |
30 | data += `| ${name} | ${value.doc} | ${formatJson(value.format)} | \`${formatJson(value.default)}\` |\n`;
31 | });
32 |
33 | data += `
34 |
35 | ---
36 |
37 | *Document generated with:* \`yarn gen-config-docs\`
38 | `;
39 |
40 | writeFile('config/README.md', data).then(() =>
41 | {
42 | console.log('done'); // eslint-disable-line
43 | }, (err) =>
44 | {
45 | console.error(`Error writing file: ${err.message}`); // eslint-disable-line
46 | });
47 |
--------------------------------------------------------------------------------
/server/lib/access/perms.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // The role(s) have permission to lock/unlock a room
3 | CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK',
4 | // The role(s) have permission to promote a peer from the lobby
5 | PROMOTE_PEER : 'PROMOTE_PEER',
6 | // The role(s) have permission to give/remove other peers roles
7 | MODIFY_ROLE : 'MODIFY_ROLE',
8 | // The role(s) have permission to send chat messages
9 | SEND_CHAT : 'SEND_CHAT',
10 | // The role(s) have permission to moderate chat
11 | MODERATE_CHAT : 'MODERATE_CHAT',
12 | // The role(s) have permission to share audio
13 | SHARE_AUDIO : 'SHARE_AUDIO',
14 | // The role(s) have permission to share video
15 | SHARE_VIDEO : 'SHARE_VIDEO',
16 | // The role(s) have permission to share screen
17 | SHARE_SCREEN : 'SHARE_SCREEN',
18 | // The role(s) have permission to produce extra video
19 | EXTRA_VIDEO : 'EXTRA_VIDEO',
20 | // The role(s) have permission to share files
21 | SHARE_FILE : 'SHARE_FILE',
22 | // The role(s) have permission to moderate files
23 | MODERATE_FILES : 'MODERATE_FILES',
24 | // The role(s) have permission to moderate room (e.g. kick user)
25 | MODERATE_ROOM : 'MODERATE_ROOM',
26 | // The role(s) have permission to local record room
27 | LOCAL_RECORD_ROOM : 'LOCAL_RECORD_ROOM'
28 | };
--------------------------------------------------------------------------------
/app/src/permissions.js:
--------------------------------------------------------------------------------
1 | export const permissions = {
2 | // The role(s) have permission to lock/unlock a room
3 | CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK',
4 | // The role(s) have permission to promote a peer from the lobby
5 | PROMOTE_PEER : 'PROMOTE_PEER',
6 | // The role(s) have permission to give/remove other peers roles
7 | MODIFY_ROLE : 'MODIFY_ROLE',
8 | // The role(s) have permission to send chat messages
9 | SEND_CHAT : 'SEND_CHAT',
10 | // The role(s) have permission to moderate chat
11 | MODERATE_CHAT : 'MODERATE_CHAT',
12 | // The role(s) have permission to share audio
13 | SHARE_AUDIO : 'SHARE_AUDIO',
14 | // The role(s) have permission to share video
15 | SHARE_VIDEO : 'SHARE_VIDEO',
16 | // The role(s) have permission to share screen
17 | SHARE_SCREEN : 'SHARE_SCREEN',
18 | // The role(s) have permission to produce extra video
19 | EXTRA_VIDEO : 'EXTRA_VIDEO',
20 | // The role(s) have permission to share files
21 | SHARE_FILE : 'SHARE_FILE',
22 | // The role(s) have permission to moderate files
23 | MODERATE_FILES : 'MODERATE_FILES',
24 | // The role(s) have permission to moderate room (e.g. kick user)
25 | MODERATE_ROOM : 'MODERATE_ROOM',
26 | // The role(s) have permission to start room recording localy
27 | LOCAL_RECORD_ROOM : 'LOCAL_RECORD_ROOM'
28 | };
--------------------------------------------------------------------------------
/server/lib/logger/Logger.ts:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | const APP_NAME = 'edumeet-server';
4 |
5 | export default class Logger
6 | {
7 | private _debug: debug.Debugger;
8 |
9 | private _info: debug.Debugger;
10 |
11 | private _warn: debug.Debugger;
12 |
13 | private _error: debug.Debugger;
14 |
15 | constructor(prefix: string)
16 | {
17 | if (prefix)
18 | {
19 | this._debug = debug(`${APP_NAME}:${prefix}`);
20 | this._info = debug(`${APP_NAME}:INFO:${prefix}`);
21 | this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
22 | this._error = debug(`${APP_NAME}:ERROR:${prefix}`);
23 | }
24 | else
25 | {
26 | this._debug = debug(APP_NAME);
27 | this._info = debug(`${APP_NAME}:INFO`);
28 | this._warn = debug(`${APP_NAME}:WARN`);
29 | this._error = debug(`${APP_NAME}:ERROR`);
30 | }
31 |
32 | /* eslint-disable no-console */
33 | this._debug.log = console.info.bind(console);
34 | this._info.log = console.info.bind(console);
35 | this._warn.log = console.warn.bind(console);
36 | this._error.log = console.error.bind(console);
37 | /* eslint-enable no-console */
38 | }
39 |
40 | get debug()
41 | {
42 | return this._debug;
43 | }
44 |
45 | get info()
46 | {
47 | return this._info;
48 | }
49 |
50 | get warn()
51 | {
52 | return this._warn;
53 | }
54 |
55 | get error()
56 | {
57 | return this._error;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/compose/README.md:
--------------------------------------------------------------------------------
1 | # Running the development environment
2 |
3 | Installing `docker-compose`:
4 | ```sh
5 | sudo curl -L "https://github.com/docker/compose/releases/download/1.28.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
6 | chmod +x /usr/local/bin/docker-compose
7 | ```
8 |
9 | Starting:
10 |
11 | ```sh
12 | CURRENT_USER=$UID:$GID docker-compose up --build -d
13 |
14 | docker-compose logs -f --tail=50 edumeet
15 | ```
16 |
17 | Accessing endpoints:
18 |
19 | - Edumeet (app live dev): https://127.0.0.1:8443/
20 | - Edumeet (app build): https://127.0.0.1:3443/
21 | - Prometheus: http://127.0.0.1:9090/
22 | - Grafana: http://127.0.0.1:9091/ (user:pass `admin`:`admin`)
23 |
24 | Note: to use the https://127.0.0.1:3443/ endpoint, visit first
25 | https://127.0.0.1:8443/ accepting the self-signed certificate exception valid
26 | also for the websocket connection.
27 |
28 | Rebuild the web application bundle:
29 |
30 | ```sh
31 | docker-compose exec -u $UID edumeet sh -c "cd app && yarn && yarn build"
32 | ```
33 |
34 | ## Known issues
35 |
36 | The docker virtual network used by this compose configuration (`172.22.0.0/24`)
37 | should be reacheble from all the started services. If iptables is filtering the
38 | INPUT chain, this rule is required to make the services communicating with each other:
39 |
40 | ```
41 | iptables -A INPUT --src 172.22.0.0/16 -j ACCEPT
42 | ```
43 |
--------------------------------------------------------------------------------
/munin/mm-plugin:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # -*- sh -*-
4 |
5 | : << =cut
6 |
7 | =head1 NAME
8 |
9 | turn - Plugin to monitor the turn server test probe.
10 |
11 | =head1 CONFIGURATION
12 |
13 | No configuration
14 |
15 | =head1 AUTHOR
16 |
17 | Unknown author
18 |
19 | =head1 LICENSE
20 |
21 | GPLv2
22 |
23 | =head1 MAGIC MARKERS
24 |
25 | #%# family=auto
26 | #%# capabilities=autoconf
27 |
28 | =cut
29 |
30 | . "$MUNIN_LIBDIR/plugins/plugin.sh"
31 |
32 | if [ "$1" = "autoconf" ]; then
33 | if [ -r /proc/sys/kernel/random/entropy_avail ]; then
34 | echo yes
35 | exit 0
36 | else
37 | echo no
38 | exit 0
39 | fi
40 | fi
41 |
42 | if [ "$1" = "config" ]; then
43 | echo 'graph_title MM stats'
44 | #echo 'graph_args --base 1000 -l 0'
45 | echo 'graph_vlabel Actual Session Count'
46 | echo 'graph_category other'
47 | echo 'graph_info This graph shows the mm stats.'
48 | echo 'rooms.label rooms'
49 | echo 'rooms.info The count of rooms.'
50 | echo 'peers.label peers'
51 | echo 'peers.info The count of peers.'
52 | exit 0
53 | fi
54 |
55 | ROOMS=`docker exec -t mm_mm_1 /opt/edumeet/server/connect.js --stats | grep 'rooms' | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" | sed -E 's/rooms:([0-9]+)/\1/g'`
56 | PEERS=`docker exec -t mm_mm_1 /opt/edumeet/server/connect.js --stats | grep 'peers' | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g" | sed -E 's/peers:([0-9]+)/\1/g'`
57 |
58 | echo "rooms.value ${ROOMS}"
59 | echo "peers.value ${PEERS}"
60 |
61 | :
62 |
--------------------------------------------------------------------------------
/app/src/components/PeerAudio/AudioPeers.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { micConsumerSelector } from '../../store/selectors';
4 | import PropTypes from 'prop-types';
5 | import PeerAudio from './PeerAudio';
6 |
7 | const AudioPeers = (props) =>
8 | {
9 | const {
10 | micConsumers,
11 | audioOutputDevice
12 | } = props;
13 |
14 | return (
15 |
16 | {
17 | micConsumers.map((micConsumer) =>
18 | {
19 | return (
20 |
26 | );
27 | })
28 | }
29 |
30 | );
31 | };
32 |
33 | AudioPeers.propTypes =
34 | {
35 | micConsumers : PropTypes.array,
36 | audioOutputDevice : PropTypes.string
37 | };
38 |
39 | const mapStateToProps = (state) =>
40 | ({
41 | micConsumers : micConsumerSelector(state),
42 | audioOutputDevice : state.settings.selectedAudioOutputDevice
43 | });
44 |
45 | const AudioPeersContainer = connect(
46 | mapStateToProps,
47 | null,
48 | null,
49 | {
50 | areStatesEqual : (next, prev) =>
51 | {
52 | return (
53 | prev.consumers === next.consumers &&
54 | prev.room.spotlights === next.room.spotlights &&
55 | prev.settings.selectedAudioOutputDevice ===
56 | next.settings.selectedAudioOutputDevice
57 | );
58 | }
59 | }
60 | )(AudioPeers);
61 |
62 | export default AudioPeersContainer;
63 |
--------------------------------------------------------------------------------
/app/src/store/reducers/lobbyPeers.js:
--------------------------------------------------------------------------------
1 | const lobbyPeer = (state = {}, action) =>
2 | {
3 | switch (action.type)
4 | {
5 | case 'ADD_LOBBY_PEER':
6 | return { id: action.payload.peerId };
7 |
8 | case 'SET_LOBBY_PEER_DISPLAY_NAME':
9 | return { ...state, displayName: action.payload.displayName };
10 | case 'SET_LOBBY_PEER_PICTURE':
11 | return { ...state, picture: action.payload.picture };
12 | case 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS':
13 | return { ...state, promotionInProgress: action.payload.flag };
14 |
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | const lobbyPeers = (state = {}, action) =>
21 | {
22 | switch (action.type)
23 | {
24 | case 'ADD_LOBBY_PEER':
25 | {
26 | return { ...state, [action.payload.peerId]: lobbyPeer(undefined, action) };
27 | }
28 |
29 | case 'REMOVE_LOBBY_PEER':
30 | {
31 | const { peerId } = action.payload;
32 | const newState = { ...state };
33 |
34 | delete newState[peerId];
35 |
36 | return newState;
37 | }
38 |
39 | case 'SET_LOBBY_PEER_DISPLAY_NAME':
40 | case 'SET_LOBBY_PEER_PICTURE':
41 | case 'SET_LOBBY_PEER_PROMOTION_IN_PROGRESS':
42 | {
43 | const oldLobbyPeer = state[action.payload.peerId];
44 |
45 | if (!oldLobbyPeer)
46 | {
47 | // Tried to update non-existent lobbyPeer. Has probably been promoted, or left.
48 | return state;
49 | }
50 |
51 | return { ...state, [oldLobbyPeer.id]: lobbyPeer(oldLobbyPeer, action) };
52 | }
53 |
54 | default:
55 | return state;
56 | }
57 | };
58 |
59 | export default lobbyPeers;
60 |
--------------------------------------------------------------------------------
/LTI/LTI.md:
--------------------------------------------------------------------------------
1 | # Learning Tools Interoperability (LTI)
2 |
3 | ## LTI
4 |
5 | Read more about IMS Global defined interface for tools like our VideoConference system integration with Learning Management Systems(LMS) (e.g. moodle).
6 | See: [IMS Global Learning Tool Interoperability](https://www.imsglobal.org/activity/learning-tools-interoperability)
7 |
8 | We implemented LTI interface version 1.0/1.1
9 |
10 | ### Server config auth section LTI settings
11 |
12 | Set in server configuration a random key and secret
13 |
14 | ``` json
15 | auth :
16 | {
17 | lti :
18 | {
19 | consumerKey : 'key',
20 | consumerSecret : 'secret'
21 | },
22 | }
23 | ```
24 |
25 | ### Configure your LMS system with secret and key settings above
26 |
27 | #### Auth tool URL
28 |
29 | Set tool URL to your server with path /auth/lti
30 |
31 | ``` url
32 | https://mm.example.com/auth/lti
33 | ```
34 |
35 | #### In moodle find external tool plugin setting and external tool action
36 |
37 | See: [moodle external tool settings](https://docs.moodle.org/38/en/External_tool_settings)
38 |
39 | #### Add and activity
40 |
41 | 
42 |
43 | #### Setup Activity
44 |
45 | ##### Activity setup basic form
46 |
47 | Open fully the settings **Click on show more!!**
48 | 
49 |
50 | ##### Empty full form
51 |
52 | 
53 |
54 | ##### Filled out form
55 |
56 | 
57 |
58 | ## moodle plugin
59 |
60 | Alternatively you can use edumeet moodle plugin:
61 | [https://github.com/edumeet/moodle-mod_edumeet](https://github.com/edumeet/moodle-mod_edumeet)
62 |
--------------------------------------------------------------------------------
/app/src/store/reducers/chat.js:
--------------------------------------------------------------------------------
1 | const initialState =
2 | {
3 | order : 'asc',
4 | isScrollEnd : true,
5 | messages : [],
6 | count : 0,
7 | countUnread : 0
8 | };
9 |
10 | const chat = (state = initialState, action) =>
11 | {
12 | switch (action.type)
13 | {
14 | case 'ADD_MESSAGE':
15 | {
16 | const { message } = action.payload;
17 |
18 | return {
19 | ...state,
20 | messages : [ ...state.messages, message ],
21 | count : state.count + 1,
22 | countUnread : message.sender === 'response' ? ++state.countUnread : state.countUnread
23 | };
24 |
25 | }
26 |
27 | case 'ADD_CHAT_HISTORY':
28 | {
29 | const { chatHistory } = action.payload;
30 |
31 | chatHistory.forEach(
32 | (item, index) => { chatHistory[index].isRead = true; }
33 | );
34 |
35 | return {
36 | ...state,
37 | messages : chatHistory,
38 | count : chatHistory.length
39 | };
40 | }
41 |
42 | case 'CLEAR_CHAT':
43 | {
44 | return {
45 | ...state,
46 | messages : [],
47 | count : 0,
48 | countUnread : 0
49 | };
50 | }
51 |
52 | case 'SORT_CHAT':
53 | {
54 | const { order } = action.payload;
55 |
56 | return { ...state, order: order };
57 | }
58 |
59 | case 'SET_IS_SCROLL_END':
60 | {
61 | const { flag } = action.payload;
62 |
63 | return { ...state, isScrollEnd: flag };
64 | }
65 |
66 | case 'SET_IS_MESSAGE_READ':
67 | {
68 | const { id, isRead } = action.payload;
69 |
70 | state.messages.forEach((key, index) =>
71 | {
72 | if (state.messages[index].time === Number(id))
73 | {
74 | state.messages[index].isRead = isRead;
75 |
76 | state.countUnread--;
77 | }
78 | });
79 |
80 | return { ...state };
81 | }
82 |
83 | default:
84 | return state;
85 | }
86 | };
87 |
88 | export default chat;
89 |
--------------------------------------------------------------------------------
/app/src/store/reducers/producers.js:
--------------------------------------------------------------------------------
1 | const initialState = {};
2 |
3 | const producers = (state = initialState, action) =>
4 | {
5 | switch (action.type)
6 | {
7 | case 'ADD_PRODUCER':
8 | {
9 | const { producer } = action.payload;
10 |
11 | return { ...state, [producer.id]: producer };
12 | }
13 |
14 | case 'REMOVE_PRODUCER':
15 | {
16 | const { producerId } = action.payload;
17 | const newState = { ...state };
18 |
19 | delete newState[producerId];
20 |
21 | return newState;
22 | }
23 |
24 | case 'SET_PRODUCER_PAUSED':
25 | {
26 | const { producerId, originator } = action.payload;
27 | const producer = state[producerId];
28 |
29 | let newProducer;
30 |
31 | if (originator === 'local')
32 | newProducer = { ...producer, locallyPaused: true };
33 | else
34 | newProducer = { ...producer, remotelyPaused: true };
35 |
36 | return { ...state, [producerId]: newProducer };
37 | }
38 |
39 | case 'SET_PRODUCER_RESUMED':
40 | {
41 | const { producerId, originator } = action.payload;
42 | const producer = state[producerId];
43 |
44 | let newProducer;
45 |
46 | if (originator === 'local')
47 | newProducer = { ...producer, locallyPaused: false };
48 | else
49 | newProducer = { ...producer, remotelyPaused: false };
50 |
51 | return { ...state, [producerId]: newProducer };
52 | }
53 |
54 | case 'SET_PRODUCER_TRACK':
55 | {
56 | const { producerId, track } = action.payload;
57 | const producer = state[producerId];
58 | const newProducer = { ...producer, track };
59 |
60 | return { ...state, [producerId]: newProducer };
61 | }
62 |
63 | case 'SET_PRODUCER_SCORE':
64 | {
65 | const { producerId, score } = action.payload;
66 |
67 | const producer = state[producerId];
68 |
69 | const newProducer = { ...producer, score };
70 |
71 | return { ...state, [producerId]: newProducer };
72 | }
73 |
74 | default:
75 | return state;
76 | }
77 | };
78 |
79 | export default producers;
80 |
--------------------------------------------------------------------------------
/app/src/store/reducers/toolarea.js:
--------------------------------------------------------------------------------
1 | const initialState =
2 | {
3 | toolAreaOpen : false,
4 | currentToolTab : 'chat', // chat, settings, users
5 | unreadMessages : 0,
6 | unreadFiles : 0
7 | };
8 |
9 | const toolarea = (state = initialState, action) =>
10 | {
11 | switch (action.type)
12 | {
13 | case 'TOGGLE_TOOL_AREA':
14 | {
15 | const toolAreaOpen = !state.toolAreaOpen;
16 | const unreadMessages = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
17 | const unreadFiles = toolAreaOpen && state.currentToolTab === 'chat' ? 0 : state.unreadFiles;
18 |
19 | return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
20 | }
21 |
22 | case 'OPEN_TOOL_AREA':
23 | {
24 | const toolAreaOpen = true;
25 | const unreadMessages = state.currentToolTab === 'chat' ? 0 : state.unreadMessages;
26 | const unreadFiles = state.currentToolTab === 'chat' ? 0 : state.unreadFiles;
27 |
28 | return { ...state, toolAreaOpen, unreadMessages, unreadFiles };
29 | }
30 |
31 | case 'CLOSE_TOOL_AREA':
32 | {
33 | const toolAreaOpen = false;
34 |
35 | return { ...state, toolAreaOpen };
36 | }
37 |
38 | case 'SET_TOOL_TAB':
39 | {
40 | const { toolTab } = action.payload;
41 | const unreadMessages = toolTab === 'chat' ? 0 : state.unreadMessages;
42 | const unreadFiles = toolTab === 'chat' ? 0 : state.unreadFiles;
43 |
44 | return { ...state, currentToolTab: toolTab, unreadMessages, unreadFiles };
45 | }
46 |
47 | case 'ADD_MESSAGE':
48 | {
49 | if (state.toolAreaOpen && state.currentToolTab === 'chat')
50 | {
51 | return state;
52 | }
53 |
54 | return { ...state, unreadMessages: state.unreadMessages + 1 };
55 | }
56 |
57 | case 'ADD_FILE':
58 | {
59 | if (state.toolAreaOpen && state.currentToolTab === 'chat')
60 | {
61 | return state;
62 | }
63 |
64 | return { ...state, unreadFiles: state.unreadFiles + 1 };
65 | }
66 |
67 | default:
68 | return state;
69 | }
70 | };
71 |
72 | export default toolarea;
73 |
--------------------------------------------------------------------------------
/app/src/store/actions/consumerActions.js:
--------------------------------------------------------------------------------
1 | export const addConsumer = (consumer, peerId) =>
2 | ({
3 | type : 'ADD_CONSUMER',
4 | payload : { consumer, peerId }
5 | });
6 |
7 | export const removeConsumer = (consumerId, peerId) =>
8 | ({
9 | type : 'REMOVE_CONSUMER',
10 | payload : { consumerId, peerId }
11 | });
12 |
13 | export const clearConsumers = () =>
14 | ({
15 | type : 'CLEAR_CONSUMERS'
16 | });
17 |
18 | export const setConsumerPaused = (consumerId, originator) =>
19 | ({
20 | type : 'SET_CONSUMER_PAUSED',
21 | payload : { consumerId, originator }
22 | });
23 |
24 | export const setConsumerResumed = (consumerId, originator) =>
25 | ({
26 | type : 'SET_CONSUMER_RESUMED',
27 | payload : { consumerId, originator }
28 | });
29 |
30 | export const setConsumerCurrentLayers = (consumerId, spatialLayer, temporalLayer) =>
31 | ({
32 | type : 'SET_CONSUMER_CURRENT_LAYERS',
33 | payload : { consumerId, spatialLayer, temporalLayer }
34 | });
35 |
36 | export const setConsumerPreferredLayers = (consumerId, spatialLayer, temporalLayer) =>
37 | ({
38 | type : 'SET_CONSUMER_PREFERRED_LAYERS',
39 | payload : { consumerId, spatialLayer, temporalLayer }
40 | });
41 |
42 | export const setConsumerPriority = (consumerId, priority) =>
43 | ({
44 | type : 'SET_CONSUMER_PRIORITY',
45 | payload : { consumerId, priority }
46 | });
47 |
48 | export const setConsumerTrack = (consumerId, track) =>
49 | ({
50 | type : 'SET_CONSUMER_TRACK',
51 | payload : { consumerId, track }
52 | });
53 |
54 | export const setConsumerScore = (consumerId, score) =>
55 | ({
56 | type : 'SET_CONSUMER_SCORE',
57 | payload : { consumerId, score }
58 | });
59 |
60 | export const setConsumerAudioGain = (consumerId, audioGain) =>
61 | ({
62 | type : 'SET_CONSUMER_AUDIO_GAIN',
63 | payload : { consumerId, audioGain }
64 | });
65 |
66 | export const setConsumerOpusConfig = (consumerId, opusConfig) =>
67 | ({
68 | type : 'SET_CONSUMER_OPUS_CONFIG',
69 | payload : { consumerId, opusConfig }
70 | });
71 |
--------------------------------------------------------------------------------
/compose/config/nginx.conf:
--------------------------------------------------------------------------------
1 | map $http_upgrade $connection_upgrade {
2 | default upgrade;
3 | '' close;
4 | }
5 |
6 | server {
7 | listen 80 default_server;
8 | listen [::]:80 default_server;
9 | listen 443 ssl http2 default_server;
10 | listen [::]:443 ssl http2 default_server;
11 | resolver 127.0.0.11:53 ipv6=off;
12 | #
13 | ssl_certificate /etc/nginx/cert.pem;
14 | ssl_certificate_key /etc/nginx/key.pem;
15 | ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
16 |
17 | # server
18 | location ~ ^/(socket\.io) {
19 | proxy_pass https://edumeet:3443;
20 | #
21 | proxy_http_version 1.1;
22 | proxy_redirect off;
23 | proxy_request_buffering off;
24 | proxy_set_header Upgrade $http_upgrade;
25 | proxy_set_header Connection $connection_upgrade;
26 | proxy_read_timeout 86400;
27 | proxy_set_header Referer $http_referer;
28 | proxy_set_header Host $http_host;
29 | proxy_set_header X-Real-IP $remote_addr;
30 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
31 | proxy_set_header X-Forwarded-Proto $scheme;
32 | proxy_max_temp_file_size 2048m;
33 | }
34 |
35 | # app
36 | location ~ ^/(sockjs-node) {
37 | proxy_pass https://edumeet:4443;
38 | #
39 | proxy_http_version 1.1;
40 | proxy_redirect off;
41 | proxy_request_buffering off;
42 | proxy_set_header Upgrade $http_upgrade;
43 | proxy_set_header Connection $connection_upgrade;
44 | proxy_read_timeout 86400;
45 | proxy_set_header Referer $http_referer;
46 | proxy_set_header Host $http_host;
47 | proxy_set_header X-Real-IP $remote_addr;
48 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
49 | proxy_set_header X-Forwarded-Proto $scheme;
50 | proxy_max_temp_file_size 2048m;
51 | }
52 |
53 | location / {
54 | proxy_pass https://edumeet:4443;
55 | }
56 | }
--------------------------------------------------------------------------------
/server/certs/edumeet-demo-cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFhTCCA22gAwIBAgIUKWAvYO7nVQFx8sxCwR4K8hlj/GEwDQYJKoZIhvcNAQEL
3 | BQAwUjELMAkGA1UEBhMCRVUxEDAOBgNVBAgMB2VkdW1lZXQxDTALBgNVBAcMBGRl
4 | bW8xEDAOBgNVBAoMB2VkdW1lZXQxEDAOBgNVBAMMB2VkdW1lZXQwHhcNMjIwMjI2
5 | MjIxNzQ0WhcNMjMwMjI2MjIxNzQ0WjBSMQswCQYDVQQGEwJFVTEQMA4GA1UECAwH
6 | ZWR1bWVldDENMAsGA1UEBwwEZGVtbzEQMA4GA1UECgwHZWR1bWVldDEQMA4GA1UE
7 | AwwHZWR1bWVldDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALNo8HRJ
8 | LVKW2ngMjAoFT6Dh1gP0eivscqHKEdc4LHWtoYjMpUNPbXH2E3+sc5NHTmYUm+e9
9 | /D+xJ3vF9EOB4W7IY2EROPeLF3mxSGizJMst3/p7rbOG+1qErODd1o21oy1fsJHS
10 | 1HB1udeYKRyTpsGrlc8faMGBnwkqaJm9SwuUU75sreTSdDwZEVGs49OSGZPKo42S
11 | J2gcYbEDUw/cUZRcueck/fN5Wx/05TWsMUKZ/kbsFgWrdfrZvMTfyrs0XgBTRUc0
12 | P9IdQAHRkOtFQZ18xAgsbJ/9KFgz429lb4BZrEZEjVOrhQ1Vq1H55rNb5+urFzFP
13 | xFBqJGA1I4I3D6pcP+RIphWEbDGWelBQ4L/PaLRIJw6ShdpEfTPZYjZro7kbMxJC
14 | JEoqrJChu0JO+l+N8qtLkpnghgrI8I5FfZKxDEuGHk6YnQWIeusY9PZ91gyFpy1S
15 | Y2Kq8e4mpuF/jO/857UXo2JQhGsRPItHvj9AeqmjjZr2dm1mIqWGTmPyNpLm5sWA
16 | bsFtLgBTKfpgErugh6KvK15jWVhl3i0Ls9C5feU2wU7dk61qH1RiOU8gBeZiYW+6
17 | kD8WSddd8PEiYHMZ0peMMJoFfbl+02uCLi4LACWzwl2Gm9JkUgL7vX0SOA96F2f+
18 | +QyLLvIB+nhhMaitw0QvT+Hugab4yXBJBFvZAgMBAAGjUzBRMB0GA1UdDgQWBBQy
19 | a2ibEZ7zhfBF/KZNUrBO0wIIODAfBgNVHSMEGDAWgBQya2ibEZ7zhfBF/KZNUrBO
20 | 0wIIODAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAKbNSY1dnm
21 | gj36y4+XJ2CC7o3JtY17sjCRpQxgj/vIoX72OyV5HYbkwGq9k4jJ00fIEuzojuGa
22 | i4ZuxYgVfdZG2W2/tI8H8G/JfNkolm2sOX8SUmAEbUsgCiT32Er/y9Asm0UY/sWA
23 | t6TPV0dxrcH58yHOU0Wgu2MPcaidBxc3MAE12MHoIOk4ioCrgvJQyCszJRBSTSd1
24 | UO6L1pMBzV6/ElxalU64lEO9/v25KBDAeI96g5jEYIEz9Wo1xLeflU3Ug6/XyytM
25 | 4egYwKZpHNKzUOTr1eq1tBbaRY4nBGNVR0MIwSskgahzCR1ieYPvSXGceme9a4Ig
26 | bXkFuOo+xEVFXCgdImK+TuDEhOlH/ioP8v9zUqwXHGWzO95gtY2lnPufFJjSvXL3
27 | BicYHBQ2G37rcv7cDLF3JuJRzn7PrWtBB2OOFvixQHP9H7ynOMf00myFUG8q4isC
28 | q1/ZP79/uEB3Aq+1cMUTaoDt7uFIqSTowQiOi6aInW6eqeToJETR0wVIGkOgieMC
29 | +qgS1lKQGqKNOgsuz4BQub/lr9jhOrmDMNGI82ZNNmRlLJgN8zxf5ZCnO7OF+WCr
30 | +9vzJ8MJa4gO9EAWWeJQi9hyrmLIY9mA1pOhPCcROXhc5eyz/Q9q2AWSZlmDeN3e
31 | UWbGZK1XT8BAIEcpJZczbYt/XJKnQ8K2HA==
32 | -----END CERTIFICATE-----
33 |
--------------------------------------------------------------------------------
/app/src/components/ConfigError.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-use-before-define
2 | import { withStyles } from '@material-ui/core/styles';
3 | import PropTypes from 'prop-types';
4 | import { FormattedMessage } from 'react-intl';
5 |
6 | import Dialog from '@material-ui/core/Dialog';
7 | import DialogTitle from '@material-ui/core/DialogTitle';
8 | import DialogContent from '@material-ui/core/DialogContent';
9 | import Grid from '@material-ui/core/Grid';
10 | import ErrorIcon from '@material-ui/icons/Error';
11 | import Button from '@material-ui/core/Button';
12 |
13 | const styles = () =>
14 | ({
15 | error : {
16 | color : 'red'
17 | }
18 | });
19 |
20 | const ConfigError = ({
21 | classes,
22 | configError
23 | }: {
24 | classes : any;
25 | configError : string;
26 | }) =>
27 | {
28 | return (
29 |
66 | );
67 | };
68 |
69 | ConfigError.propTypes =
70 | {
71 | classes : PropTypes.object.isRequired,
72 | configError : PropTypes.string.isRequired
73 | };
74 |
75 | export default withStyles(styles)(ConfigError);
76 |
--------------------------------------------------------------------------------
/server/lib/stats/promExporter.js:
--------------------------------------------------------------------------------
1 | import Logger from '../logger/Logger';
2 |
3 | const express = require('express');
4 | const promClient = require('prom-client');
5 |
6 | const collectDefaultMetrics = require('../stats/metrics/default');
7 | const RegisterAggregated = require('../stats/metrics/aggregated');
8 |
9 | const logger = new Logger('promClient');
10 |
11 | import { config } from '../config/config';
12 |
13 | module.exports = async function(workers, rooms, peers)
14 | {
15 | try
16 | {
17 | logger.debug(`config.prometheus.deidentify=${config.prometheus.deidentify}`);
18 | logger.debug(`config.prometheus.listen=${config.prometheus.listen}`);
19 | logger.debug(`config.prometheus.numeric=${config.prometheus.numeric}`);
20 | logger.debug(`config.prometheus.port=${config.prometheus.port}`);
21 | logger.debug(`config.prometheus.quiet=${config.prometheus.quiet}`);
22 |
23 | const app = express();
24 |
25 | // default register
26 | app.get('/', async (req, res) =>
27 | {
28 | logger.debug(`GET ${req.originalUrl}`);
29 | const registry = new promClient.Registry();
30 |
31 | await collectDefaultMetrics(
32 | workers, rooms, peers, registry, config.prometheus);
33 | res.set('Content-Type', registry.contentType);
34 | const data = await registry.metrics();
35 |
36 | res.end(data);
37 | });
38 |
39 | // aggregated register
40 | const registerAggregated = RegisterAggregated(
41 | workers, rooms, peers, config.prometheus);
42 |
43 | app.get('/metrics', async (req, res) =>
44 | {
45 | logger.debug(`GET ${req.originalUrl}`);
46 |
47 | if (config.prometheus.secret
48 | && req.headers.authorization !== `Bearer ${ config.prometheus.secret}`)
49 | {
50 | logger.error('Invalid authorization header');
51 |
52 | return res.status(401).end();
53 | }
54 |
55 | res.set('Content-Type', registerAggregated.contentType);
56 | const data = await registerAggregated.metrics();
57 |
58 | res.end(data);
59 | });
60 |
61 | const server = app.listen(config.prometheus.port, config.prometheus.listen, () =>
62 | {
63 | const address = server.address();
64 |
65 | logger.info(`listening ${address.address}:${address.port}`);
66 | });
67 | }
68 | catch (err)
69 | {
70 | logger.error(err);
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | edumeet
17 |
18 |
19 |
20 |
21 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/__tests__/App.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { Route, MemoryRouter } from 'react-router-dom';
5 | import { act } from 'react-dom/test-utils';
6 | import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
7 | import App from '../components/App';
8 | import ChooseRoom from '../components/ChooseRoom';
9 | import RoomContext from '../RoomContext';
10 |
11 | import configureStore from 'redux-mock-store';
12 |
13 | const mockStore = configureStore([]);
14 |
15 | let container;
16 |
17 | let store;
18 |
19 | let intl;
20 |
21 | const roomClient = {};
22 |
23 | beforeEach(() =>
24 | {
25 | container = document.createElement('div');
26 |
27 | store = mockStore({
28 | me : {
29 | displayNameInProgress : false,
30 | id : 'jesttester',
31 | loggedIn : false,
32 | loginEnabled : true
33 | },
34 | room : {
35 | },
36 | settings : {
37 | displayName : 'Jest Tester'
38 | }
39 | });
40 |
41 | const cache = createIntlCache();
42 |
43 | const locale = 'en';
44 |
45 | intl = createIntl({
46 | locale,
47 | messages : {}
48 | }, cache);
49 |
50 | document.body.appendChild(container);
51 | });
52 |
53 | afterEach(() =>
54 | {
55 | document.body.removeChild(container);
56 | container = null;
57 | });
58 |
59 | describe('', () =>
60 | {
61 | test('renders chooseroom', () =>
62 | {
63 | act(() =>
64 | {
65 | ReactDOM.render(
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | ,
75 | container);
76 | });
77 | });
78 | });
79 |
80 | describe('', () =>
81 | {
82 | test('renders joindialog', () =>
83 | {
84 | act(() =>
85 | {
86 | ReactDOM.render(
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | ,
96 | container);
97 | });
98 | });
99 | });
--------------------------------------------------------------------------------
/app/src/components/appPropTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export const Room = PropTypes.shape(
4 | {
5 | state : PropTypes.oneOf(
6 | [ 'new', 'connecting', 'connected', 'closed' ]).isRequired,
7 | activeSpeakerId : PropTypes.string
8 | });
9 |
10 | export const Me = PropTypes.shape(
11 | {
12 | id : PropTypes.string.isRequired,
13 | canSendMic : PropTypes.bool.isRequired,
14 | canSendWebcam : PropTypes.bool.isRequired,
15 | webcamInProgress : PropTypes.bool.isRequired
16 | });
17 |
18 | export const Producer = PropTypes.shape(
19 | {
20 | id : PropTypes.string.isRequired,
21 | source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
22 | deviceLabel : PropTypes.string,
23 | type : PropTypes.oneOf([ 'front', 'back', 'screen', 'extravideo' ]),
24 | paused : PropTypes.bool.isRequired,
25 | track : PropTypes.any,
26 | codec : PropTypes.string.isRequired
27 | });
28 |
29 | export const Peer = PropTypes.shape(
30 | {
31 | id : PropTypes.string.isRequired,
32 | displayName : PropTypes.string,
33 | consumers : PropTypes.arrayOf(PropTypes.string).isRequired
34 | });
35 |
36 | export const Consumer = PropTypes.shape(
37 | {
38 | id : PropTypes.string.isRequired,
39 | peerId : PropTypes.string.isRequired,
40 | source : PropTypes.oneOf([ 'mic', 'webcam', 'screen', 'extravideo' ]).isRequired,
41 | locallyPaused : PropTypes.bool.isRequired,
42 | remotelyPaused : PropTypes.bool.isRequired,
43 | width : PropTypes.number,
44 | height : PropTypes.number,
45 | profile : PropTypes.oneOf([ 'none', 'default', 'low', 'medium', 'high' ]),
46 | track : PropTypes.any,
47 | codec : PropTypes.string
48 | });
49 |
50 | export const Notification = PropTypes.shape(
51 | {
52 | id : PropTypes.string.isRequired,
53 | type : PropTypes.oneOf([ 'info', 'error' ]).isRequired,
54 | timeout : PropTypes.number
55 | });
56 |
57 | export const Message = PropTypes.shape(
58 | {
59 | type : PropTypes.string,
60 | component : PropTypes.string,
61 | text : PropTypes.string,
62 | sender : PropTypes.string
63 | });
64 |
65 | export const FileEntryProps = PropTypes.shape(
66 | {
67 | data : PropTypes.shape({
68 | id : PropTypes.string.isRequired,
69 | picture : PropTypes.string,
70 | file : PropTypes.shape({
71 | magnet : PropTypes.string.isRequired
72 | }).isRequired,
73 | me : PropTypes.bool
74 | }).isRequired,
75 | notify : PropTypes.func.isRequired
76 | });
--------------------------------------------------------------------------------
/app/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | const isLocalhost = Boolean(
2 | window.location.hostname === 'localhost' ||
3 | window.location.hostname === '[::1]' ||
4 | window.location.hostname.match(
5 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
6 | )
7 | );
8 |
9 | export function register(config)
10 | {
11 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator)
12 | {
13 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
14 |
15 | if (publicUrl.origin !== window.location.origin)
16 | return;
17 |
18 | window.addEventListener('load', () =>
19 | {
20 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
21 |
22 | if (isLocalhost)
23 | {
24 | checkValidServiceWorker(swUrl, config);
25 |
26 | navigator.serviceWorker.ready.then(() =>
27 | {});
28 | }
29 | else
30 | {
31 | registerValidSW(swUrl, config);
32 | }
33 | });
34 | }
35 | }
36 |
37 | function registerValidSW(swUrl, config)
38 | {
39 | navigator.serviceWorker
40 | .register(swUrl)
41 | .then((registration) =>
42 | {
43 | registration.onupdatefound = () =>
44 | {
45 | const installingWorker = registration.installing;
46 |
47 | if (installingWorker == null)
48 | return;
49 |
50 | installingWorker.onstatechange = () =>
51 | {
52 | if (installingWorker.state === 'installed')
53 | {
54 | if (navigator.serviceWorker.controller)
55 | {
56 | if (config && config.onUpdate)
57 | config.onUpdate(registration);
58 | }
59 | else if (config && config.onSuccess)
60 | config.onSuccess(registration);
61 | }
62 | };
63 | };
64 | })
65 | .catch(() =>
66 | {});
67 | }
68 |
69 | function checkValidServiceWorker(swUrl, config)
70 | {
71 | fetch(swUrl)
72 | .then((response) =>
73 | {
74 | const contentType = response.headers.get('content-type');
75 |
76 | if (response.status === 404 ||
77 | (contentType != null && contentType.indexOf('javascript') === -1))
78 | {
79 | navigator.serviceWorker.ready.then((registration) =>
80 | {
81 | registration.unregister().then(() =>
82 | {
83 | window.location.reload();
84 | });
85 | });
86 | }
87 | else
88 | {
89 | registerValidSW(swUrl, config);
90 | }
91 | })
92 | .catch(() =>
93 | {});
94 | }
95 |
96 | export function unregister()
97 | {
98 | if ('serviceWorker' in navigator)
99 | {
100 | navigator.serviceWorker.ready.then((registration) =>
101 | {
102 | registration.unregister();
103 | });
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/prom.md:
--------------------------------------------------------------------------------
1 | # Prometheus exporter
2 |
3 | The goal of this version is to offer a few basic metrics for
4 | initial testing. The set of supported metrics can be extended.
5 |
6 | The current implementation is partly
7 | [unconventional](https://prometheus.io/docs/instrumenting/writing_exporters)
8 | in that it creates new metrics each time but does not register a
9 | custom collector. Reasons are that the exporter should
10 | [clear out metrics](https://github.com/prometheus/client_python/issues/182)
11 | for closed connections but that `prom-client`
12 | [does not yet support](https://github.com/siimon/prom-client/issues/241)
13 | custom collectors.
14 |
15 | This version has been ported from an earlier Python version that was not part
16 | of `edumeet` but connected as an interactive client.
17 |
18 | ## Configuration
19 |
20 | See `prometheus` in `server/config/config.example.js` for options and
21 | applicable defaults.
22 |
23 | If `edumeet` was installed with
24 | [`mm-absible`](https://github.com/edumeet/edumeet-ansible)
25 | it may be necessary to open the `iptables` firewall for incoming TCP traffic
26 | on the allocated port (see `/etc/ferm/ferm.conf`).
27 |
28 | ## Metrics
29 |
30 | | metric | value |
31 | |--------|-------|
32 | | `edumeet_peers`| |
33 | | `edumeet_rooms`| |
34 | | `mediasoup_consumer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) |
35 | | `mediasoup_consumer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Consumer-Statistics) |
36 | | `mediasoup_producer_byte_count_bytes`| [`byteCount`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) |
37 | | `mediasoup_producer_score`| [`score`](https://mediasoup.org/documentation/v3/mediasoup/rtc-statistics/#Producer-Statistics) |
38 |
39 | ## Architecture
40 |
41 | ```
42 | +-----------+ +---------------------------------------------+
43 | | workers | | server observer API |
44 | | | sock | +------o------+----o-----+
45 | | +------+ | int. server | exporter |
46 | | | | | | |
47 | | mediasoup | | express socket.io | net | express |
48 | +-----+-----+ +----+---------+-----+-----+-------+-----+----+
49 | ^ min-max ^ 443 ^ 443 ^ sock ^ 8889
50 | | RTP | HTTPS | ws | | HTTP
51 | | | | | |
52 | | +-+---------+-+ +------+------+ +---+--------+
53 | +---------------+ app | | int. client | | Prometheus |
54 | +-------------+ +-------------+ +------------+
55 | ```
56 |
--------------------------------------------------------------------------------
/app/src/components/FullScreen.js:
--------------------------------------------------------------------------------
1 | const key = {
2 | fullscreenEnabled : 0,
3 | fullscreenElement : 1,
4 | requestFullscreen : 2,
5 | exitFullscreen : 3,
6 | fullscreenchange : 4,
7 | fullscreenerror : 5
8 | };
9 |
10 | const webkit = [
11 | 'webkitFullscreenEnabled',
12 | 'webkitFullscreenElement',
13 | 'webkitRequestFullscreen',
14 | 'webkitExitFullscreen',
15 | 'webkitfullscreenchange',
16 | 'webkitfullscreenerror'
17 | ];
18 |
19 | const moz = [
20 | 'mozFullScreenEnabled',
21 | 'mozFullScreenElement',
22 | 'mozRequestFullScreen',
23 | 'mozCancelFullScreen',
24 | 'mozfullscreenchange',
25 | 'mozfullscreenerror'
26 | ];
27 |
28 | const ms = [
29 | 'msFullscreenEnabled',
30 | 'msFullscreenElement',
31 | 'msRequestFullscreen',
32 | 'msExitFullscreen',
33 | 'MSFullscreenChange',
34 | 'MSFullscreenError'
35 | ];
36 |
37 | export default class FullScreen
38 | {
39 | constructor(document)
40 | {
41 | this.document = document;
42 | this.vendor = (
43 | ('fullscreenEnabled' in this.document && Object.keys(key)) ||
44 | (webkit[0] in this.document && webkit) ||
45 | (moz[0] in this.document && moz) ||
46 | (ms[0] in this.document && ms) ||
47 | []
48 | );
49 | }
50 |
51 | requestFullscreen(element)
52 | {
53 | element[this.vendor[key.requestFullscreen]]();
54 | }
55 |
56 | requestFullscreenFunction(element)
57 | {
58 | // eslint-disable-next-line
59 | element[this.vendor[key.requestFullscreen]];
60 | }
61 |
62 | addEventListener(type, handler)
63 | {
64 | this.document.addEventListener(this.vendor[key[type]], handler);
65 | }
66 |
67 | removeEventListener(type, handler)
68 | {
69 | this.document.removeEventListener(this.vendor[key[type]], handler);
70 | }
71 |
72 | get exitFullscreen()
73 | {
74 | return this.document[this.vendor[key.exitFullscreen]].bind(this.document);
75 | }
76 |
77 | get fullscreenEnabled()
78 | {
79 | return Boolean(this.document[this.vendor[key.fullscreenEnabled]]);
80 | }
81 | set fullscreenEnabled(val) {}
82 |
83 | get fullscreenElement()
84 | {
85 | return this.document[this.vendor[key.fullscreenElement]];
86 | }
87 | set fullscreenElement(val) {}
88 |
89 | get onfullscreenchange()
90 | {
91 | return this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()];
92 | }
93 |
94 | set onfullscreenchange(handler)
95 | {
96 | this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()] = handler;
97 | }
98 |
99 | get onfullscreenerror()
100 | {
101 | return this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()];
102 | }
103 |
104 | set onfullscreenerror(handler)
105 | {
106 | this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()] = handler;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/app/src/components/VideoWindow/VideoWindow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import NewWindow from './NewWindow';
4 | import PropTypes from 'prop-types';
5 | import * as appPropTypes from '../appPropTypes';
6 | import * as roomActions from '../../store/actions/roomActions';
7 | import FullView from '../VideoContainers/FullView';
8 |
9 | const VideoWindow = (props) =>
10 | {
11 | const {
12 | advancedMode,
13 | consumer,
14 | aspectRatio,
15 | toggleConsumerWindow
16 | } = props;
17 |
18 | if (!consumer)
19 | return null;
20 |
21 | const consumerVisible = (
22 | Boolean(consumer) &&
23 | !consumer.locallyPaused &&
24 | !consumer.remotelyPaused
25 | );
26 |
27 | return (
28 |
29 |
51 |
52 | );
53 | };
54 |
55 | VideoWindow.propTypes =
56 | {
57 | advancedMode : PropTypes.bool,
58 | consumer : appPropTypes.Consumer,
59 | aspectRatio : PropTypes.number.isRequired,
60 | toggleConsumerWindow : PropTypes.func.isRequired
61 | };
62 |
63 | const mapStateToProps = (state) =>
64 | {
65 | return {
66 | consumer : state.consumers[state.room.windowConsumer],
67 | aspectRatio : state.settings.aspectRatio
68 | };
69 | };
70 |
71 | const mapDispatchToProps = (dispatch) =>
72 | {
73 | return {
74 | toggleConsumerWindow : () =>
75 | {
76 | dispatch(roomActions.toggleConsumerWindow());
77 | }
78 | };
79 | };
80 |
81 | const VideoWindowContainer = connect(
82 | mapStateToProps,
83 | mapDispatchToProps,
84 | null,
85 | {
86 | areStatesEqual : (next, prev) =>
87 | {
88 | return (
89 | prev.consumers[prev.room.windowConsumer] ===
90 | next.consumers[next.room.windowConsumer] &&
91 | prev.settings.aspectRatio === next.settings.aspectRatio
92 | );
93 | }
94 | }
95 | )(VideoWindow);
96 |
97 | export default VideoWindowContainer;
98 |
--------------------------------------------------------------------------------
/app/src/images/buddy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
70 |
--------------------------------------------------------------------------------
/app/src/store/actions/meActions.js:
--------------------------------------------------------------------------------
1 | export const setMe = ({ peerId, loginEnabled }) =>
2 | ({
3 | type : 'SET_ME',
4 | payload : { peerId, loginEnabled }
5 | });
6 |
7 | export const setBrowser = (browser) =>
8 | ({
9 | type : 'SET_BROWSER',
10 | payload : { browser }
11 | });
12 |
13 | export const loggedIn = (flag) =>
14 | ({
15 | type : 'LOGGED_IN',
16 | payload : { flag }
17 | });
18 |
19 | export const addRole = (roleId) =>
20 | ({
21 | type : 'ADD_ROLE',
22 | payload : { roleId }
23 | });
24 |
25 | export const removeRole = (roleId) =>
26 | ({
27 | type : 'REMOVE_ROLE',
28 | payload : { roleId }
29 | });
30 |
31 | export const setPicture = (picture) =>
32 | ({
33 | type : 'SET_PICTURE',
34 | payload : { picture }
35 | });
36 |
37 | export const setMediaCapabilities = ({
38 | canSendMic,
39 | canSendWebcam,
40 | canShareScreen,
41 | canShareFiles
42 | }) =>
43 | ({
44 | type : 'SET_MEDIA_CAPABILITIES',
45 | payload : { canSendMic, canSendWebcam, canShareScreen, canShareFiles }
46 | });
47 |
48 | export const setAudioDevices = (devices) =>
49 | ({
50 | type : 'SET_AUDIO_DEVICES',
51 | payload : { devices }
52 | });
53 |
54 | export const setAudioOutputDevices = (devices) =>
55 | ({
56 | type : 'SET_AUDIO_OUTPUT_DEVICES',
57 | payload : { devices }
58 | });
59 |
60 | export const setWebcamDevices = (devices) =>
61 | ({
62 | type : 'SET_WEBCAM_DEVICES',
63 | payload : { devices }
64 | });
65 |
66 | export const setRaisedHand = (flag) =>
67 | ({
68 | type : 'SET_RAISED_HAND',
69 | payload : { flag }
70 | });
71 |
72 | export const setAudioInProgress = (flag) =>
73 | ({
74 | type : 'SET_AUDIO_IN_PROGRESS',
75 | payload : { flag }
76 | });
77 |
78 | export const setAudioOutputInProgress = (flag) =>
79 | ({
80 | type : 'SET_AUDIO_OUTPUT_IN_PROGRESS',
81 | payload : { flag }
82 | });
83 |
84 | export const setWebcamInProgress = (flag) =>
85 | ({
86 | type : 'SET_WEBCAM_IN_PROGRESS',
87 | payload : { flag }
88 | });
89 |
90 | export const setScreenShareInProgress = (flag) =>
91 | ({
92 | type : 'SET_SCREEN_SHARE_IN_PROGRESS',
93 | payload : { flag }
94 | });
95 |
96 | export const setRaisedHandInProgress = (flag) =>
97 | ({
98 | type : 'SET_RAISED_HAND_IN_PROGRESS',
99 | payload : { flag }
100 | });
101 |
102 | export const setDisplayNameInProgress = (flag) =>
103 | ({
104 | type : 'SET_DISPLAY_NAME_IN_PROGRESS',
105 | payload : { flag }
106 | });
107 |
108 | export const setIsSpeaking = (flag) =>
109 | ({
110 | type : 'SET_IS_SPEAKING',
111 | payload : { flag }
112 | });
113 |
114 | export const setAutoMuted = (flag) =>
115 | ({
116 | type : 'SET_AUTO_MUTED',
117 | payload : { flag }
118 | });
119 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "edumeet-server",
3 | "version": "3.5.1",
4 | "private": true,
5 | "description": "edumeet server",
6 | "author": "Håvar Aambø Fosstveit ",
7 | "contributors": [
8 | "Stefan Otto",
9 | "Mészáros Mihály",
10 | "Roman Drozd",
11 | "Rémai Gábor László",
12 | "Piotr Pawałowski"
13 | ],
14 | "license": "MIT",
15 | "main": "lib/index.js",
16 | "scripts": {
17 | "start": "node dist/server.js",
18 | "build": "mkdir -p dist && find dist/* -maxdepth 0 ! -name public -exec rm -rf {} \\; && tsc && ln -s ../certs dist/certs && chmod 755 dist/server.js && ( for fileExt in yaml json toml ; do [ -f config/config.$fileExt ] && cp config/config.$fileExt dist/config/; done ) | true && touch 'dist/ __AUTO_GENERATED_CONTENT_REFRESHED_AFTER_REBUILDING!__ '",
19 | "dev": "nodemon --exec ts-node --ignore dist/ -e js,ts server.js",
20 | "connect": "ts-node connect.js",
21 | "lint": "eslint ./ --ext .js,.ts; exit 0",
22 | "lint-fix": "eslint ./ --fix --ext .js,.ts; exit 0",
23 | "gen-config-docs": "ts-node utils/gen-config-docs.ts"
24 | },
25 | "dependencies": {
26 | "awaitqueue": "^1.0.0",
27 | "axios": "^0.21.1",
28 | "base-64": "^0.1.0",
29 | "bcrypt": "^5.0.1",
30 | "body-parser": "^1.19.0",
31 | "colors": "^1.4.0",
32 | "compression": "^1.7.4",
33 | "connect-redis": "^4.0.3",
34 | "convict": "^6.1.0",
35 | "convict-format-with-validator": "^6.0.1",
36 | "cookie-parser": "^1.4.4",
37 | "debug": "^4.3.1",
38 | "express": "^4.17.1",
39 | "express-session": "^1.17.0",
40 | "express-socket.io-session": "^1.3.5",
41 | "fast-stats": "^0.0.6",
42 | "helmet": "^3.21.2",
43 | "ims-lti": "^3.0.2",
44 | "json5": "^2.2.0",
45 | "jsonwebtoken": "^8.5.1",
46 | "mediasoup": "3.9.6",
47 | "openid-client": "^3.7.3",
48 | "passport": "^0.4.0",
49 | "passport-local": "^1.0.0",
50 | "passport-lti": "0.0.7",
51 | "passport-saml": "^3.1.0",
52 | "pidusage": "^2.0.21",
53 | "prom-client": "^13.1.0",
54 | "redis": "v3",
55 | "socket.io": "^2.4.0",
56 | "spdy": "^4.0.2",
57 | "toml": "^3.0.0",
58 | "uuid": "^7.0.2",
59 | "yaml": "^1.10.2"
60 | },
61 | "devDependencies": {
62 | "@types/base-64": "^0.1.3",
63 | "@types/body-parser": "^1.19.0",
64 | "@types/compression": "^1.7.0",
65 | "@types/connect-redis": "^0.0.18",
66 | "@types/cookie-parser": "^1.4.2",
67 | "@types/debug": "^4.1.5",
68 | "@types/express": "^4.17.11",
69 | "@types/express-session": "^1.17.3",
70 | "@types/jsonwebtoken": "^8.5.1",
71 | "@types/node": "^14.14.37",
72 | "@types/passport": "^1.0.6",
73 | "@types/passport-local": "^1.0.33",
74 | "@types/uuid": "^8.3.0",
75 | "@typescript-eslint/eslint-plugin": "^4.21.0",
76 | "@typescript-eslint/parser": "^4.21.0",
77 | "eslint": "6.8.0",
78 | "eslint-plugin-import": "^2.22.1",
79 | "ts-node": "^9.1.1",
80 | "typescript": "^4.2.4"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/components/PeerAudio/PeerAudio.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class PeerAudio extends React.PureComponent
5 | {
6 | constructor(props)
7 | {
8 | super(props);
9 |
10 | // Latest received audio track.
11 | // @type {MediaStreamTrack}
12 | this._audioTrack = null;
13 | this._audioOutputDevice = null;
14 | this._gainNode = null;
15 | }
16 |
17 | render()
18 | {
19 | return (
20 |
24 | );
25 | }
26 |
27 | componentDidMount()
28 | {
29 | const { audioTrack, audioOutputDevice, audioGain } = this.props;
30 |
31 | this._setTrack(audioTrack);
32 | this._setOutputDevice(audioOutputDevice);
33 | this._setAudioGain(audioGain);
34 | }
35 |
36 | componentDidUpdate(prevProps)
37 | {
38 | if (prevProps !== this.props)
39 | {
40 | const { audioTrack, audioOutputDevice, audioGain } = this.props;
41 |
42 | this._setTrack(audioTrack);
43 | this._setOutputDevice(audioOutputDevice);
44 | this._setAudioGain(audioGain);
45 | }
46 | }
47 |
48 | _setTrack(audioTrack)
49 | {
50 | if (this._audioTrack === audioTrack)
51 | return;
52 |
53 | this._audioTrack = audioTrack;
54 |
55 | const { audio } = this.refs;
56 |
57 | if (audioTrack)
58 | {
59 | const stream = new MediaStream();
60 |
61 | stream.addTrack(audioTrack);
62 | audio.srcObject = stream;
63 |
64 | }
65 | else
66 | {
67 | audio.srcObject = null;
68 | }
69 | }
70 |
71 | _setOutputDevice(audioOutputDevice)
72 | {
73 | if (this._audioOutputDevice === audioOutputDevice)
74 | return;
75 |
76 | this._audioOutputDevice = audioOutputDevice;
77 |
78 | const { audio } = this.refs;
79 |
80 | if (audioOutputDevice && typeof audio.setSinkId === 'function')
81 | audio.setSinkId(audioOutputDevice);
82 | }
83 |
84 | _setAudioGain(audioGain)
85 | {
86 | if (audioGain === undefined)
87 | {
88 | return;
89 | }
90 |
91 | if (this._gainNode == null)
92 | {
93 | const { audio } = this.refs;
94 |
95 | if (!audio.srcObject)
96 | {
97 | return;
98 | }
99 |
100 | const AudioContext = window.AudioContext || window.webkitAudioContext,
101 | audioCtx = new AudioContext(),
102 | src = audioCtx.createMediaStreamSource(audio.srcObject);
103 |
104 | /* dst = audioCtx.createMediaStreamDestination() */
105 |
106 | this._gainNode = audioCtx.createGain();
107 | src.connect(this._gainNode);
108 | this._gainNode.connect(audioCtx.destination);
109 | audio.volume = 0;
110 | }
111 |
112 | this._gainNode.gain.value = audioGain;
113 | }
114 | }
115 |
116 | PeerAudio.propTypes =
117 | {
118 | audioTrack : PropTypes.any,
119 | audioOutputDevice : PropTypes.string,
120 | audioGain : PropTypes.number
121 | };
122 |
--------------------------------------------------------------------------------
/app/src/components/VideoContainers/FullView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classnames from 'classnames';
4 | import { withStyles } from '@material-ui/core/styles';
5 |
6 | const styles = () =>
7 | ({
8 | root :
9 | {
10 | position : 'relative',
11 | flex : '100 100 auto',
12 | height : '100%',
13 | width : '100%',
14 | display : 'flex',
15 | flexDirection : 'column',
16 | overflow : 'hidden'
17 | },
18 | video :
19 | {
20 | flex : '100 100 auto',
21 | height : '100%',
22 | width : '100%',
23 | objectFit : 'contain',
24 | userSelect : 'none',
25 | transitionProperty : 'opacity',
26 | transitionDuration : '.15s',
27 | backgroundColor : 'rgba(0, 0, 0, 1)',
28 | '&.hidden' :
29 | {
30 | opacity : 0,
31 | transitionDuration : '0s'
32 | },
33 | '&.loading' :
34 | {
35 | filter : 'blur(5px)'
36 | }
37 | }
38 | });
39 |
40 | class FullView extends React.PureComponent
41 | {
42 | constructor(props)
43 | {
44 | super(props);
45 |
46 | // Latest received video track.
47 | // @type {MediaStreamTrack}
48 | this._videoTrack = null;
49 |
50 | this.video = React.createRef();
51 | }
52 |
53 | render()
54 | {
55 | const {
56 | videoVisible,
57 | videoProfile,
58 | classes
59 | } = this.props;
60 |
61 | return (
62 |
63 |
73 |
74 | );
75 | }
76 |
77 | componentDidMount()
78 | {
79 | const { videoTrack } = this.props;
80 |
81 | this._setTracks(videoTrack);
82 | }
83 |
84 | componentDidUpdate(prevProps)
85 | {
86 | if (prevProps !== this.props)
87 | {
88 | const { videoTrack } = this.props;
89 |
90 | this._setTracks(videoTrack);
91 | }
92 | }
93 |
94 | _setTracks(videoTrack)
95 | {
96 | if (this._videoTrack === videoTrack)
97 | return;
98 |
99 | this._videoTrack = videoTrack;
100 |
101 | const video = this.video.current;
102 |
103 | if (videoTrack)
104 | {
105 | const stream = new MediaStream();
106 |
107 | stream.addTrack(videoTrack);
108 | video.srcObject = stream;
109 | }
110 | else
111 | {
112 | video.srcObject = null;
113 | }
114 | }
115 | }
116 |
117 | FullView.propTypes =
118 | {
119 | videoTrack : PropTypes.any,
120 | videoVisible : PropTypes.bool,
121 | videoProfile : PropTypes.string,
122 | classes : PropTypes.object.isRequired
123 | };
124 |
125 | export default withStyles(styles)(FullView);
126 |
--------------------------------------------------------------------------------
/app/src/store/reducers/files.js:
--------------------------------------------------------------------------------
1 | const initialState =
2 | {
3 | files : [],
4 | count : 0,
5 | countUnread : 0
6 | };
7 |
8 | const files = (state = initialState, action) =>
9 | {
10 | switch (action.type)
11 | {
12 | case 'ADD_FILE':
13 | {
14 | const file = action.payload;
15 |
16 | return {
17 | ...state,
18 | files : [
19 | ...state.files,
20 | {
21 | ...file,
22 | active : false,
23 | progress : 0,
24 | files : null
25 | }
26 | ],
27 | count : state.count + 1,
28 | countUnread : file.sender === 'response' ? ++state.countUnread : state.countUnread
29 |
30 | };
31 | }
32 |
33 | case 'ADD_FILE_HISTORY':
34 | {
35 | const { fileHistory } = action.payload;
36 |
37 | const newFileHistory = [];
38 |
39 | fileHistory.forEach((file) =>
40 | {
41 | newFileHistory.push({
42 | active : false,
43 | progress : 0,
44 | files : null,
45 | ...file
46 | });
47 | });
48 |
49 | return {
50 | ...state,
51 | files : newFileHistory,
52 | count : newFileHistory.length
53 | };
54 | }
55 |
56 | case 'SET_FILE_ACTIVE':
57 | {
58 | const { magnetUri } = action.payload;
59 |
60 | state.files.forEach((item, index) =>
61 | {
62 | if (item.magnetUri === magnetUri)
63 | {
64 | state.files[index].active = true;
65 | }
66 | });
67 |
68 | return { ...state };
69 | }
70 |
71 | case 'SET_FILE_INACTIVE':
72 | {
73 | const { magnetUri } = action.payload;
74 |
75 | state.files.forEach((item, index) =>
76 | {
77 | if (item.magnetUri === magnetUri)
78 | {
79 | state.files[index].active = false;
80 | }
81 | });
82 |
83 | return { ...state };
84 | }
85 |
86 | case 'SET_FILE_PROGRESS':
87 | {
88 | const { magnetUri, progress } = action.payload;
89 |
90 | state.files.forEach((item, index) =>
91 | {
92 | if (item.magnetUri === magnetUri)
93 | {
94 | state.files[index].progress = progress;
95 | }
96 | });
97 |
98 | return { ...state };
99 | }
100 |
101 | case 'SET_FILE_DONE':
102 | {
103 | const { magnetUri, sharedFiles } = action.payload;
104 |
105 | state.files.forEach((item, index) =>
106 | {
107 | if (item.magnetUri === magnetUri)
108 | {
109 | state.files[index] = {
110 | ...item,
111 | files : sharedFiles,
112 | progress : 1,
113 | active : false,
114 | timeout : false
115 | };
116 | }
117 | });
118 |
119 | return { ...state };
120 | }
121 |
122 | case 'CLEAR_FILES':
123 | {
124 | return {
125 | ...state,
126 | files : [],
127 | count : 0,
128 | countUnread : 0
129 | };
130 |
131 | }
132 | default:
133 | return state;
134 | }
135 | };
136 |
137 | export default files;
138 |
--------------------------------------------------------------------------------
/compose/config/edumeet-server-config.js:
--------------------------------------------------------------------------------
1 | const os = require('os');
2 |
3 | // To gather ip address only on interface like eth0, ens0p3
4 | const ifaceWhiteListRegex = /^(eth.*)|(ens.*)|(tun.*)|(wlp.*)/
5 |
6 | function getListenIps() {
7 | let listenIP = [];
8 | const ifaces = os.networkInterfaces();
9 | Object.keys(ifaces).forEach(function (ifname) {
10 | if (ifname.match(ifaceWhiteListRegex)) {
11 | ifaces[ifname].forEach(function (iface) {
12 | if (
13 | (iface.family !== "IPv4" &&
14 | (iface.family !== "IPv6" || iface.scopeid !== 0)) ||
15 | iface.internal !== false
16 | ) {
17 | // skip over internal (i.e. 127.0.0.1) and non-ipv4 or ipv6 non global addresses
18 | return;
19 | }
20 | listenIP.push({ ip: iface.address, announcedIp: iface.address });
21 | });
22 | }
23 | });
24 | console.log('Using listenips:', listenIP);
25 | return listenIP;
26 | }
27 |
28 | module.exports =
29 | {
30 | // Mediasoup settings
31 | mediasoup :
32 | {
33 | numWorkers : 2, //Object.keys(os.cpus()).length,
34 | // mediasoup Worker settings.
35 | worker :
36 | {
37 | logLevel : 'warn',
38 | logTags :
39 | [
40 | 'info',
41 | 'ice',
42 | 'dtls',
43 | 'rtp',
44 | 'srtp',
45 | 'rtcp'
46 | ],
47 | rtcMinPort : 40000,
48 | rtcMaxPort : 49999
49 | },
50 | // mediasoup Router settings.
51 | router :
52 | {
53 | // Router media codecs.
54 | mediaCodecs :
55 | [
56 | {
57 | kind : 'audio',
58 | mimeType : 'audio/opus',
59 | clockRate : 48000,
60 | channels : 2
61 | },
62 | {
63 | kind : 'video',
64 | mimeType : 'video/VP8',
65 | clockRate : 90000,
66 | parameters :
67 | {
68 | 'x-google-start-bitrate' : 1000
69 | }
70 | },
71 | {
72 | kind : 'video',
73 | mimeType : 'video/VP9',
74 | clockRate : 90000,
75 | parameters :
76 | {
77 | 'profile-id' : 2,
78 | 'x-google-start-bitrate' : 1000
79 | }
80 | },
81 | {
82 | kind : 'video',
83 | mimeType : 'video/h264',
84 | clockRate : 90000,
85 | parameters :
86 | {
87 | 'packetization-mode' : 1,
88 | 'profile-level-id' : '4d0032',
89 | 'level-asymmetry-allowed' : 1,
90 | 'x-google-start-bitrate' : 1000
91 | }
92 | },
93 | {
94 | kind : 'video',
95 | mimeType : 'video/h264',
96 | clockRate : 90000,
97 | parameters :
98 | {
99 | 'packetization-mode' : 1,
100 | 'profile-level-id' : '42e01f',
101 | 'level-asymmetry-allowed' : 1,
102 | 'x-google-start-bitrate' : 1000
103 | }
104 | }
105 | ]
106 | },
107 | // mediasoup WebRtcTransport settings.
108 | webRtcTransport :
109 | {
110 | listenIps : getListenIps(),
111 | }
112 | }
113 | };
114 |
--------------------------------------------------------------------------------
/app/src/store/actions/peerActions.js:
--------------------------------------------------------------------------------
1 | export const addPeer = (peer) =>
2 | ({
3 | type : 'ADD_PEER',
4 | payload : { peer }
5 | });
6 |
7 | export const removePeer = (peerId) =>
8 | ({
9 | type : 'REMOVE_PEER',
10 | payload : { peerId }
11 | });
12 |
13 | export const clearPeers = () =>
14 | ({
15 | type : 'CLEAR_PEERS'
16 | });
17 |
18 | export const setPeerDisplayName = (displayName, peerId) =>
19 | ({
20 | type : 'SET_PEER_DISPLAY_NAME',
21 | payload : { displayName, peerId }
22 | });
23 |
24 | export const setPeerVideoInProgress = (peerId, flag) =>
25 | ({
26 | type : 'SET_PEER_VIDEO_IN_PROGRESS',
27 | payload : { peerId, flag }
28 | });
29 |
30 | export const setPeerAudioInProgress = (peerId, flag) =>
31 | ({
32 | type : 'SET_PEER_AUDIO_IN_PROGRESS',
33 | payload : { peerId, flag }
34 | });
35 |
36 | export const setPeerScreenInProgress = (peerId, flag) =>
37 | ({
38 | type : 'SET_PEER_SCREEN_IN_PROGRESS',
39 | payload : { peerId, flag }
40 | });
41 |
42 | export const setPeerRaisedHand = (peerId, raisedHand, raisedHandTimestamp) =>
43 | ({
44 | type : 'SET_PEER_RAISED_HAND',
45 | payload : { peerId, raisedHand, raisedHandTimestamp }
46 | });
47 |
48 | export const setPeerRaisedHandInProgress = (peerId, flag) =>
49 | ({
50 | type : 'SET_PEER_RAISED_HAND_IN_PROGRESS',
51 | payload : { peerId, flag }
52 | });
53 |
54 | export const setPeerPicture = (peerId, picture) =>
55 | ({
56 | type : 'SET_PEER_PICTURE',
57 | payload : { peerId, picture }
58 | });
59 |
60 | export const addPeerRole = (peerId, roleId) =>
61 | ({
62 | type : 'ADD_PEER_ROLE',
63 | payload : { peerId, roleId }
64 | });
65 |
66 | export const removePeerRole = (peerId, roleId) =>
67 | ({
68 | type : 'REMOVE_PEER_ROLE',
69 | payload : { peerId, roleId }
70 | });
71 |
72 | export const setPeerModifyRolesInProgress = (peerId, flag) =>
73 | ({
74 | type : 'SET_PEER_MODIFY_ROLES_IN_PROGRESS',
75 | payload : { peerId, flag }
76 | });
77 |
78 | export const setPeerKickInProgress = (peerId, flag) =>
79 | ({
80 | type : 'SET_PEER_KICK_IN_PROGRESS',
81 | payload : { peerId, flag }
82 | });
83 |
84 | export const setMutePeerInProgress = (peerId, flag) =>
85 | ({
86 | type : 'STOP_PEER_AUDIO_IN_PROGRESS',
87 | payload : { peerId, flag }
88 | });
89 |
90 | export const setStopPeerVideoInProgress = (peerId, flag) =>
91 | ({
92 | type : 'STOP_PEER_VIDEO_IN_PROGRESS',
93 | payload : { peerId, flag }
94 | });
95 |
96 | export const setStopPeerScreenSharingInProgress = (peerId, flag) =>
97 | ({
98 | type : 'STOP_PEER_SCREEN_SHARING_IN_PROGRESS',
99 | payload : { peerId, flag }
100 | });
101 |
102 | export const setPeerLocalRecordingState = (peerId, localRecordingState) =>
103 | ({
104 | type : 'SET_PEER_LOCAL_RECORDING_STATE',
105 | payload : { peerId, localRecordingState }
106 | });
107 |
108 | export const setPeerLocalRecordingConsent = (peerId, consent) =>
109 | ({
110 | type : 'SET_PEER_LOCAL_RECORDING_CONSENT',
111 | payload : { peerId, consent }
112 | });
113 |
--------------------------------------------------------------------------------
/app/src/components/MeetingDrawer/Chat/Menu/Moderator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 | import { withRoomContext } from '../../../../RoomContext';
5 | import { withStyles } from '@material-ui/core/styles';
6 | import { useIntl, FormattedMessage } from 'react-intl';
7 | import { permissions } from '../../../../permissions';
8 | import { makePermissionSelector } from '../../../../store/selectors';
9 | import Button from '@material-ui/core/Button';
10 |
11 | const styles = (theme) =>
12 | ({
13 | root :
14 | {
15 | display : 'flex',
16 | padding : theme.spacing(1),
17 | boxShadow : '0 2px 5px 2px rgba(0, 0, 0, 0.2)',
18 | backgroundColor : 'rgba(255, 255, 255, 1)'
19 | },
20 | listheader :
21 | {
22 | padding : theme.spacing(1),
23 | fontWeight : 'bolder'
24 | },
25 | actionButton :
26 | {
27 | marginLeft : 'auto'
28 | }
29 | });
30 |
31 | const ChatModerator = (props) =>
32 | {
33 | const intl = useIntl();
34 |
35 | const {
36 | roomClient,
37 | isChatModerator,
38 | isFileSharingModerator,
39 | room,
40 | classes
41 | } = props;
42 |
43 | if (!isChatModerator)
44 | return null;
45 |
46 | if (!isFileSharingModerator)
47 | return null;
48 |
49 | const handleClearChat = () =>
50 | {
51 | roomClient.clearChat();
52 | };
53 |
54 | return (
55 |
56 | -
57 |
61 |
62 |
78 |
79 | );
80 | };
81 |
82 | ChatModerator.propTypes =
83 | {
84 | roomClient : PropTypes.any.isRequired,
85 | isFileSharingModerator : PropTypes.bool,
86 | isChatModerator : PropTypes.bool,
87 | room : PropTypes.object,
88 | classes : PropTypes.object.isRequired
89 | };
90 |
91 | const makeMapStateToProps = () =>
92 | {
93 | const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT);
94 |
95 | const mapStateToProps = (state) =>
96 | ({
97 | isChatModerator : hasPermission(state),
98 | isFileSharingModerator : hasPermission(state),
99 | room : state.room
100 | });
101 |
102 | return mapStateToProps;
103 | };
104 |
105 | export default withRoomContext(connect(
106 | makeMapStateToProps,
107 | null,
108 | null,
109 | {
110 | areStatesEqual : (next, prev) =>
111 | {
112 | return (
113 | prev.room === next.room &&
114 | prev.me === next.me &&
115 | prev.peers === next.peers
116 | );
117 | }
118 | }
119 | )(withStyles(styles)(ChatModerator)));
--------------------------------------------------------------------------------
/app/src/components/MeetingDrawer/ParticipantList/ListMe.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import { withRoomContext } from '../../../RoomContext';
5 | import classnames from 'classnames';
6 | import PropTypes from 'prop-types';
7 | import * as appPropTypes from '../../appPropTypes';
8 | import { useIntl } from 'react-intl';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import Tooltip from '@material-ui/core/Tooltip';
11 | import PanIcon from '@material-ui/icons/PanTool';
12 | import EmptyAvatar from '../../../images/avatar-empty.jpeg';
13 |
14 | const styles = (theme) =>
15 | ({
16 | root :
17 | {
18 | width : '100%',
19 | overflow : 'hidden',
20 | cursor : 'auto',
21 | display : 'flex'
22 | },
23 | avatar :
24 | {
25 | borderRadius : '50%',
26 | height : '2rem',
27 | width : '2rem',
28 | objectFit : 'cover',
29 | marginTop : theme.spacing(0.5)
30 | },
31 | peerInfo :
32 | {
33 | fontSize : '1rem',
34 | display : 'flex',
35 | paddingLeft : theme.spacing(1),
36 | flexGrow : 1,
37 | alignItems : 'center'
38 | },
39 | buttons :
40 | {
41 | padding : theme.spacing(1)
42 | },
43 | green :
44 | {
45 | color : 'rgba(0, 153, 0, 1)'
46 | }
47 | });
48 |
49 | const ListMe = (props) =>
50 | {
51 | const intl = useIntl();
52 |
53 | const {
54 | roomClient,
55 | me,
56 | settings,
57 | classes
58 | } = props;
59 |
60 | const picture = me.picture || EmptyAvatar;
61 |
62 | return (
63 |
64 |

65 |
66 |
67 | {settings.displayName}
68 |
69 |
76 |
87 | {
88 | e.stopPropagation();
89 |
90 | roomClient.setRaisedHand(!me.raisedHand);
91 | }}
92 | >
93 |
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | ListMe.propTypes =
101 | {
102 | roomClient : PropTypes.object.isRequired,
103 | me : appPropTypes.Me.isRequired,
104 | settings : PropTypes.object.isRequired,
105 | classes : PropTypes.object.isRequired
106 | };
107 |
108 | const mapStateToProps = (state) => ({
109 | me : state.me,
110 | settings : state.settings
111 | });
112 |
113 | export default withRoomContext(connect(
114 | mapStateToProps,
115 | null,
116 | null,
117 | {
118 | areStatesEqual : (next, prev) =>
119 | {
120 | return (
121 | prev.me === next.me &&
122 | prev.settings === next.settings
123 | );
124 | }
125 | }
126 | )(withStyles(styles)(ListMe)));
127 |
--------------------------------------------------------------------------------
/docs/HAproxy.md:
--------------------------------------------------------------------------------
1 | # Howto deploy a (room based) load balanced cluster
2 |
3 | This example will show how to setup an HA proxy to provide load balancing between several
4 | edumeet servers.
5 |
6 | ## IP and DNS
7 |
8 | In this basic example we use the following names and ips:
9 |
10 | ### Backend
11 |
12 | * `mm1.example.com` <=> `192.0.2.1`
13 | * `mm2.example.com` <=> `192.0.2.2`
14 | * `mm3.example.com` <=> `192.0.2.3`
15 |
16 | ### Redis
17 |
18 | * `redis.example.com` <=> `192.0.2.4`
19 |
20 | ### Load balancer HAproxy
21 |
22 | * `meet.example.com` <=> `192.0.2.5`
23 |
24 | ## Deploy multiple edumeet servers
25 |
26 | This is most easily done using Ansible (see below), but can be done
27 | in any way you choose (manual, Docker, Ansible).
28 |
29 | Read more here: [mm-ansible](https://github.com/edumeet/edumeet-ansible)
30 | [](https://asciinema.org/a/311365)
31 |
32 | ## Setup Redis for central HTTP session store
33 |
34 | ### Use one Redis for all edumeet servers
35 |
36 | * Deploy a Redis cluster for all instances.
37 | * We will use in our actual example `192.0.2.4` as redis HA cluster ip. It is out of scope howto deploy it.
38 |
39 | OR
40 |
41 | * For testing you can use Redis from one the edumeet servers. e.g. If you plan only for testing on your first edumeet server.
42 | * Configure Redis `redis.conf` to not only bind to your loopback but also to your global ip address too:
43 |
44 | ``` plaintext
45 | bind 192.0.2.1
46 | ```
47 |
48 | This example sets this to `192.0.2.1`, change this according to your local installation.
49 |
50 | * Change your firewall config to allow incoming Redis. Example (depends on the type of firewall):
51 |
52 | ``` plaintext
53 | chain INPUT {
54 | policy DROP;
55 |
56 | saddr mm2.example.com proto tcp dport 6379 ACCEPT;
57 | saddr mm3.example.com proto tcp dport 6379 ACCEPT;
58 | }
59 | ```
60 |
61 | * **Set a password, or if you don't (like in this basic example) take care to set strict firewall rules**
62 |
63 | ## Configure edumeet servers
64 |
65 | ### Server config
66 |
67 | mm/configs/server/config.js
68 |
69 | ``` js
70 | redisOptions : { host: '192.0.2.4'},
71 | listeningPort: 80,
72 | httpOnly: true,
73 | trustProxy : ['192.0.2.5'],
74 | ```
75 |
76 | ## Deploy HA proxy
77 |
78 | * Configure certificate / letsencrypt for `meet.example.com`
79 | * In this example we put a complete chain and private key in /root/certificate.pem.
80 | * Install and setup haproxy
81 |
82 | `apt install haproxy`
83 |
84 | * Add to /etc/haproxy/haproxy.cfg config
85 |
86 | ``` plaintext
87 | backend edumeet
88 | balance url_param roomId
89 | hash-type consistent
90 |
91 | server mm1 192.0.2.1:80 check maxconn 2000 verify none
92 | server mm2 192.0.2.2:80 check maxconn 2000 verify none
93 | server mm3 192.0.2.3:80 check maxconn 2000 verify none
94 |
95 | frontend meet.example.com
96 | bind 192.0.2.5:80
97 | bind 192.0.2.5:443 ssl crt /root/certificate.pem
98 | http-request redirect scheme https unless { ssl_fc }
99 | reqadd X-Forwarded-Proto:\ https
100 | default_backend edumeet
101 | ```
102 |
--------------------------------------------------------------------------------
/compose/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.2'
2 |
3 | networks:
4 | edumeet:
5 | driver: bridge
6 | ipam:
7 | driver: default
8 | config:
9 | - subnet: 172.22.0.0/24
10 |
11 | services:
12 |
13 | edumeet:
14 | build: ./edumeet
15 | container_name: edumeet
16 | restart: unless-stopped
17 | user: "${CURRENT_USER}"
18 | volumes:
19 | - ${PWD}/..:/edumeet
20 | - ${PWD}/config/edumeet-server-config.js:/edumeet/server/config/config.js:ro
21 | - ${PWD}/config/edumeet-server-config.yaml:/edumeet/server/config/config.yaml:ro
22 | - ${PWD}/config/edumeet-app-config.js:/edumeet/app/public/config/config.js:ro
23 | #- ${PWD}/../app/public/config/config.example.js:/edumeet/app/public/config/config.js:ro
24 | network_mode: "host"
25 | extra_hosts:
26 | redis: 172.22.0.2
27 | depends_on:
28 | - redis
29 |
30 | redis:
31 | image: redis
32 | container_name: edumeet_redis
33 | restart: unless-stopped
34 | networks:
35 | edumeet:
36 | ipv4_address: 172.22.0.2
37 |
38 | nginx:
39 | image: nginx
40 | container_name: edumeet_nginx
41 | restart: unless-stopped
42 | ports:
43 | - 8443:443
44 | volumes:
45 | - ${PWD}/../server/certs/mediasoup-demo.localhost.cert.pem:/etc/nginx/cert.pem:ro
46 | - ${PWD}/../server/certs/mediasoup-demo.localhost.key.pem:/etc/nginx/key.pem:ro
47 | - ${PWD}/config/nginx.conf:/etc/nginx/conf.d/default.conf:ro
48 | extra_hosts:
49 | edumeet: 172.22.0.1
50 | depends_on:
51 | - edumeet
52 |
53 | prometheus:
54 | image: prom/prometheus:v2.26.0
55 | user: root
56 | container_name: edumeet_prometheus
57 | restart: unless-stopped
58 | volumes:
59 | - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
60 | - ./data/prometheus:/prometheus
61 | command:
62 | - '--config.file=/etc/prometheus/prometheus.yml'
63 | - '--storage.tsdb.path=/prometheus'
64 | ports:
65 | - 9090:9090
66 | links:
67 | #- cadvisor:cadvisor
68 | - node-exporter:node-exporter
69 | - edumeet:edumeet
70 | extra_hosts:
71 | edumeet: 172.22.0.1
72 |
73 | node-exporter:
74 | image: prom/node-exporter:v1.1.2
75 | container_name: edumeet_node_exporter
76 | restart: unless-stopped
77 |
78 | #cadvisor:
79 | # image: google/cadvisor:latest
80 | # container_name: edumeet_cadvisor
81 | # restart: unless-stopped
82 | # volumes:
83 | # - /:/rootfs:ro
84 | # - /var/run:/var/run:rw
85 | # - /sys:/sys:ro
86 | # - /var/lib/docker/:/var/lib/docker:ro
87 | # expose:
88 | # - 8080
89 |
90 | grafana:
91 | image: grafana/grafana:7.5.3
92 | user: root
93 | container_name: edumeet_grafana
94 | restart: unless-stopped
95 | links:
96 | - prometheus:prometheus
97 | ports:
98 | - 9091:3000
99 | volumes:
100 | - ./config/grafana-prometheus-datasource.yml:/etc/grafana/provisioning/datasources/prometheus.yml
101 | - ./config/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/all.yml
102 | - ./config/grafana-dashboards:/var/lib/grafana/dashboards
103 | - ./data/grafana:/var/lib/grafana
104 | environment:
105 | - GF_SECURITY_ADMIN_USER=admin
106 | - GF_SECURITY_ADMIN_PASSWORD=admin
107 | - GF_USERS_ALLOW_SIGN_UP=false
108 |
--------------------------------------------------------------------------------
/app/src/components/ConfigDocumentation.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'; // eslint-disable-line no-use-before-define
2 | import { withStyles } from '@material-ui/core/styles';
3 | import PropTypes from 'prop-types';
4 | import { FormattedMessage } from 'react-intl';
5 |
6 | import Card from '@material-ui/core/Card';
7 | import CardActions from '@material-ui/core/CardActions';
8 | import CardContent from '@material-ui/core/CardContent';
9 | import Typography from '@material-ui/core/Typography';
10 | import Table from '@material-ui/core/Table';
11 | import TableBody from '@material-ui/core/TableBody';
12 | import TableCell from '@material-ui/core/TableCell';
13 | import TableContainer from '@material-ui/core/TableContainer';
14 | import TableHead from '@material-ui/core/TableHead';
15 | import TableRow from '@material-ui/core/TableRow';
16 | import Paper from '@material-ui/core/Paper';
17 | import Button from '@material-ui/core/Button';
18 |
19 | import { formatDocs } from '../config';
20 |
21 | const configDocs = formatDocs();
22 |
23 | const styles = () =>
24 | ({
25 | table : {
26 | minWidth : 700
27 | },
28 | pre : {
29 | fontSize : '0.8rem'
30 | },
31 | cell : {
32 | maxWidth : '25vw',
33 | overflow : 'auto'
34 | }
35 | });
36 |
37 | const ConfigDocumentation = ({
38 | classes
39 | }: {
40 | classes : any;
41 | }) =>
42 | {
43 | return (
44 |
45 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Property
58 | Description
59 | Format
60 | Default value
61 |
62 |
63 |
64 | {Object.entries(configDocs).map(([ name, value ] : [ string, any ]) =>
65 | {
66 | return (
67 |
68 | {name}
69 | {value.doc}
70 |
71 | {value.format}
72 |
73 |
74 | {value.default}
75 |
76 |
77 | );
78 | })}
79 |
80 |
81 |
82 |
83 |
84 |
85 |
91 |
92 |
93 | );
94 | };
95 |
96 | ConfigDocumentation.propTypes =
97 | {
98 | classes : PropTypes.object.isRequired
99 | };
100 |
101 | export default withStyles(styles)(ConfigDocumentation);
102 |
--------------------------------------------------------------------------------
/server/certs/edumeet-demo-key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCzaPB0SS1Sltp4
3 | DIwKBU+g4dYD9Hor7HKhyhHXOCx1raGIzKVDT21x9hN/rHOTR05mFJvnvfw/sSd7
4 | xfRDgeFuyGNhETj3ixd5sUhosyTLLd/6e62zhvtahKzg3daNtaMtX7CR0tRwdbnX
5 | mCkck6bBq5XPH2jBgZ8JKmiZvUsLlFO+bK3k0nQ8GRFRrOPTkhmTyqONkidoHGGx
6 | A1MP3FGUXLnnJP3zeVsf9OU1rDFCmf5G7BYFq3X62bzE38q7NF4AU0VHND/SHUAB
7 | 0ZDrRUGdfMQILGyf/ShYM+NvZW+AWaxGRI1Tq4UNVatR+eazW+frqxcxT8RQaiRg
8 | NSOCNw+qXD/kSKYVhGwxlnpQUOC/z2i0SCcOkoXaRH0z2WI2a6O5GzMSQiRKKqyQ
9 | obtCTvpfjfKrS5KZ4IYKyPCORX2SsQxLhh5OmJ0FiHrrGPT2fdYMhactUmNiqvHu
10 | Jqbhf4zv/Oe1F6NiUIRrETyLR74/QHqpo42a9nZtZiKlhk5j8jaS5ubFgG7BbS4A
11 | Uyn6YBK7oIeiryteY1lYZd4tC7PQuX3lNsFO3ZOtah9UYjlPIAXmYmFvupA/FknX
12 | XfDxImBzGdKXjDCaBX25ftNrgi4uCwAls8JdhpvSZFIC+719EjgPehdn/vkMiy7y
13 | Afp4YTGorcNEL0/h7oGm+MlwSQRb2QIDAQABAoICAQCWwCqry4FF0HQqQ4C4OtY5
14 | /QlzsU2m8rsvrzdmfFD/YLJG3I5RDMCN7ZNcyG8k5dm+dLq78yut6RGgMymYP95I
15 | 1CCNQ4d2mW1UV97b+wuDnjyBoMLIAzfZS3poSH8r+9/tFGatYVYYWRObUMPau0Z3
16 | ndH3hBDl6CDV9siFxkT0qeHkNDW5/AynIvkmg/u7nxvWz4K2RoTOOmrr7jsxLJNv
17 | 8qpSywaIOwSSyZh/jPynVfYPafjnMrej3Kl9U/5pZwtFgrLHreOijelmnc0Do+IK
18 | jve5Vnq/xFzOIGuPAtC6LJ9RO/D0yT63gbC+5RkwxJ0PrWeDi98NIuMF5CC+Hb4O
19 | o9pzZa5xoifS9Dq2jRYOEPrOekSkmBmzhLnRGa1DSnLddCOHoyIeTFyX2aP0auyl
20 | Xp78+mBo30TyMxK9ETDceqwMsYZRy0oaX7KT4QLufHOSJHkRcbKIbP8zMp9dITTQ
21 | FjmB+0m8ijcx1qZt3XMa09DWso5cZi24F1NW1iLPkXCg4aD/z2EfoOYnv3y4SOWR
22 | 1P9PvgTt6pDXPFmqD3NDC+WdfUytFggGTBol1WGPyfRpXbhTLM1a4vXW+f2cVlT2
23 | kQfaZK7+8I0lBecm9uWGhTT/VTXW3z2jBbpnowM5avDQkdX0vkNO8+M1PVazQR7g
24 | n3mHtE7c9gkbH1SHSZEyAQKCAQEA2Yo9/u8lZpZYvr5xbqrHCDyJpS7bzeBEzA0k
25 | qzni9heX4I947ykNiycXEfW6dkaI/DnWIcXI/yBAwvSFPWb4ybiCUnT3gY5of8qK
26 | /vZD1k5CC2OCW3q/WnlEfDS4aTc4oa2yCKFNrDtYi3iuC9tqJzowfF3umGgGzd7D
27 | eBaocG/QlTYhoV0bgJ/gF+lAVjZzGSd3MFkw7L4nos7dk+1la6S/XIUDSaVys9ML
28 | tcVJ35bCGw5bZeVfDJz1c7GnB0U5mjoyIRRPbxuRvTGgiyew+ODA3exUuNUvsyyJ
29 | 9/9I17sJTzHaD5ag8kQz9zzLWHofWiHRDaiEQmH1f4wVAy/FKQKCAQEA0yDygk9P
30 | ZUCwmjtYqhs3nZuz+calvR+pvxyh/d+YgSQIzLMgf7HVvjVnKRvD/RHapR14web7
31 | nRuM4d5IRi/wTI6bAQgHsyCDSjIF5e8UitjamXO+/0uXeK0Goh86Iuosmn+mZSRy
32 | Aie7HdXX5c3uHTuW8gSEy1lULkGyhPV9C2YEHfMOMmiCyc+iPcxXKQ35v2XNAc1q
33 | 4rSx2B+8w+XL1ey9oais7gAijm2F6OizLL2sIAsq6yyryKnEoN4ssEVbbOVYgsaW
34 | fr+gQfTkBZCDmoVGQSl9PevQvJNmsRe4Ifgzbz2qQSp2M0jBIE/GjCgMPpbo+q9W
35 | XDqwJrwDyo+HMQKCAQEAgC7mUwblmepzhom/Wz+EIgVR8iSHXmuM/lOsTLzCUNIc
36 | KzU/RGWDVoCFJo8N/U2YwE7wL1xVEIgXwQjGTiUT6gEvwZiskwmv58UYXB4OYQQi
37 | BIXxNShCAvS79xg1pcHlO9eWtWEe3KLnjN9iZxg2F8FA+rd6tRFvGPXvZh6rx/0L
38 | AjEwZd3wK72JFW3a/DH/Zk4L/FBB9O4jetq8U8Mp5ODh5Yl3I3k6+l0cZFJJMleq
39 | LrkqAAPFGzCNrUt8KVuQEqHCEh3epJCxMrNAb17G+A+vddUhIvxzq/dNoPUrHftX
40 | A/RTEqZmVfr8R+3pwEvNl5WfkpW+wULpnuuTIhqO6QKCAQBbRADRB8vAb0hoQ4M+
41 | dWmDPg18ybxXltpf/Nah0ggwuwz6v+wqhwton9kqUhBU02T6v1S2LU1TSteJw/bm
42 | ME7mKTckKP57FnDqn9kg3kq5AqjscLZ90YV26wTVDD6rXSNO3iNl0W1fNSGT8h7T
43 | /kMSa/ICSKXG6aSUIl4zT5NwW/cnoyvd6oOvDYyKvkxnON1fOXh3cP7lZUsDrCSD
44 | YlDM9vu6aBnpADHv87RRTFY33v4LFAjHhJX1tj2DdMdIo5Kz7ihmz8W6oMd7+4qe
45 | RSw+naITBQZYwBmJiwZ3Q1Obi5lgWv3AEcTqwmaJuzKO37j7TW0FAMqKL+x5sgjJ
46 | hWXhAoIBAQCaZza/pA0b0s3/yvJgj5ArY+kjbT4v6RfBeXtPl7I2e+2gO7ZO41iq
47 | RyUR+i5mwv5ATsqlar92P5O0LLSvGx5u2Z4VVBvDK/SbKTY8BL1AnKt3AOYSR3uh
48 | AS796EHg8Fc28MCZh74r3G8i6byNlZyOADkPKdBlskvuMx2qpcTGjHrleZrBAGlA
49 | y/hm+uTqKa5TH/DYjbONqAF0sW9RSlHhfGgJGHqQzQrApJpu9yhf/T3WPF2egeAd
50 | Tk1dHJniuVGUMzt4uYzpSn3QNrPYLMHI7tJPC/loFnWhEk/zlpMcFDgIgqZdAppX
51 | 3FD+CVuiwTatCHF5ABS4cF2dhQrzIuQY
52 | -----END PRIVATE KEY-----
53 |
--------------------------------------------------------------------------------
/app/src/intl/locales.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | /* eslint-disable import/no-dynamic-require */
3 |
4 | const list = [
5 | {
6 | name : 'English',
7 | file : 'en',
8 | locale : [ 'en', 'en-en' ]
9 | },
10 | {
11 | name : 'Czech',
12 | file : 'cs',
13 | locale : [ 'cs', 'cs-cs' ]
14 | },
15 | {
16 | name : 'Chinese (Simplified)',
17 | file : 'cn',
18 | locale : [ 'zn', 'zn-zn', 'zn-cn' ]
19 | }, // hans
20 | {
21 | name : 'Chinese (Traditional)',
22 | file : 'tw',
23 | locale : [ 'zn-tw', 'zn-hk', 'zn-sg' ]
24 | }, // hant
25 | {
26 | name : 'Croatian',
27 | file : 'hr',
28 | locale : [ 'hr', 'hr-hr' ]
29 | },
30 | {
31 | name : 'Danish',
32 | file : 'dk',
33 | locale : [ 'dk', 'dk-dk' ]
34 | },
35 | {
36 | name : 'French',
37 | file : 'fr',
38 | locale : [ 'fr', 'fr-fr' ]
39 | },
40 | {
41 | name : 'German',
42 | file : 'de',
43 | locale : [ 'de', 'de-de' ]
44 | },
45 | {
46 | name : 'Greek',
47 | file : 'el',
48 | locale : [ 'el', 'el-el' ]
49 | },
50 | {
51 | name : 'Hindi',
52 | file : 'hi',
53 | locale : [ 'hi', 'hi-hi' ]
54 | },
55 | {
56 | name : 'Hungarian',
57 | file : 'hu',
58 | locale : [ 'hu', 'hu-hu' ]
59 | },
60 | {
61 | name : 'Italian',
62 | file : 'it',
63 | locale : [ 'it', 'it-it' ]
64 | },
65 | {
66 | name : 'Kazakh',
67 | file : 'kk',
68 | locale : [ 'kk', 'kk-kz ' ]
69 | },
70 | {
71 | name : 'Latvian',
72 | file : 'lv',
73 | locale : [ 'lv', 'lv-lv' ]
74 | },
75 | {
76 | name : 'Norwegian',
77 | file : 'nb',
78 | locale : [ 'nb', 'nb-no' ]
79 | },
80 | {
81 | name : 'Polish',
82 | file : 'pl',
83 | locale : [ 'pl', 'pl-pl' ]
84 | },
85 | {
86 | name : 'Portuguese',
87 | file : 'pt',
88 | locale : [ 'pt', 'pt-pt' ]
89 | },
90 | {
91 | name : 'Romanian',
92 | file : 'ro',
93 | locale : [ 'ro', 'ro-ro' ]
94 | },
95 | {
96 | name : 'Russian',
97 | file : 'ru',
98 | locale : [ 'ru', 'ru-ru' ]
99 | },
100 | {
101 | name : 'Spanish',
102 | file : 'es',
103 | locale : [ 'es', 'es-es' ]
104 | },
105 | {
106 | name : 'Turkish',
107 | file : 'tr',
108 | locale : [ 'tr', 'tr-tr' ]
109 | },
110 | {
111 | name : 'Ukrainian',
112 | file : 'uk',
113 | locale : [ 'uk', 'uk-uk' ]
114 | }
115 | ];
116 |
117 | export const detect = () =>
118 | {
119 | const localeFull = (navigator.language ||
120 | (navigator as any).browserLanguage).toLowerCase();
121 |
122 | // const localeCountry = localeFull.split(/[-_]/)[0];
123 |
124 | // const localeRegion = localeFull.split(/[-_]/)[1] || null;
125 |
126 | return localeFull;
127 | };
128 |
129 | export const getList = () => list;
130 |
131 | export interface ILocale {
132 | name: string;
133 | file: string;
134 | locale: string[];
135 | messages: any;
136 | }
137 |
138 | export const loadOne = (locale: string): ILocale =>
139 | {
140 | let res: any = {};
141 |
142 | try
143 | {
144 | res = list.filter(
145 | // (item) => item.locale.includes(locale) || item.locale.includes(locale.split(/[-_]/)[0])
146 | (item) => item.locale.includes(locale)
147 | )[0];
148 |
149 | res.messages = require(`./translations/${res.file}`);
150 | }
151 | catch
152 | {
153 | res = list.filter((item) => item.locale.includes('en'))[0];
154 |
155 | res.messages = require(`./translations/${res.file}`);
156 | }
157 |
158 | return res;
159 | };
160 |
--------------------------------------------------------------------------------
/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import PropTypes from 'prop-types';
5 | import classnames from 'classnames';
6 | import { withRoomContext } from '../../../RoomContext';
7 | import { useIntl } from 'react-intl';
8 | import { permissions } from '../../../permissions';
9 | import { makePermissionSelector } from '../../../store/selectors';
10 | import ListItem from '@material-ui/core/ListItem';
11 | import ListItemText from '@material-ui/core/ListItemText';
12 | import IconButton from '@material-ui/core/IconButton';
13 | import ListItemAvatar from '@material-ui/core/ListItemAvatar';
14 | import Avatar from '@material-ui/core/Avatar';
15 | import EmptyAvatar from '../../../images/avatar-empty.jpeg';
16 | import PromoteIcon from '@material-ui/icons/OpenInBrowser';
17 | import Tooltip from '@material-ui/core/Tooltip';
18 |
19 | const styles = () =>
20 | ({
21 | root :
22 | {
23 | alignItems : 'center'
24 | }
25 | });
26 |
27 | const ListLobbyPeer = (props) =>
28 | {
29 | const {
30 | roomClient,
31 | peer,
32 | promotionInProgress,
33 | canPromote,
34 | classes
35 | } = props;
36 |
37 | const intl = useIntl();
38 |
39 | const picture = peer.picture || EmptyAvatar;
40 |
41 | return (
42 |
48 |
49 |
50 |
51 |
54 |
60 |
68 | {
69 | e.stopPropagation();
70 | roomClient.promoteLobbyPeer(peer.id);
71 | }}
72 | >
73 |
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | ListLobbyPeer.propTypes =
81 | {
82 | roomClient : PropTypes.any.isRequired,
83 | advancedMode : PropTypes.bool,
84 | peer : PropTypes.object.isRequired,
85 | promotionInProgress : PropTypes.bool.isRequired,
86 | canPromote : PropTypes.bool.isRequired,
87 | classes : PropTypes.object.isRequired
88 | };
89 |
90 | const makeMapStateToProps = (initialState, { id }) =>
91 | {
92 | const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER);
93 |
94 | const mapStateToProps = (state) =>
95 | {
96 | return {
97 | peer : state.lobbyPeers[id],
98 | promotionInProgress : state.room.lobbyPeersPromotionInProgress,
99 | canPromote : hasPermission(state)
100 | };
101 | };
102 |
103 | return mapStateToProps;
104 | };
105 |
106 | export default withRoomContext(connect(
107 | makeMapStateToProps,
108 | null,
109 | null,
110 | {
111 | areStatesEqual : (next, prev) =>
112 | {
113 | return (
114 | prev.room === next.room &&
115 | prev.peers === next.peers && // For checking permissions
116 | prev.me.roles === next.me.roles &&
117 | prev.lobbyPeers === next.lobbyPeers
118 | );
119 | }
120 | }
121 | )(withStyles(styles)(ListLobbyPeer)));
--------------------------------------------------------------------------------
/app/src/components/MeetingDrawer/ParticipantList/ListModerator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import PropTypes from 'prop-types';
5 | import { withRoomContext } from '../../../RoomContext';
6 | import { useIntl, FormattedMessage } from 'react-intl';
7 | import Button from '@material-ui/core/Button';
8 |
9 | const styles = (theme) =>
10 | ({
11 | root :
12 | {
13 | padding : theme.spacing(1),
14 | display : 'flex',
15 | flexWrap : 'wrap',
16 | marginRight : -theme.spacing(1),
17 | marginTop : -theme.spacing(1)
18 | },
19 | button :
20 | {
21 | marginTop : theme.spacing(1),
22 | marginRight : theme.spacing(1),
23 | flexGrow : '1'
24 | }
25 | });
26 |
27 | const ListModerator = (props) =>
28 | {
29 | const intl = useIntl();
30 |
31 | const {
32 | roomClient,
33 | room,
34 | classes
35 | } = props;
36 |
37 | return (
38 |
39 |
55 |
71 |
87 |
103 |
104 | );
105 | };
106 |
107 | ListModerator.propTypes =
108 | {
109 | roomClient : PropTypes.any.isRequired,
110 | room : PropTypes.object.isRequired,
111 | classes : PropTypes.object.isRequired
112 | };
113 |
114 | const mapStateToProps = (state) => ({
115 | room : state.room
116 | });
117 |
118 | export default withRoomContext(connect(
119 | mapStateToProps,
120 | null,
121 | null,
122 | {
123 | areStatesEqual : (next, prev) =>
124 | {
125 | return (
126 | prev.room === next.room
127 | );
128 | }
129 | }
130 | )(withStyles(styles)(ListModerator)));
--------------------------------------------------------------------------------
/app/src/__tests__/Room.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { act } from 'react-dom/test-utils';
5 | import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
6 | import Room from '../components/Room';
7 | import { SnackbarProvider } from 'notistack';
8 | import RoomContext from '../RoomContext';
9 |
10 | import configureStore from 'redux-mock-store';
11 |
12 | const mockStore = configureStore([]);
13 |
14 | let container;
15 |
16 | let store;
17 |
18 | let intl;
19 |
20 | const roomClient = {};
21 |
22 | beforeEach(() =>
23 | {
24 | container = document.createElement('div');
25 |
26 | store = mockStore({
27 | chat : [],
28 | consumers : {},
29 | files : {},
30 | lobbyPeers : {},
31 | me : {
32 | audioDevices : null,
33 | audioInProgress : false,
34 | audioOutputDevices : null,
35 | audioOutputInProgress : false,
36 | canSendMic : false,
37 | canSendWebcam : false,
38 | canShareFiles : false,
39 | canShareScreen : false,
40 | displayNameInProgress : false,
41 | id : 'jesttester',
42 | loggedIn : false,
43 | loginEnabled : true,
44 | picture : null,
45 | raisedHand : false,
46 | raisedHandInProgress : false,
47 | screenShareInProgress : false,
48 | webcamDevices : null,
49 | webcamInProgress : false
50 | },
51 | notifications : [],
52 | peerVolumes : {},
53 | peers : {},
54 | producers : {},
55 | room : {
56 | accessCode : '',
57 | activeSpeakerId : null,
58 | fullScreenConsumer : null,
59 | inLobby : true,
60 | joinByAccessCode : true,
61 | joined : false,
62 | lockDialogOpen : false,
63 | locked : false,
64 | mode : 'democratic',
65 | name : 'test',
66 | selectedPeerId : null,
67 | settingsOpen : false,
68 | showSettings : false,
69 | signInRequired : false,
70 | spotlights : [],
71 | state : 'connecting',
72 | toolbarsVisible : true,
73 | torrentSupport : false,
74 | windowConsumer : null
75 | },
76 | settings : {
77 | advancedMode : true,
78 | displayName : 'Jest Tester',
79 | resolution : 'ultra',
80 | selectedAudioDevice : 'default',
81 | selectedAudioOutputDevice : 'default',
82 | selectedWebcam : 'soifjsiajosjfoi'
83 | },
84 | toolarea : {
85 | currentToolTab : 'chat',
86 | toolAreaOpen : false,
87 | unreadFiles : 0,
88 | unreadMessages : 0
89 | }
90 | });
91 |
92 | const cache = createIntlCache();
93 |
94 | const locale = 'en';
95 |
96 | intl = createIntl({
97 | locale,
98 | messages : {}
99 | }, cache);
100 |
101 | document.body.appendChild(container);
102 | });
103 |
104 | afterEach(() =>
105 | {
106 | document.body.removeChild(container);
107 | container = null;
108 | });
109 |
110 | describe('', () =>
111 | {
112 | test('renders correctly', () =>
113 | {
114 | act(() =>
115 | {
116 | ReactDOM.render(
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | ,
126 | container);
127 | });
128 | });
129 | });
--------------------------------------------------------------------------------
/app/src/transforms/receiver.ts:
--------------------------------------------------------------------------------
1 | import Logger from '../Logger';
2 | import { store } from '../store/store';
3 | import * as consumerActions from '../store/actions/consumerActions';
4 |
5 | const logger = new Logger('transforms.receiver');
6 |
7 | /**
8 | * Pipes the receiver streams without parsing or modifications.
9 | * @param receiver
10 | */
11 | export function directReceiverTransform(receiver: RTCRtpReceiver)
12 | {
13 | logger.debug('directReceiverTransform', { receiver });
14 |
15 | // @ts-ignore
16 | const receiverStreams = receiver.createEncodedStreams();
17 | const readableStream = receiverStreams.readable || receiverStreams.readableStream;
18 | const writableStream = receiverStreams.writable || receiverStreams.writableStream;
19 |
20 | readableStream.pipeTo(writableStream);
21 | }
22 |
23 | // Opus config parser
24 | // Ref. https://tools.ietf.org/html/rfc6716#section-3.1
25 | const OPUS_CONFIGS = [
26 | 'Silk NB 10ms',
27 | 'Silk NB 20ms',
28 | 'Silk NB 40ms',
29 | 'Silk NB 60ms',
30 | //
31 | 'Silk MB 10ms',
32 | 'Silk MB 20ms',
33 | 'Silk MB 40ms',
34 | 'Silk MB 60ms',
35 | //
36 | 'Silk WB 10ms',
37 | 'Silk WB 20ms',
38 | 'Silk WB 40ms',
39 | 'Silk WB 60ms',
40 | //
41 | 'Hybrid SWB 10ms',
42 | 'Hybrid SWB 20ms',
43 | //
44 | 'Hybrid FB 10ms',
45 | 'Hybrid FB 20ms',
46 | //
47 | 'Celt NB 2.5ms',
48 | 'Celt NB 5ms',
49 | 'Celt NB 10ms',
50 | 'Celt NB 20ms',
51 | //
52 | 'Celt WB 2.5ms',
53 | 'Celt WB 5ms',
54 | 'Celt WB 10ms',
55 | 'Celt WB 20ms',
56 | //
57 | 'Celt SWB 2.5ms',
58 | 'Celt SWB 5ms',
59 | 'Celt SWB 10ms',
60 | 'Celt SWB 20ms',
61 | //
62 | 'Celt FB 2.5ms',
63 | 'Celt FB 5ms',
64 | 'Celt FB 10ms',
65 | 'Celt FB 20ms'
66 | ];
67 |
68 | const OPUS_STEREO = [
69 | 'M',
70 | 'S'
71 | ];
72 |
73 | const OPUS_FRAMES = [
74 | '1f', // 1 frame in the packet
75 | '2ef', // 2 frames in the packet, each with equal compressed size
76 | '2df', // 2 frames in the packet, with different compressed sizes
77 | 'af' // an arbitrary number of frames in the packet
78 | ];
79 |
80 | /**
81 | * Opus receiver transform
82 | * @param receiver
83 | * @param consumerId
84 | */
85 | export function opusReceiverTransform(receiver: RTCRtpReceiver, consumerId: string)
86 | {
87 | logger.debug('opusReceiverTransform', { receiver, consumerId });
88 |
89 | // @ts-ignore
90 | const receiverStreams = receiver.createEncodedStreams();
91 | const readableStream = receiverStreams.readable || receiverStreams.readableStream;
92 | const writableStream = receiverStreams.writable || receiverStreams.writableStream;
93 |
94 | const transformStream = new window.TransformStream({
95 | transform : (encodedFrame, controller) =>
96 | {
97 | if (encodedFrame.data.byteLength)
98 | {
99 | const byte = new DataView(encodedFrame.data, 0, 1).getUint8(0);
100 |
101 | const config = byte >> 3; // eslint-disable-line no-bitwise
102 | const stereo = (byte >> 1) & 0x01; // eslint-disable-line no-bitwise
103 | const frames = byte & 0x03; // eslint-disable-line no-bitwise
104 |
105 | const opusConfig = `${OPUS_CONFIGS[config]} ${OPUS_STEREO[stereo]} ${OPUS_FRAMES[frames]}`;
106 | const consumer = store.getState().consumers[consumerId];
107 |
108 | if (consumer?.opusConfig !== opusConfig)
109 | {
110 | logger.debug('opusReceiverTransform',
111 | { config, stereo, frames, opusConfig });
112 | store.dispatch(consumerActions.setConsumerOpusConfig(
113 | consumerId, opusConfig));
114 | }
115 | }
116 |
117 | controller.enqueue(encodedFrame);
118 | }
119 | });
120 |
121 | readableStream
122 | .pipeThrough(transformStream)
123 | .pipeTo(writableStream);
124 | }
125 |
--------------------------------------------------------------------------------
/app/src/store/reducers/consumers.js:
--------------------------------------------------------------------------------
1 | const initialState = {};
2 |
3 | const consumers = (state = initialState, action) =>
4 | {
5 | switch (action.type)
6 | {
7 | case 'ADD_CONSUMER':
8 | {
9 | const { consumer } = action.payload;
10 |
11 | return { ...state, [consumer.id]: consumer };
12 | }
13 |
14 | case 'REMOVE_CONSUMER':
15 | {
16 | const { consumerId } = action.payload;
17 | const newState = { ...state };
18 |
19 | delete newState[consumerId];
20 |
21 | return newState;
22 | }
23 |
24 | case 'SET_CONSUMER_PAUSED':
25 | {
26 | const { consumerId, originator } = action.payload;
27 | const consumer = state[consumerId];
28 |
29 | let newConsumer;
30 |
31 | if (originator === 'local')
32 | newConsumer = { ...consumer, locallyPaused: true };
33 | else
34 | newConsumer = { ...consumer, remotelyPaused: true };
35 |
36 | return { ...state, [consumerId]: newConsumer };
37 | }
38 |
39 | case 'SET_CONSUMER_RESUMED':
40 | {
41 | const { consumerId, originator } = action.payload;
42 | const consumer = state[consumerId];
43 |
44 | let newConsumer;
45 |
46 | if (originator === 'local')
47 | newConsumer = { ...consumer, locallyPaused: false };
48 | else
49 | newConsumer = { ...consumer, remotelyPaused: false };
50 |
51 | return { ...state, [consumerId]: newConsumer };
52 | }
53 |
54 | case 'SET_CONSUMER_CURRENT_LAYERS':
55 | {
56 | const { consumerId, spatialLayer, temporalLayer } = action.payload;
57 | const consumer = state[consumerId];
58 | const newConsumer =
59 | {
60 | ...consumer,
61 | currentSpatialLayer : spatialLayer,
62 | currentTemporalLayer : temporalLayer
63 | };
64 |
65 | return { ...state, [consumerId]: newConsumer };
66 | }
67 |
68 | case 'SET_CONSUMER_PREFERRED_LAYERS':
69 | {
70 | const { consumerId, spatialLayer, temporalLayer } = action.payload;
71 | const consumer = state[consumerId];
72 | const newConsumer =
73 | {
74 | ...consumer,
75 | preferredSpatialLayer : spatialLayer,
76 | preferredTemporalLayer : temporalLayer
77 | };
78 |
79 | return { ...state, [consumerId]: newConsumer };
80 | }
81 |
82 | case 'SET_CONSUMER_PRIORITY':
83 | {
84 | const { consumerId, priority } = action.payload;
85 | const consumer = state[consumerId];
86 | const newConsumer = { ...consumer, priority };
87 |
88 | return { ...state, [consumerId]: newConsumer };
89 | }
90 |
91 | case 'SET_CONSUMER_TRACK':
92 | {
93 | const { consumerId, track } = action.payload;
94 | const consumer = state[consumerId];
95 | const newConsumer = { ...consumer, track };
96 |
97 | return { ...state, [consumerId]: newConsumer };
98 | }
99 |
100 | case 'SET_CONSUMER_AUDIO_GAIN':
101 | {
102 | const { consumerId, audioGain } = action.payload;
103 | const consumer = state[consumerId];
104 | const newConsumer = { ...consumer, audioGain };
105 |
106 | return { ...state, [consumerId]: newConsumer };
107 | }
108 |
109 | case 'SET_CONSUMER_SCORE':
110 | {
111 | const { consumerId, score } = action.payload;
112 | const consumer = state[consumerId];
113 |
114 | if (!consumer)
115 | return state;
116 |
117 | const newConsumer = { ...consumer, score };
118 |
119 | return { ...state, [consumerId]: newConsumer };
120 | }
121 |
122 | case 'CLEAR_CONSUMERS':
123 | {
124 | return initialState;
125 | }
126 |
127 | case 'SET_CONSUMER_OPUS_CONFIG':
128 | {
129 | const { consumerId, opusConfig } = action.payload;
130 | const consumer = state[consumerId];
131 | const newConsumer =
132 | {
133 | ...consumer,
134 | opusConfig
135 | };
136 |
137 | return { ...state, [consumerId]: newConsumer };
138 | }
139 |
140 | default:
141 | return state;
142 | }
143 | };
144 |
145 | export default consumers;
146 |
--------------------------------------------------------------------------------
/app/src/store/reducers/me.js:
--------------------------------------------------------------------------------
1 | const initialState =
2 | {
3 | id : null,
4 | picture : null,
5 | browser : null,
6 | roles : [],
7 | canSendMic : false,
8 | canSendWebcam : false,
9 | canShareScreen : false,
10 | canShareFiles : false,
11 | audioDevices : null,
12 | webcamDevices : null,
13 | webcamInProgress : false,
14 | audioInProgress : false,
15 | screenShareInProgress : false,
16 | displayNameInProgress : false,
17 | loginEnabled : false,
18 | raisedHand : false,
19 | raisedHandInProgress : false,
20 | loggedIn : false,
21 | isSpeaking : false,
22 | isAutoMuted : true
23 | };
24 |
25 | const me = (state = initialState, action) =>
26 | {
27 | switch (action.type)
28 | {
29 | case 'SET_ME':
30 | {
31 | const {
32 | peerId,
33 | loginEnabled
34 | } = action.payload;
35 |
36 | return {
37 | ...state,
38 | id : peerId,
39 | loginEnabled
40 | };
41 | }
42 |
43 | case 'SET_BROWSER':
44 | {
45 | const { browser } = action.payload;
46 |
47 | return { ...state, browser };
48 | }
49 |
50 | case 'LOGGED_IN':
51 | {
52 | const { flag } = action.payload;
53 |
54 | return { ...state, loggedIn: flag };
55 | }
56 |
57 | case 'ADD_ROLE':
58 | {
59 | const roles = [ ...state.roles, action.payload.roleId ];
60 |
61 | return { ...state, roles };
62 | }
63 |
64 | case 'REMOVE_ROLE':
65 | {
66 | const roles = state.roles.filter((roleId) =>
67 | roleId !== action.payload.roleId);
68 |
69 | return { ...state, roles };
70 | }
71 |
72 | case 'SET_PICTURE':
73 | return { ...state, picture: action.payload.picture };
74 |
75 | case 'SET_MEDIA_CAPABILITIES':
76 | {
77 | const {
78 | canSendMic,
79 | canSendWebcam,
80 | canShareScreen,
81 | canShareFiles
82 | } = action.payload;
83 |
84 | return {
85 | ...state,
86 | canSendMic,
87 | canSendWebcam,
88 | canShareScreen,
89 | canShareFiles
90 | };
91 | }
92 |
93 | case 'SET_AUDIO_DEVICES':
94 | {
95 | const { devices } = action.payload;
96 |
97 | return { ...state, audioDevices: devices };
98 | }
99 |
100 | case 'SET_AUDIO_OUTPUT_DEVICES':
101 | {
102 | const { devices } = action.payload;
103 |
104 | return { ...state, audioOutputDevices: devices };
105 | }
106 |
107 | case 'SET_WEBCAM_DEVICES':
108 | {
109 | const { devices } = action.payload;
110 |
111 | return { ...state, webcamDevices: devices };
112 | }
113 |
114 | case 'SET_AUDIO_IN_PROGRESS':
115 | {
116 | const { flag } = action.payload;
117 |
118 | return { ...state, audioInProgress: flag };
119 | }
120 |
121 | case 'SET_WEBCAM_IN_PROGRESS':
122 | {
123 | const { flag } = action.payload;
124 |
125 | return { ...state, webcamInProgress: flag };
126 | }
127 |
128 | case 'SET_SCREEN_SHARE_IN_PROGRESS':
129 | {
130 | const { flag } = action.payload;
131 |
132 | return { ...state, screenShareInProgress: flag };
133 | }
134 |
135 | case 'SET_RAISED_HAND':
136 | {
137 | const { flag } = action.payload;
138 |
139 | return { ...state, raisedHand: flag };
140 | }
141 |
142 | case 'SET_RAISED_HAND_IN_PROGRESS':
143 | {
144 | const { flag } = action.payload;
145 |
146 | return { ...state, raisedHandInProgress: flag };
147 | }
148 |
149 | case 'SET_DISPLAY_NAME_IN_PROGRESS':
150 | {
151 | const { flag } = action.payload;
152 |
153 | return { ...state, displayNameInProgress: flag };
154 | }
155 |
156 | case 'SET_IS_SPEAKING':
157 | {
158 | const { flag } = action.payload;
159 |
160 | return { ...state, isSpeaking: flag };
161 | }
162 |
163 | case 'SET_AUTO_MUTED':
164 | {
165 | const { flag } = action.payload;
166 |
167 | return { ...state, isAutoMuted: flag };
168 | }
169 |
170 | default:
171 | return state;
172 | }
173 | };
174 |
175 | export default me;
176 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "edumeet",
3 | "version": "3.5.1",
4 | "private": true,
5 | "description": "edumeet meeting service",
6 | "author": "Håvar Aambø Fosstveit ",
7 | "contributors": [
8 | "Stefan Otto",
9 | "Mészáros Mihály",
10 | "Roman Drozd",
11 | "Rémai Gábor László",
12 | "Piotr Pawałowski"
13 | ],
14 | "license": "MIT",
15 | "homepage": "./",
16 | "main": "src/electron-starter.js",
17 | "type": "module",
18 | "dependencies": {
19 | "@material-ui/core": "^4.11.3",
20 | "@material-ui/icons": "^4.11.2",
21 | "@material-ui/lab": "^4.0.0-alpha.57",
22 | "@react-hook/window-size": "^3.0.7",
23 | "@types/node": "^14.14.37",
24 | "@types/react": "^17.0.3",
25 | "@types/react-dom": "^17.0.3",
26 | "bowser": "^2.7.0",
27 | "chroma-js": "^2.1.1",
28 | "classnames": "^2.2.6",
29 | "convict": "^6.0.1",
30 | "convict-format-with-validator": "^6.0.1",
31 | "create-torrent": "^4.4.1",
32 | "deep-object-diff": "^1.1.0",
33 | "dompurify": "^2.0.7",
34 | "domready": "^1.0.8",
35 | "draft-js": "^0.11.7",
36 | "draft-js-export-html": "^1.4.1",
37 | "draft-js-plugins-editor": "^3.0.0",
38 | "draft-js-single-line-plugin": "^2.0.5",
39 | "end-of-stream": "1.4.1",
40 | "file-saver": "^2.0.2",
41 | "hark": "^1.2.3",
42 | "idb": "^6.0.0",
43 | "is-electron": "^2.2.0",
44 | "marked": "^0.8.0",
45 | "material-ui-popup-state": "^1.8.0",
46 | "mediasoup-client": "^3.6.47",
47 | "notistack": "^0.9.5",
48 | "prop-types": "^15.7.2",
49 | "random-string": "^0.2.0",
50 | "react": "^16.10.2",
51 | "react-cookie-consent": "^2.5.0",
52 | "react-dom": "^16.10.2",
53 | "react-flip-toolkit": "^7.0.9",
54 | "react-image-file-resizer": "^0.3.8",
55 | "react-images-upload": "^1.2.0",
56 | "react-intl": "^3.4.0",
57 | "react-intl-redux": "^2.2.0",
58 | "react-redux": "^7.2.3",
59 | "react-router-dom": "^5.1.2",
60 | "react-scripts": "^4.0.3",
61 | "react-wakelock-react16": "0.0.7",
62 | "redux": "^4.0.4",
63 | "redux-logger": "^3.0.6",
64 | "redux-persist": "^6.0.0",
65 | "redux-persist-transform-filter": "0.0.20",
66 | "redux-thunk": "^2.3.0",
67 | "reselect": "^4.0.0",
68 | "riek": "^1.1.0",
69 | "socket.io-client": "^2.4.0",
70 | "source-map-explorer": "^2.1.0",
71 | "streamsaver": "^2.0.5",
72 | "typescript": "^4.2.4",
73 | "web-streams-polyfill": "^3.0.2",
74 | "webtorrent": "^0.108.1"
75 | },
76 | "scripts": {
77 | "analyze": "source-map-explorer build/static/js/*",
78 | "start": "HTTPS=true PORT=4443 react-scripts start",
79 | "build": "react-scripts build && rm -rf ../server/public && DEST='../server/dist/public' && rm -rf $DEST && mkdir -p $DEST && mv -T build/ $DEST",
80 | "test": "react-scripts test",
81 | "eject": "react-scripts eject",
82 | "electron": "electron --no-sandbox .",
83 | "dev": "nf start -p 3000",
84 | "lint": "eslint ./ --ext .js,.jsx,.ts,.tsx; exit 0",
85 | "lint-fix": "eslint ./ --fix --ext .js,.jsx,.ts,.tsx; exit 0",
86 | "gen-config-docs": "node --loader ts-node/esm src/config.ts && eslint -c .eslintrc.json public/config/config.example.js --fix"
87 | },
88 | "browserslist": [
89 | ">0.2%",
90 | "not dead",
91 | "not ie > 0",
92 | "not op_mini all"
93 | ],
94 | "devDependencies": {
95 | "@types/chroma-js": "^2.1.3",
96 | "@types/convict": "^6.0.1",
97 | "@types/convict-format-with-validator": "^6.0.2",
98 | "@typescript-eslint/eslint-plugin": "^4.20.0",
99 | "@typescript-eslint/parser": "^4.20.0",
100 | "babel-eslint": "^10.1.0",
101 | "electron": "^12.0.0",
102 | "eslint-config-react-app": "^6.0.0",
103 | "eslint-import-resolver-typescript": "^2.4.0",
104 | "eslint-plugin-flowtype": "^5.4.0",
105 | "eslint-plugin-import": "^2.22.1",
106 | "eslint-plugin-jest": "^24.3.3",
107 | "eslint-plugin-jsx-a11y": "^6.4.1",
108 | "eslint-plugin-react": "^7.23.1",
109 | "eslint-plugin-react-hooks": "^4.2.0",
110 | "eslint-webpack-plugin": "^2.5.3",
111 | "foreman": "^3.0.1",
112 | "redux-mock-store": "^1.5.3",
113 | "ts-node": "^10.5.0"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/components/Settings/AdvancedSettings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import { withRoomContext } from '../../RoomContext';
5 | import * as settingsActions from '../../store/actions/settingsActions';
6 | import PropTypes from 'prop-types';
7 | import classnames from 'classnames';
8 | import { useIntl, FormattedMessage } from 'react-intl';
9 | import MenuItem from '@material-ui/core/MenuItem';
10 | import FormHelperText from '@material-ui/core/FormHelperText';
11 | import FormControl from '@material-ui/core/FormControl';
12 | import FormControlLabel from '@material-ui/core/FormControlLabel';
13 | import Select from '@material-ui/core/Select';
14 | import Switch from '@material-ui/core/Switch';
15 | import { config } from '../../config';
16 |
17 | const styles = (theme) =>
18 | ({
19 | setting :
20 | {
21 | padding : theme.spacing(2)
22 | },
23 | formControl :
24 | {
25 | display : 'flex'
26 | },
27 | switchLabel : {
28 | justifyContent : 'space-between',
29 | flex : 'auto',
30 | display : 'flex',
31 | padding : theme.spacing(1),
32 | marginRight : 0
33 | }
34 | });
35 |
36 | const AdvancedSettings = ({
37 | roomClient,
38 | settings,
39 | onToggleAdvancedMode,
40 | onToggleNotificationSounds,
41 | classes
42 | }) =>
43 | {
44 | const intl = useIntl();
45 |
46 | return (
47 |
48 | }
51 | labelPlacement='start'
52 | label={intl.formatMessage({
53 | id : 'settings.advancedMode',
54 | defaultMessage : 'Advanced mode'
55 | })}
56 | />
57 | }
60 | labelPlacement='start'
61 | label={intl.formatMessage({
62 | id : 'settings.notificationSounds',
63 | defaultMessage : 'Notification sounds'
64 | })}
65 | />
66 | { !config.lockLastN &&
67 |
100 | }
101 |
102 | );
103 | };
104 |
105 | AdvancedSettings.propTypes =
106 | {
107 | roomClient : PropTypes.any.isRequired,
108 | settings : PropTypes.object.isRequired,
109 | onToggleAdvancedMode : PropTypes.func.isRequired,
110 | onToggleNotificationSounds : PropTypes.func.isRequired,
111 | classes : PropTypes.object.isRequired
112 | };
113 |
114 | const mapStateToProps = (state) =>
115 | ({
116 | settings : state.settings
117 | });
118 |
119 | const mapDispatchToProps = {
120 | onToggleAdvancedMode : settingsActions.toggleAdvancedMode,
121 | onToggleNotificationSounds : settingsActions.toggleNotificationSounds
122 | };
123 |
124 | export default withRoomContext(connect(
125 | mapStateToProps,
126 | mapDispatchToProps,
127 | null,
128 | {
129 | areStatesEqual : (next, prev) =>
130 | {
131 | return (
132 | prev.settings === next.settings
133 | );
134 | }
135 | }
136 | )(withStyles(styles)(AdvancedSettings)));
--------------------------------------------------------------------------------
/app/src/store/store.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | applyMiddleware,
4 | compose
5 | } from 'redux';
6 | import thunk from 'redux-thunk';
7 | import { createLogger } from 'redux-logger';
8 | import { createMigrate, persistStore, persistReducer } from 'redux-persist';
9 | import storage from 'redux-persist/lib/storage';
10 | import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
11 | // import { createFilter } from 'redux-persist-transform-filter';
12 | import { diff } from 'deep-object-diff';
13 | import rootReducer from './reducers/rootReducer';
14 | import Logger from '../Logger';
15 | import { config } from '../config';
16 |
17 | const logger = new Logger('store');
18 |
19 | const migrations =
20 | {
21 | // initial version 0: we will clean up all historical data
22 | // from local storage for the time
23 | // before we began with migration versioning
24 | // next version 1 we have to implement like this:
25 | // oldValue = undefined; // will remove oldValue from next local storage
26 | // new values can be defined from app/public/config.js and go that way to new local storage
27 | // redux-persist will save a version number to each local store.
28 | // Next time store is initialized it will check if there are newer versions here in migrations
29 | // and iterate over all defined greater version functions until version in persistConfig is reached.
30 | 0 : (state) =>
31 | {
32 | state = {};
33 |
34 | return { ...state };
35 | },
36 | 1 : (state) =>
37 | {
38 | state.me = undefined;
39 |
40 | return { ...state };
41 | },
42 | 2 : (state) =>
43 | {
44 | state.settings.autoGainControl = true;
45 |
46 | return { ...state };
47 | }
48 | // Next version
49 | // 4 : (state) =>
50 | // {
51 | // return { ...state };
52 | // }
53 | };
54 |
55 | const persistConfig =
56 | {
57 | key : 'root',
58 | storage : storage,
59 | // migrate will iterate state over all version-functions
60 | // from migrations until version is reached
61 | version : 2,
62 | migrate : createMigrate(migrations, { debug: true }),
63 | stateReconciler : autoMergeLevel2,
64 | whitelist : [ 'settings', 'intl', 'config' ]
65 | };
66 |
67 | /* const saveSubsetFilter = createFilter(
68 | 'me',
69 | [ 'loggedIn' ]
70 | );*/
71 |
72 | const reduxMiddlewares =
73 | [
74 | thunk
75 | ];
76 |
77 | if (process.env.REACT_APP_DEBUG === '*' || process.env.NODE_ENV !== 'production')
78 | {
79 | const LOG_IGNORE = [
80 | 'SET_PEER_VOLUME',
81 | 'SET_ROOM_ACTIVE_SPEAKER',
82 | 'ADD_TRANSPORT_STATS'
83 | ];
84 |
85 | const reduxLogger = createLogger(
86 | {
87 | predicate : (getState, action) => LOG_IGNORE.indexOf(action.type) === -1,
88 | duration : true,
89 | collapsed : true,
90 | timestamp : false,
91 | level : 'info',
92 | logErrors : true
93 | });
94 |
95 | reduxMiddlewares.push(reduxLogger);
96 | }
97 |
98 | const composeEnhancers =
99 | typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
100 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) :
101 | compose;
102 |
103 | const enhancer = composeEnhancers(
104 | applyMiddleware(...reduxMiddlewares)
105 | );
106 |
107 | const pReducer = persistReducer(persistConfig, rootReducer);
108 |
109 | const initialState = {
110 | intl : {
111 | locale : null,
112 | messages : null
113 | }
114 | // ...other initialState
115 | };
116 |
117 | export const store = createStore(
118 | pReducer,
119 | initialState,
120 | enhancer
121 | );
122 |
123 | export const persistor = persistStore(store, null, () =>
124 | {
125 | // Check if the app config differs from the stored version.
126 | const currentConfig = store.getState().config;
127 | const changed = diff(currentConfig, config);
128 | const changedKeys = Object.keys(changed);
129 |
130 | if (changedKeys.length)
131 | {
132 | logger.debug('store config changed:', changed);
133 | const changedSettings = {};
134 |
135 | changedKeys.forEach((key) =>
136 | {
137 | changedSettings[key] = config[key];
138 | });
139 |
140 | store.dispatch({ type: 'SETTINGS_UPDATE', payload: changedSettings });
141 | store.dispatch({ type: 'CONFIG_SET', payload: config });
142 | }
143 | });
144 |
145 | /*
146 |
147 | export const persistor = persistStore(store, {
148 | transforms : [
149 | saveSubsetFilter
150 | ]
151 |
152 | });
153 | */
154 |
--------------------------------------------------------------------------------
/.github/workflows/develop-deb.yml:
--------------------------------------------------------------------------------
1 | name: Debian package
2 |
3 | on:
4 | push:
5 | branches: [ master, develop ]
6 | # pull_request:
7 | # branches: [ develop ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | env:
14 | CI: false
15 |
16 | strategy:
17 | matrix:
18 | node-version: [16.x]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | with:
23 | path: edumeet
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 |
29 | - name: Get eduMEET version
30 | id: get-version
31 | run: |
32 | echo "::set-output name=VERSION::$(cat edumeet/server/package.json | jq -r '.version')"
33 |
34 | - name: Build Debian package
35 | id: build-deb
36 | run: |
37 | cd edumeet
38 | cp server/config/config.example.js server/config/config.js
39 | cp server/config/config.example.yaml server/config/config.yaml
40 | cp app/public/config/config.example.js app/public/config/config.js
41 | cd app
42 | yarn install && yarn build
43 | cd ../server
44 | yarn install && yarn build
45 | cat <<< $(jq '.bundleDependencies += .dependencies' package.json) > package.json
46 | npm pack
47 | VERSION=${{ steps.get-version.outputs.VERSION }}
48 | DATE=$(date)
49 | mkdir -p /home/runner/package
50 | cd /home/runner/package
51 | mkdir DEBIAN
52 | mkdir -p usr/local/src/edumeet/server
53 | mkdir -p etc/systemd/system/
54 | tar -xf /home/runner/work/***/***/***/server/***-server-$VERSION.tgz package/ 1>/dev/null 2>/dev/null || true
55 | mv package/* usr/local/src/edumeet/server/
56 | mv /home/runner/work/***/***/***/*.service etc/systemd/system/
57 | rm -rf package
58 | touch DEBIAN/md5sums
59 | touch DEBIAN/md5sums
60 | touch DEBIAN/control
61 | #find . -type f ! -regex '.*.hg.*' ! -regex '.*?debian-binary.*' ! -regex '.*?DEBIAN.*' -printf '%P ' | xargs md5sum 1>/dev/null 2>/dev/null || true
62 | #
63 | cat > DEBIAN/control <= 16), redis
75 | EOF
76 | #
77 | cat > DEBIAN/postinst < around 50 concurrent users per thread --> 400 concurrent users per server
29 | * Example 3(refering to example server from above): lastN=5: 1 one big room 4000 [consumer] / 10 [consumer/participant] = 400 [participant] That's the maximum number of participants per server for lastN=5 in one big room.
30 | * Example 3(refering to example server from above): lastN=25: 1 one big room 4000 [consumer] / 50 [consumer/participant] = 80 [participant] That's the maximum number of participants per server for lastN=25 in one single big room
31 |
32 |
33 | ### Bandwidth:
34 | * Configurable: maxIncomingBitrate per participant in server config
35 | * Low video bandwidth is around 160Kbps (240p-vp8)
36 | * Typical acceptable good video bandwidth is around (800-1000)Kbps (720p)
37 | * Possibility to activate Simulcast / SVC to provide different clients with different bandwidths
38 | ## Scaling
39 | You can setup more than 1 server with same configuration and load balance with [HAproxy.md](HAproxy.md)
40 | This will scale linearly.
41 |
42 | ### Limitations / work in progress / ToDo
43 | You can fine tune max number of active streams in same room by setting lastN parameter in server/config/config.js - this is then globally for whole server installation. Clients can override this in advanced settings locally.
44 |
45 | There is heavy development for separating signal/control part from media part (branch **[feat-media-node](https://github.com/edumeet/edumeet/tree/feat-media-node)** ) when this is ready you can fire up several media nodes completely separated from signal/control. For multi-tenant you can install one server node (or more for redundancy) per tenant with separate configurations/domains and share all media nodes across tenants. One room can then spread over several media nodes so max number of participants is limited only by size of your infrastructure.
46 |
47 | Right now simulcast is supported and we are working on using bandwidth more effectively. Small video windows don't need high quality video streams so we should switch to lower quality streams according to video container sizes on screen. This could enable for much higher number of lastN.
48 |
--------------------------------------------------------------------------------
/app/src/components/Settings/Settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import * as roomActions from '../../store/actions/roomActions';
5 | import PropTypes from 'prop-types';
6 | import { useIntl, FormattedMessage } from 'react-intl';
7 | import Tabs from '@material-ui/core/Tabs';
8 | import Tab from '@material-ui/core/Tab';
9 | import MediaSettings from './MediaSettings';
10 | import AppearanceSettings from './AppearanceSettings';
11 | import AdvancedSettings from './AdvancedSettings';
12 | import Dialog from '@material-ui/core/Dialog';
13 | import DialogTitle from '@material-ui/core/DialogTitle';
14 | import DialogActions from '@material-ui/core/DialogActions';
15 | import Button from '@material-ui/core/Button';
16 | import Close from '@material-ui/icons/Close';
17 |
18 | const tabs =
19 | [
20 | 'media',
21 | 'appearance',
22 | 'advanced'
23 | ];
24 |
25 | const styles = (theme) =>
26 | ({
27 | root :
28 | {
29 | },
30 | dialogPaper :
31 | {
32 | width : '30vw',
33 | [theme.breakpoints.down('lg')] :
34 | {
35 | width : '40vw'
36 | },
37 | [theme.breakpoints.down('md')] :
38 | {
39 | width : '50vw'
40 | },
41 | [theme.breakpoints.down('sm')] :
42 | {
43 | width : '70vw'
44 | },
45 | [theme.breakpoints.down('xs')] :
46 | {
47 | width : '90vw'
48 | }
49 | },
50 | tabsHeader :
51 | {
52 | flexGrow : 1
53 | }
54 | });
55 |
56 | const Settings = ({
57 | currentSettingsTab,
58 | settingsOpen,
59 | handleCloseSettings,
60 | setSettingsTab,
61 | classes
62 | }) =>
63 | {
64 | const intl = useIntl();
65 |
66 | return (
67 |
125 | );
126 | };
127 |
128 | Settings.propTypes =
129 | {
130 | currentSettingsTab : PropTypes.string.isRequired,
131 | settingsOpen : PropTypes.bool.isRequired,
132 | handleCloseSettings : PropTypes.func.isRequired,
133 | setSettingsTab : PropTypes.func.isRequired,
134 | classes : PropTypes.object.isRequired
135 | };
136 |
137 | const mapStateToProps = (state) =>
138 | ({
139 | currentSettingsTab : state.room.currentSettingsTab,
140 | settingsOpen : state.room.settingsOpen
141 | });
142 |
143 | const mapDispatchToProps = {
144 | handleCloseSettings : roomActions.setSettingsOpen,
145 | setSettingsTab : roomActions.setSettingsTab
146 | };
147 |
148 | export default connect(
149 | mapStateToProps,
150 | mapDispatchToProps,
151 | null,
152 | {
153 | areStatesEqual : (next, prev) =>
154 | {
155 | return (
156 | prev.room.currentSettingsTab === next.room.currentSettingsTab &&
157 | prev.room.settingsOpen === next.room.settingsOpen
158 | );
159 | }
160 | }
161 | )(withStyles(styles)(Settings));
--------------------------------------------------------------------------------
/app/src/components/UnsupportedBrowser.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withStyles } from '@material-ui/core/styles';
3 | import PropTypes from 'prop-types';
4 | import { FormattedMessage } from 'react-intl';
5 |
6 | import Dialog from '@material-ui/core/Dialog';
7 | import DialogTitle from '@material-ui/core/DialogTitle';
8 | import DialogContent from '@material-ui/core/DialogContent';
9 | import Grid from '@material-ui/core/Grid';
10 | import List from '@material-ui/core/List';
11 | import ListItem from '@material-ui/core/ListItem';
12 | import ListItemText from '@material-ui/core/ListItemText';
13 | import ListItemAvatar from '@material-ui/core/ListItemAvatar';
14 | import Avatar from '@material-ui/core/Avatar';
15 | import WebAssetIcon from '@material-ui/icons/WebAsset';
16 | import ErrorIcon from '@material-ui/icons/Error';
17 | import Hidden from '@material-ui/core/Hidden';
18 |
19 | const styles = (theme) =>
20 | ({
21 | dialogPaper :
22 | {
23 | width : '40vw',
24 | [theme.breakpoints.down('lg')] :
25 | {
26 | width : '40vw'
27 | },
28 | [theme.breakpoints.down('md')] :
29 | {
30 | width : '50vw'
31 | },
32 | [theme.breakpoints.down('sm')] :
33 | {
34 | width : '70vw'
35 | },
36 | [theme.breakpoints.down('xs')] :
37 | {
38 | width : '90vw'
39 | }
40 | // display : 'flex',
41 | // flexDirection : 'column'
42 | },
43 | list : {
44 | backgroundColor : theme.palette.background.paper
45 | },
46 | errorAvatar : {
47 | width : theme.spacing(20),
48 | height : theme.spacing(20)
49 | }
50 | });
51 |
52 | let dense = false;
53 |
54 | const supportedBrowsers=[
55 | { name: 'Chrome/Chromium', version: '74', vendor: 'Google' },
56 | { name: 'Edge', version: '18', vendor: 'Microsoft' },
57 | { name: 'Firefox', version: '60', vendor: 'Mozilla' },
58 | { name: 'Safari', version: '12', vendor: 'Apple' },
59 | { name: 'Opera', version: '62', vendor: '' },
60 | // { name: 'Brave', version: '1.5', vendor: '' },
61 | // { name: 'Vivaldi', version: '3', vendor: '' },
62 | { name: 'Samsung Internet', version: '11.1.1.52', vendor: '' }
63 | ];
64 |
65 | const UnsupportedBrowser = ({
66 | platform,
67 | webrtcUnavailable,
68 | classes
69 | }) =>
70 | {
71 | if (platform !== 'desktop')
72 | dense = true;
73 |
74 | return (
75 |
139 | );
140 | };
141 |
142 | UnsupportedBrowser.propTypes =
143 | {
144 | webrtcUnavailable : PropTypes.bool.isRequired,
145 | platform : PropTypes.string.isRequired,
146 | classes : PropTypes.object.isRequired
147 | };
148 |
149 | export default withStyles(styles)(UnsupportedBrowser);
150 |
--------------------------------------------------------------------------------
/app/src/components/Controls/ExtraVideo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import { withRoomContext } from '../../RoomContext';
5 | import * as roomActions from '../../store/actions/roomActions';
6 | import PropTypes from 'prop-types';
7 | import { useIntl, FormattedMessage } from 'react-intl';
8 | import Dialog from '@material-ui/core/Dialog';
9 | import DialogTitle from '@material-ui/core/DialogTitle';
10 | import DialogActions from '@material-ui/core/DialogActions';
11 | import Button from '@material-ui/core/Button';
12 | import MenuItem from '@material-ui/core/MenuItem';
13 | import FormHelperText from '@material-ui/core/FormHelperText';
14 | import FormControl from '@material-ui/core/FormControl';
15 | import Select from '@material-ui/core/Select';
16 |
17 | const styles = (theme) =>
18 | ({
19 | dialogPaper :
20 | {
21 | width : '30vw',
22 | [theme.breakpoints.down('lg')] :
23 | {
24 | width : '40vw'
25 | },
26 | [theme.breakpoints.down('md')] :
27 | {
28 | width : '50vw'
29 | },
30 | [theme.breakpoints.down('sm')] :
31 | {
32 | width : '70vw'
33 | },
34 | [theme.breakpoints.down('xs')] :
35 | {
36 | width : '90vw'
37 | }
38 | },
39 | setting :
40 | {
41 | padding : theme.spacing(2)
42 | },
43 | formControl :
44 | {
45 | display : 'flex'
46 | }
47 | });
48 |
49 | const ExtraVideo = ({
50 | roomClient,
51 | extraVideoOpen,
52 | webcamDevices,
53 | handleCloseExtraVideo,
54 | classes
55 | }) =>
56 | {
57 | const intl = useIntl();
58 |
59 | const [ videoDevice, setVideoDevice ] = React.useState('');
60 |
61 | const handleChange = (event) =>
62 | {
63 | setVideoDevice(event.target.value);
64 | };
65 |
66 | let videoDevices;
67 |
68 | if (webcamDevices)
69 | videoDevices = Object.values(webcamDevices);
70 | else
71 | videoDevices = [];
72 |
73 | return (
74 |
132 | );
133 | };
134 |
135 | ExtraVideo.propTypes =
136 | {
137 | roomClient : PropTypes.object.isRequired,
138 | extraVideoOpen : PropTypes.bool.isRequired,
139 | webcamDevices : PropTypes.object,
140 | handleCloseExtraVideo : PropTypes.func.isRequired,
141 | classes : PropTypes.object.isRequired
142 | };
143 |
144 | const mapStateToProps = (state) =>
145 | ({
146 | webcamDevices : state.me.webcamDevices,
147 | extraVideoOpen : state.room.extraVideoOpen
148 | });
149 |
150 | const mapDispatchToProps = {
151 | handleCloseExtraVideo : roomActions.setExtraVideoOpen
152 | };
153 |
154 | export default withRoomContext(connect(
155 | mapStateToProps,
156 | mapDispatchToProps,
157 | null,
158 | {
159 | areStatesEqual : (next, prev) =>
160 | {
161 | return (
162 | prev.me.webcamDevices === next.me.webcamDevices &&
163 | prev.room.extraVideoOpen === next.room.extraVideoOpen
164 | );
165 | }
166 | }
167 | )(withStyles(styles)(ExtraVideo)));
--------------------------------------------------------------------------------
/app/src/store/reducers/peers.js:
--------------------------------------------------------------------------------
1 | const initialState = {};
2 |
3 | const peer = (state = initialState, action) =>
4 | {
5 | switch (action.type)
6 | {
7 | case 'ADD_PEER':
8 | return action.payload.peer;
9 |
10 | case 'SET_PEER_DISPLAY_NAME':
11 | return { ...state, displayName: action.payload.displayName };
12 |
13 | case 'SET_PEER_VIDEO_IN_PROGRESS':
14 | return { ...state, peerVideoInProgress: action.payload.flag };
15 |
16 | case 'SET_PEER_AUDIO_IN_PROGRESS':
17 | return { ...state, peerAudioInProgress: action.payload.flag };
18 |
19 | case 'SET_PEER_SCREEN_IN_PROGRESS':
20 | return { ...state, peerScreenInProgress: action.payload.flag };
21 |
22 | case 'SET_PEER_KICK_IN_PROGRESS':
23 | return { ...state, peerKickInProgress: action.payload.flag };
24 |
25 | case 'SET_PEER_MODIFY_ROLES_IN_PROGRESS':
26 | return { ...state, peerModifyRolesInProgress: action.payload.flag };
27 |
28 | case 'SET_PEER_RAISED_HAND':
29 | return {
30 | ...state,
31 | raisedHand : action.payload.raisedHand,
32 | raisedHandTimestamp : action.payload.raisedHandTimestamp
33 | };
34 |
35 | case 'SET_PEER_RAISED_HAND_IN_PROGRESS':
36 | return {
37 | ...state,
38 | raisedHandInProgress : action.payload.flag
39 | };
40 |
41 | case 'ADD_CONSUMER':
42 | {
43 | const consumers = [ ...state.consumers, action.payload.consumer.id ];
44 |
45 | return { ...state, consumers };
46 | }
47 |
48 | case 'REMOVE_CONSUMER':
49 | {
50 | const consumers = state.consumers.filter((consumer) =>
51 | consumer !== action.payload.consumerId);
52 |
53 | return { ...state, consumers };
54 | }
55 |
56 | case 'SET_PEER_PICTURE':
57 | {
58 | return { ...state, picture: action.payload.picture };
59 | }
60 |
61 | case 'ADD_PEER_ROLE':
62 | {
63 | const roles = [ ...state.roles, action.payload.roleId ];
64 |
65 | return { ...state, roles };
66 | }
67 |
68 | case 'REMOVE_PEER_ROLE':
69 | {
70 | const roles = state.roles.filter((roleId) =>
71 | roleId !== action.payload.roleId);
72 |
73 | return { ...state, roles };
74 | }
75 |
76 | case 'STOP_PEER_AUDIO_IN_PROGRESS':
77 | return {
78 | ...state,
79 | stopPeerAudioInProgress : action.payload.flag
80 | };
81 |
82 | case 'STOP_PEER_VIDEO_IN_PROGRESS':
83 | return {
84 | ...state,
85 | stopPeerVideoInProgress : action.payload.flag
86 | };
87 |
88 | case 'STOP_PEER_SCREEN_SHARING_IN_PROGRESS':
89 | return {
90 | ...state,
91 | stopPeerScreenSharingInProgress : action.payload.flag
92 | };
93 |
94 | case 'SET_PEER_LOCAL_RECORDING_STATE':
95 | return {
96 | ...state,
97 | localRecordingState : action.payload.localRecordingState
98 | };
99 |
100 | case 'SET_PEER_LOCAL_RECORDING_CONSENT':
101 | return {
102 | ...state,
103 | localRecordingConsent : action.payload.consent
104 | };
105 |
106 | default:
107 | return state;
108 | }
109 | };
110 |
111 | const peers = (state = initialState, action) =>
112 | {
113 | switch (action.type)
114 | {
115 | case 'ADD_PEER':
116 | {
117 | return { ...state, [action.payload.peer.id]: peer(undefined, action) };
118 | }
119 |
120 | case 'REMOVE_PEER':
121 | {
122 | const { peerId } = action.payload;
123 | const newState = { ...state };
124 |
125 | delete newState[peerId];
126 |
127 | return newState;
128 | }
129 |
130 | case 'SET_PEER_DISPLAY_NAME':
131 | case 'SET_PEER_VIDEO_IN_PROGRESS':
132 | case 'SET_PEER_AUDIO_IN_PROGRESS':
133 | case 'SET_PEER_SCREEN_IN_PROGRESS':
134 | case 'SET_PEER_RAISED_HAND':
135 | case 'SET_PEER_RAISED_HAND_IN_PROGRESS':
136 | case 'SET_PEER_PICTURE':
137 | case 'ADD_CONSUMER':
138 | case 'ADD_PEER_ROLE':
139 | case 'REMOVE_PEER_ROLE':
140 | case 'STOP_PEER_AUDIO_IN_PROGRESS':
141 | case 'STOP_PEER_VIDEO_IN_PROGRESS':
142 | case 'STOP_PEER_SCREEN_SHARING_IN_PROGRESS':
143 | case 'SET_PEER_KICK_IN_PROGRESS':
144 | case 'SET_PEER_MODIFY_ROLES_IN_PROGRESS':
145 | case 'REMOVE_CONSUMER':
146 | case 'SET_PEER_LOCAL_RECORDING_STATE':
147 | {
148 | const oldPeer = state[action.payload.peerId];
149 |
150 | // NOTE: This means that the Peer was closed before, so it's ok.
151 | if (!oldPeer)
152 | return state;
153 |
154 | return { ...state, [oldPeer.id]: peer(oldPeer, action) };
155 | }
156 | case 'SET_PEER_LOCAL_RECORDING_CONSENT':
157 | {
158 | const oldPeer = state[action.payload.peerId];
159 |
160 | // NOTE: This means that the Peer was closed before, so it's ok.
161 | if (!oldPeer)
162 | return state;
163 |
164 | return { ...state, [oldPeer.id]: peer(oldPeer, action) };
165 | }
166 | case 'CLEAR_PEERS':
167 | {
168 | return initialState;
169 | }
170 |
171 | default:
172 | return state;
173 | }
174 | };
175 |
176 | export default peers;
177 |
--------------------------------------------------------------------------------
/app/src/components/Containers/Volume.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import PropTypes from 'prop-types';
4 | import classnames from 'classnames';
5 | import { withStyles } from '@material-ui/core/styles';
6 |
7 | const styles = () =>
8 | ({
9 | volumeLarge :
10 | {
11 | position : 'absolute',
12 | top : 0,
13 | bottom : 0,
14 | right : 2,
15 | width : 10,
16 | display : 'flex',
17 | flexDirection : 'column',
18 | justifyContent : 'center',
19 | alignItems : 'center'
20 | },
21 | largeBar :
22 | {
23 | width : 6,
24 | borderRadius : 6,
25 | background : 'rgba(yellow, 0.65)',
26 | transitionProperty : 'height background-color',
27 | transitionDuration : '0.25s',
28 | '&.level0' :
29 | {
30 | height : 0,
31 | backgroundColor : 'rgba(255, 255, 0, 0.65)'
32 | },
33 | '&.level1' :
34 | {
35 | height : '10%',
36 | backgroundColor : 'rgba(255, 255, 0, 0.65)'
37 | },
38 | '&.level2' :
39 | {
40 | height : '20%',
41 | backgroundColor : 'rgba(255, 255, 0, 0.65)'
42 | },
43 | '&.level3' :
44 | {
45 | height : '30%',
46 | backgroundColor : 'rgba(255, 255, 0, 0.65)'
47 | },
48 | '&.level4' :
49 | {
50 | height : '40%',
51 | backgroundColor : 'rgba(255, 165, 0, 0.65)'
52 | },
53 | '&.level5' :
54 | {
55 | height : '50%',
56 | backgroundColor : 'rgba(255, 165, 0, 0.65)'
57 | },
58 | '&.level6' :
59 | {
60 | height : '60%',
61 | backgroundColor : 'rgba(255, 165, 0, 0.65)'
62 | },
63 | '&.level7' :
64 | {
65 | height : '70%',
66 | backgroundColor : 'rgba(255, 100, 0, 0.65)'
67 | },
68 | '&.level8' :
69 | {
70 | height : '80%',
71 | backgroundColor : 'rgba(255, 60, 0, 0.65)'
72 | },
73 | '&.level9' :
74 | {
75 | height : '90%',
76 | backgroundColor : 'rgba(255, 30, 0, 0.65)'
77 | },
78 | '&.level10' :
79 | {
80 | height : '100%',
81 | backgroundColor : 'rgba(255, 0, 0, 0.65)'
82 | }
83 | },
84 | volumeSmall :
85 | {
86 | float : 'right',
87 | display : 'flex',
88 | flexDirection : 'row',
89 | justifyContent : 'flex-start',
90 | width : '1vmin',
91 | position : 'relative',
92 | backgroundSize : '75%'
93 | },
94 | smallBar :
95 | {
96 | flex : '0 0 auto',
97 | backgroundSize : '75%',
98 | backgroundRepeat : 'no-repeat',
99 | backgroundColor : 'rgba(0, 0, 0, 1)',
100 | cursor : 'pointer',
101 | transitionProperty : 'opacity, background-color',
102 | width : 3,
103 | borderRadius : 2,
104 | transitionDuration : '0.25s',
105 | position : 'absolute',
106 | top : '50%',
107 | transform : 'translateY(-50%)',
108 | '&.level0' : { height: 0 },
109 | '&.level1' : { height: '0.2vh' },
110 | '&.level2' : { height: '0.4vh' },
111 | '&.level3' : { height: '0.6vh' },
112 | '&.level4' : { height: '0.8vh' },
113 | '&.level5' : { height: '1.0vh' },
114 | '&.level6' : { height: '1.2vh' },
115 | '&.level7' : { height: '1.4vh' },
116 | '&.level8' : { height: '1.6vh' },
117 | '&.level9' : { height: '1.8vh' },
118 | '&.level10' : { height: '2.0vh' }
119 | }
120 | });
121 |
122 | const Volume = (props) =>
123 | {
124 | const {
125 | small,
126 | volume,
127 | classes
128 | } = props;
129 |
130 | return (
131 |
138 | );
139 | };
140 |
141 | Volume.propTypes =
142 | {
143 | small : PropTypes.bool,
144 | volume : PropTypes.number,
145 | classes : PropTypes.object.isRequired
146 | };
147 |
148 | const makeMapStateToProps = (initialState, props) =>
149 | {
150 | const mapStateToProps = (state) =>
151 | {
152 | if (state.peerVolumes[props.id]>state.settings.noiseThreshold)
153 | {
154 | return {
155 | // scale volume (noiseThreshold...0db) -> (0...10)
156 | // this is looks only better but is not correct
157 | volume : Math.round(
158 | (state.peerVolumes[props.id] - state.settings.noiseThreshold) *
159 | -100/(state.settings.noiseThreshold) / 10)
160 | };
161 | }
162 | else
163 | {
164 | return { volume: 0 };
165 | }
166 | };
167 |
168 | return mapStateToProps;
169 | };
170 |
171 | export default connect(
172 | makeMapStateToProps
173 | )(withStyles(styles)(Volume));
174 |
--------------------------------------------------------------------------------
/app/src/components/LeaveDialog.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import { withRoomContext } from '../RoomContext';
5 | import * as roomActions from '../store/actions/roomActions';
6 | import PropTypes from 'prop-types';
7 | import { FormattedMessage } from 'react-intl';
8 | import Dialog from '@material-ui/core/Dialog';
9 | import DialogTitle from '@material-ui/core/DialogTitle';
10 | import DialogActions from '@material-ui/core/DialogActions';
11 | import DialogContent from '@material-ui/core/DialogContent';
12 | import Button from '@material-ui/core/Button';
13 | import MeetingRoomIcon from '@material-ui/icons/MeetingRoom';
14 | import CancelIcon from '@material-ui/icons/Cancel';
15 | import SaveIcon from '@material-ui/icons/Save';
16 |
17 | const styles = (theme) =>
18 | ({
19 | dialogPaper :
20 | {
21 | width : '30vw',
22 | [theme.breakpoints.down('lg')] :
23 | {
24 | width : '40vw'
25 | },
26 | [theme.breakpoints.down('md')] :
27 | {
28 | width : '50vw'
29 | },
30 | [theme.breakpoints.down('sm')] :
31 | {
32 | width : '70vw'
33 | },
34 | [theme.breakpoints.down('xs')] :
35 | {
36 | width : '90vw'
37 | }
38 | },
39 | dialogActions :
40 | {
41 | flexDirection : 'row',
42 | [theme.breakpoints.down('xs')] :
43 | {
44 | flexDirection : 'column'
45 | }
46 | },
47 |
48 | logo :
49 | {
50 | marginLeft : theme.spacing(1.5),
51 | marginRight : 'auto'
52 | },
53 | divider :
54 | {
55 | marginBottom : theme.spacing(3)
56 | }
57 | });
58 |
59 | const LeaveDialog = ({
60 | roomClient,
61 | leaveOpen,
62 | classes,
63 | handleSetLeaveOpen,
64 | chatCount
65 | }) =>
66 |
67 | {
68 | const buttonYes = useRef();
69 |
70 | const handleEnterKey = (event) =>
71 | {
72 | if (event.key === 'Enter')
73 | {
74 | buttonYes.current.click();
75 | }
76 | else
77 | if (event.key === 'Escape' || event.key === 'Esc')
78 | {
79 | handleSetLeaveOpen(false);
80 | }
81 | };
82 |
83 | const handleStay = () => handleSetLeaveOpen(false);
84 |
85 | const handleLeave = () => roomClient.close();
86 |
87 | const handleLeaveWithSavingChat = () =>
88 | {
89 | roomClient.saveChat();
90 |
91 | setTimeout(() =>
92 | {
93 | roomClient.close();
94 | }, 1000);
95 | };
96 |
97 | return (
98 |
156 | );
157 | };
158 |
159 | LeaveDialog.propTypes =
160 | {
161 | roomClient : PropTypes.object.isRequired,
162 | leaveOpen : PropTypes.bool.isRequired,
163 | handleSetLeaveOpen : PropTypes.func.isRequired,
164 | classes : PropTypes.object.isRequired,
165 | chatCount : PropTypes.number.isRequired
166 | };
167 |
168 | const mapStateToProps = (state) =>
169 | ({
170 | leaveOpen : state.room.leaveOpen,
171 | chatCount : state.chat.count
172 | });
173 |
174 | const mapDispatchToProps = {
175 | handleSetLeaveOpen : roomActions.setLeaveOpen
176 | };
177 |
178 | export default withRoomContext(connect(
179 | mapStateToProps,
180 | mapDispatchToProps,
181 | null,
182 | {
183 | areStatesEqual : (next, prev) =>
184 | {
185 | return (
186 | prev.room.leaveOpen === next.room.leaveOpen &&
187 | prev.chat.count === next.chat.count
188 | );
189 | }
190 | }
191 | )(withStyles(styles)(LeaveDialog)));
192 |
--------------------------------------------------------------------------------
/app/src/components/AccessControl/LoginDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withStyles } from '@material-ui/core/styles';
3 | import isElectron from 'is-electron';
4 | import PropTypes from 'prop-types';
5 | import { useIntl, FormattedMessage } from 'react-intl';
6 | import Dialog from '@material-ui/core/Dialog';
7 | import Typography from '@material-ui/core/Typography';
8 | import Button from '@material-ui/core/Button';
9 | import TextField from '@material-ui/core/TextField';
10 | import CookieConsent from 'react-cookie-consent';
11 | import MuiDialogTitle from '@material-ui/core/DialogTitle';
12 | import MuiDialogContent from '@material-ui/core/DialogContent';
13 | import MuiDialogActions from '@material-ui/core/DialogActions';
14 | import { config } from '../../config';
15 |
16 | const styles = (theme) =>
17 | ({
18 | root :
19 | {
20 | display : 'flex',
21 | width : '100%',
22 | height : '100%',
23 | backgroundColor : 'var(--background-color)',
24 | backgroundImage : `url(${config.background})`,
25 | backgroundAttachment : 'fixed',
26 | backgroundPosition : 'center',
27 | backgroundSize : 'cover',
28 | backgroundRepeat : 'no-repeat'
29 | },
30 | dialogTitle :
31 | {
32 | },
33 | dialogPaper :
34 | {
35 | width : '30vw',
36 | padding : theme.spacing(2),
37 | [theme.breakpoints.down('lg')] :
38 | {
39 | width : '40vw'
40 | },
41 | [theme.breakpoints.down('md')] :
42 | {
43 | width : '50vw'
44 | },
45 | [theme.breakpoints.down('sm')] :
46 | {
47 | width : '70vw'
48 | },
49 | [theme.breakpoints.down('xs')] :
50 | {
51 | width : '90vw'
52 | }
53 | },
54 | logo :
55 | {
56 | display : 'block',
57 | paddingBottom : '1vh'
58 | },
59 | loginButton :
60 | {
61 | position : 'absolute',
62 | right : theme.spacing(2),
63 | top : theme.spacing(2),
64 | padding : 0
65 | },
66 | largeIcon :
67 | {
68 | fontSize : '2em'
69 | },
70 | largeAvatar :
71 | {
72 | width : 50,
73 | height : 50
74 | },
75 | green :
76 | {
77 | color : 'rgba(0, 153, 0, 1)'
78 | }
79 | });
80 |
81 | const DialogTitle = withStyles((theme) => ({
82 | root :
83 | {
84 | margin : 0,
85 | padding : theme.spacing(1)
86 | }
87 | }))(MuiDialogTitle);
88 |
89 | const DialogContent = withStyles((theme) => ({
90 | root :
91 | {
92 | padding : theme.spacing(2)
93 | }
94 | }))(MuiDialogContent);
95 |
96 | const DialogActions = withStyles((theme) => ({
97 | root :
98 | {
99 | margin : 0,
100 | padding : theme.spacing(1)
101 | }
102 | }))(MuiDialogActions);
103 |
104 | const ChooseRoom = ({
105 | classes
106 | }) =>
107 | {
108 | const intl = useIntl();
109 |
110 | return (
111 |
112 |
187 |
188 | );
189 | };
190 |
191 | ChooseRoom.propTypes =
192 | {
193 | classes : PropTypes.object.isRequired
194 | };
195 |
196 | export default withStyles(styles)(ChooseRoom);
197 |
--------------------------------------------------------------------------------