Compare commits

...

45 Commits

Author SHA1 Message Date
paweldomas
6453ceb048 ref: remove jest and lastn functions.test.js
It doesn't play well with webpack and it's babel config
and I couldn't find a way to make it work.
2020-08-21 07:38:21 -07:00
Andrei Gavrilescu
e51bbe6125 fix syntax error 2020-08-20 17:30:59 -05:00
Andrei Gavrilescu
d725c0ab8a Use rtcstats with keep-alive / add rtcstats enabled config 2020-08-20 17:30:59 -05:00
Hristo Terezov
2c2edace2a Merge pull request #7475 from vp8x8/prejoin-focus
fix(prejoin): Auto focus display name input
2020-08-20 15:28:04 -05:00
paweldomas
d3d5847605 feat: configurable quality levels for video height
Allows to adjust thresholds which control the video quality level
in the thumbnail view.

Changes the default behaviour to request the SD (360p) resolution only
when the thumbnails are at least 360 pixels tall and the height of
720 is required for the high quality level.

The thresholds can be configured with the 'videoQuality.minHeightForQualityLvl'
config property. Check the description in the config.js for more details.
2020-08-20 11:07:36 -07:00
Hristo Terezov
89ad76142d Merge pull request #7449 from muscat1/promotional-close
feat(close3): Move readyToClose flow to the close page
2020-08-20 11:48:42 -05:00
Vlad Piersec
1e76b8b6ea misc: Add test ids for prejoin buttons 2020-08-20 11:20:49 -05:00
Hristo Terezov
55175e2e95 fix(subject): set to ' ' after settings change. 2020-08-20 10:48:06 -05:00
Vlad Piersec
453c07cb17 feat(prejoin): Add precall connection quality indicator
* Adds a dropdown indicator which displays the status of the internet connection.
* It uses the same data as `https://network.callstats.io`.
* The algorithm for the strings displayed to the user is also the one used on `network.callstas.io`.
2020-08-20 08:25:15 -07:00
Tudor-Ovidiu Avram
10c2652a4f feat(prejoin) show error when trying to join and name is required 2020-08-18 13:18:58 -05:00
Aaron van Meerten
c3329ec931 Merge pull request #7518 from jitsi/aaronkvanmeerten/jibri-queue-component-modules
FEAT: prosody jibri queue component module
2020-08-18 10:16:39 -05:00
Mihai Uscat
9cf7199c0e feat(close3): Move readyToClose flow to the close page 2020-08-18 17:31:10 +03:00
Vlad Piersec
d82bb0a89b fix(prejoin): Fix join without audio 2020-08-17 08:31:55 -05:00
Tudor-Ovidiu Avram
295dd8a45d fix(prejoin) remove version parameter 2020-08-17 10:54:22 +03:00
damencho
25ae83bcf4 fix: Fixes #7514 when promoting new moderator and lobby is enabled. 2020-08-14 17:56:24 -05:00
Aaron van Meerten
82b1408454 FEAT: jibri queue clear asap cache for token util on config reload 2020-08-14 15:24:26 -05:00
Aaron van Meerten
36565f0c50 FIX: token util keyurl definition move to above callback definition 2020-08-14 15:23:54 -05:00
Aaron van Meerten
0c48e205d7 Merge branch 'master' into aaronkvanmeerten/jibri-queue-component-modules 2020-08-14 14:21:13 -05:00
Aaron van Meerten
5e35b69fc9 FIX: prosody token util handles race on timeout gracefully 2020-08-14 14:14:29 -05:00
Aaron van Meerten
3fd85720bc FIX: prosody jibri queue component reloads configuration 2020-08-14 14:13:57 -05:00
Aaron van Meerten
e439d065b7 FEAT: token util better logging for timeouts, verification 2020-08-14 13:52:25 -05:00
Jaya Allamsetty
5dcecdbb54 deps: lib-jitsi-meet@latest 2020-08-14 12:00:09 -04:00
Niek van der Maas
8d2a52d0e8 debian: improve compressions + add expire headers
* Improve compressions + add expire headers
* Remove MSIE check, caching only for versioned files, do not gzip MP3/JPG/PNG
* Lower GZIP min length, enable compressions on WASM
2020-08-14 10:29:25 +02:00
Russell Graves
2aa6f7ff4b Add codec reporting (if present in lib-jitsi-meet output) to connection stats (#6054)
* Add codec reporting to the stats window for connections.
This will report the audio/video codecs, if reported by lib-jitsi-meet.
2020-08-13 17:56:14 -04:00
Aaron van Meerten
d716665f27 FIX: jibri-queue module log improvements 2020-08-13 16:41:42 -05:00
Дамян Минков
4ca4e242b1 ref: Moves xmpp logs to be accessed from connection. (#7517)
* ref: Moves xmpp logs to be accessed from connection.

In cases where there is no room like pre-join and lobby screen we still want to be able to debug xmpp messages.

* squash: Updates lib-jitsi-meet.
2020-08-13 13:12:56 -05:00
damencho
cdd782a82f fix: Fixes uncaught exception on malformed jwt.
Does not skip passing jwt even when malformed to allow getting the error, terminating the connection and showing the warning. We were not passing jwt when malformed and were successfully joining a conference for deployments where no token is allowed.
2020-08-13 11:00:04 -05:00
Jaya Allamsetty
713ae817c0 deps: lib-jitsi-meet@latest 2020-08-13 09:29:21 -04:00
Aaron van Meerten
d05fa32413 FIX: add flag to control whether to check room claim in JWT validation
jibri queue component stop checking room validation in token
Jibri queue component debug output when bad token is found
2020-08-12 14:43:34 -05:00
Aaron van Meerten
3da7798e9f FIX: prosody: output string for time and position in jibri queue 2020-08-10 15:21:56 -05:00
Aaron van Meerten
6fc9606c0d FEAT: support updating accepted issuer/aud for token lib 2020-08-10 15:21:31 -05:00
Aaron van Meerten
c4155575f9 FIX: prosody: room validation on jibri-queue
The full room JID is now passed properly to verify_token
verify_token now also expects the correct jid for validation
2020-08-07 12:10:00 -05:00
Vlad Piersec
b670b29d7f fix(prejoin): Auto focus display name input 2020-08-07 10:27:29 +03:00
Aaron van Meerten
9b7e8c98ad FEAT: default value for jibri queue region 2020-08-06 17:12:53 -05:00
Aaron van Meerten
ad44558153 FEAT: validate keys at specific URL for jibri queue
Provide region value in POST to jibri-queue service
2020-08-06 17:12:31 -05:00
Aaron van Meerten
d70f9d6fd6 FIX: use correct URL paths for jibri queue service 2020-07-22 16:24:08 -04:00
Aaron van Meerten
7858f12df2 FEATURE: proper outbound iq handler for REST requests 2020-07-20 12:51:07 -04:00
Aaron van Meerten
828e578af4 FIX: rename disco info component to correct name
FIX: reply to iq only on successful reply from queue server
2020-07-17 16:19:25 -04:00
Aaron van Meerten
4289b23135 feature: jibri queue authorization header handler 2020-07-16 22:48:52 -04:00
Aaron van Meerten
099820b6ac prosody modules: jibri queue events for leave, room destroyed 2020-07-14 16:50:34 -04:00
Aaron van Meerten
25ded0bdeb prosody modules: add util function for rewritesplit JID 2020-07-14 16:49:51 -04:00
Aaron van Meerten
51fd10278b FIX: prosody jibri queue handle iq properly 2020-07-13 18:04:48 -04:00
Aaron Van Meerten
24c75b7332 FIX: better URL handler for jibri queue events 2020-06-29 18:46:15 -05:00
Aaron Van Meerten
2327a6d0b4 FEATURE: prosody: add http handler for jibri queue 2020-06-29 18:20:04 -05:00
Aaron Van Meerten
b94c357cc2 WIP: jibri queue component prosody modules 2020-06-29 18:11:41 -05:00
58 changed files with 1477 additions and 7530 deletions

1
.gitignore vendored
View File

@@ -84,3 +84,4 @@ android/app/google-services.json
ios/app/dropbox.key
ios/app/GoogleService-Info.plist
.vscode

View File

@@ -121,7 +121,8 @@ import { suspendDetected } from './react/features/power-monitor';
import {
initPrejoin,
isPrejoinPageEnabled,
isPrejoinPageVisible
isPrejoinPageVisible,
makePrecallTest
} from './react/features/prejoin';
import { createRnnoiseProcessorPromise } from './react/features/rnnoise';
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
@@ -759,7 +760,15 @@ export default {
}
if (isPrejoinPageEnabled(APP.store.getState())) {
_connectionPromise = connect(roomName);
_connectionPromise = connect(roomName).then(c => {
// we want to initialize it early, in case of errors to be able
// to gather logs
APP.connection = c;
return c;
});
APP.store.dispatch(makePrecallTest(this._getConferenceOptions()));
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
const tracks = await tryCreateLocalTracks;
@@ -1206,10 +1215,6 @@ export default {
// end used by torture
getLogs() {
return room.getLogs();
},
/**
* Download logs, a function that can be called from console while
* debugging.
@@ -1218,7 +1223,7 @@ export default {
saveLogs(filename = 'meetlog.json') {
// this can be called from console and will not have reference to this
// that's why we reference the global var
const logs = APP.conference.getLogs();
const logs = APP.connection.getLogs();
const data = encodeURIComponent(JSON.stringify(logs, null, ' '));
const elem = document.createElement('a');
@@ -2856,7 +2861,14 @@ export default {
this._room = undefined;
room = undefined;
APP.API.notifyReadyToClose();
/**
* Don't call {@code notifyReadyToClose} if the promotional page flag is set
* and let the page take care of sending the message, since there will be
* a redirect to the page regardlessly.
*/
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
APP.API.notifyReadyToClose();
}
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
});
},

View File

@@ -244,6 +244,21 @@ var config = {
// low: 200000,
// standard: 500000,
// high: 1500000
// },
//
// // The options can be used to override default thresholds of video thumbnail heights corresponding to
// // the video quality levels used in the application. At the time of this writing the allowed levels are:
// // 'low' - for the low quality level (180p at the time of this writing)
// // 'standard' - for the medium quality level (360p)
// // 'high' - for the high quality level (720p)
// // The keys should be positive numbers which represent the minimal thumbnail height for the quality level.
// //
// // With the default config value below the application will use 'low' quality until the thumbnails are
// // at least 360 pixels tall. If the thumbnail height reaches 720 pixels then the application will switch to
// // the high quality.
// minHeightForQualityLvl: {
// 360: 'standard,
// 720: 'high'
// }
// },
@@ -444,6 +459,12 @@ var config = {
// amplitudeAPPKey: '<APP_KEY>'
// Configuration for the rtcstats server:
// By enabling rtcstats server every time a conference is joined the rtcstats
// module connects to the provided rtcstatsEndpoint and sends statistics regarding
// PeerConnection states along with getStats metrics polled at the specified
// interval.
// rtcstatsEnabled: true,
// In order to enable rtcstats one needs to provide a endpoint url.
// rtcstatsEndpoint: wss://rtcstats-server-pilot.jitsi.net/,

View File

@@ -82,7 +82,7 @@ function checkForAttachParametersAndConnect(id, password, connection) {
*/
function connect(id, password, roomName) {
const connectionConfig = Object.assign({}, config);
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
const { jwt } = APP.store.getState()['features/base/jwt'];
// Use Websocket URL for the web app if configured. Note that there is no 'isWeb' check, because there's assumption
// that this code executes only on web browsers/electron. This needs to be changed when mobile and web are unified.
@@ -94,11 +94,7 @@ function connect(id, password, roomName) {
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
const connection
= new JitsiMeetJS.JitsiConnection(
null,
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
connectionConfig);
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, connectionConfig);
if (config.iAmRecorder) {
connection.addFeature(DISCO_JIBRI_FEATURE);
@@ -211,10 +207,9 @@ export function openConnection({ id, password, retry, roomName }) {
return connect(id, password, roomName).catch(err => {
if (retry) {
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
const { jwt } = APP.store.getState()['features/base/jwt'];
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED
&& (!jwt || issuer === 'anonymous')) {
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED && !jwt) {
return AuthHandler.requestAuth(roomName, connect);
}
}

View File

@@ -0,0 +1,60 @@
.con-status {
position: absolute;
top: 40px;
width: 100%;
z-index: $toolbarZ + 3;
&-container {
background: rgba(28, 32, 37, .5);
border-radius: 3px;
color: #fff;
font-size: 13px;
line-height: 20px;
margin: 0 auto;
width: 304px;
}
&-header {
align-items: center;
display: flex;
justify-content: space-between;
padding: 8px;
}
&-circle {
border-radius: 50%;
display: inline-block;
padding: 4px;
}
&--good {
background: #31B76A;
}
&--poor {
background: #E12D2D;
}
&--non-optimal {
background: #E39623;
}
&-arrow {
&--up {
transform: rotate(180deg);
}
&>svg {
cursor: pointer;
}
}
&-text {
text-align: center;
}
&-details {
border-top: 1px solid #5E6D7A;
padding: 16px;
}
}

View File

@@ -39,6 +39,16 @@
margin-bottom: 14px;
width: 100%;
}
&-error {
color: white;
background-color: rgba(229, 75, 75, 0.5);
width: 100%;
padding: 3px;
margin-top: 4px;
font-size: 13px;
text-align: center;
}
}
@mixin name-placeholder {

View File

@@ -102,5 +102,6 @@ $flagsImagePath: "../images/";
@import 'premeeting-screens';
@import 'e2ee';
@import 'responsive';
@import 'connection-status';
/* Modules END */

View File

@@ -45,8 +45,10 @@ server {
error_page 404 /static/404.html;
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
gzip_vary on;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 512;
location = /config.js {
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
@@ -61,6 +63,11 @@ server {
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;
# cache all versioned files
if ($arg_v) {
expires 1y;
}
}
# BOSH

View File

@@ -14,6 +14,12 @@ server {
ssi on;
}
gzip on;
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
gzip_vary on;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 512;
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;

View File

@@ -28,6 +28,12 @@ server {
tcp_nodelay on;
}
gzip on;
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
gzip_vary on;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 512;
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}

View File

@@ -1,9 +0,0 @@
module.exports = {
moduleFileExtensions: [
'js'
],
testMatch: [
'<rootDir>/react/**/?(*.)+(test)?(.web).js?(x)'
],
verbose: true
};

View File

@@ -102,6 +102,7 @@
"bandwidth": "Estimated bandwidth:",
"bitrate": "Bitrate:",
"bridgeCount": "Server count: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Connected to:",
"e2e_rtt": "E2E RTT:",
"framerate": "Frame rate:",
@@ -510,6 +511,25 @@
"callMeAtNumber": "Call me at this number:",
"configuringDevices": "Configuring devices...",
"connectedWithAudioQ": "Youre connected with audio?",
"connection": {
"good": "Your internet connection looks good!",
"nonOptimal": "Your internet connection is not optimal",
"poor": "You have a poor internet connection"
},
"connectionDetails": {
"audioClipping": "We expect your audio to be clipped.",
"audioHighQuality": "We expect your audio to have excellent quality.",
"audioLowNoVideo": "We expect your audio quality to be low and no video.",
"goodQuality": "Awesome! Your media quality is going to be great.",
"noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
"noVideo": "We expect that your video will be terrible.",
"undetectable": "If you still can not make calls in browser, we recommend that you make sure your speakers, microphone and camera are properly set up, that you have granted your browser rights to use your microphone and camera, and that your browser version is up-to-date. If you still have trouble calling, you should contact the web application developer.",
"veryPoorConnection": "We expect your call quality to be really terrible.",
"videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",
"videoHighQuality": "We expect your video to have good quality.",
"videoLowQuality": "We expect your video to have low quality in terms of frame rate and resolution.",
"videoTearing": "We expect your video to be pixelated or have visual artefacts."
},
"copyAndShare": "Copy & share meeting link",
"dialInMeeting": "Dial into the meeting",
"dialInPin": "Dial into the meeting and enter PIN code:",
@@ -519,6 +539,7 @@
"errorDialOutDisconnected": "Could not dial out. Disconnected",
"errorDialOutFailed": "Could not dial out. Call failed",
"errorDialOutStatus": "Error getting dial out status",
"errorMissingName": "Please enter your name to join the meeting",
"errorStatusCode": "Error dialing out, status code: {{status}}",
"errorValidation": "Number validation failed",
"iWantToDialIn": "I want to dial in",

7290
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f74cd0abe9c696a9c3ca7dbb9ca170e6e84d6756",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#15dcc57424cc937290e1963b8eb402c1fcf48ccb",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -90,7 +90,7 @@
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v6.1.3",
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
"styled-components": "3.4.9",
"util": "0.12.1",
"uuid": "3.1.0",
@@ -125,7 +125,6 @@
"expose-loader": "0.7.5",
"flow-bin": "0.104.0",
"imports-loader": "0.7.1",
"jest": "26.1.0",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.14.1",
@@ -145,7 +144,6 @@
"scripts": {
"lint": "eslint . && flow",
"postinstall": "jetify",
"test": "jest",
"validate": "npm ls"
},
"browser": {

View File

@@ -2,6 +2,7 @@
import type { Dispatch } from 'redux';
import { API_ID } from '../../../modules/API/constants';
import { setRoom } from '../base/conference';
import {
configWillLoad,
@@ -168,9 +169,11 @@ export function redirectWithStoredParams(pathname: string) {
* window.location.pathname. If the specified pathname is relative, the context
* root of the Web app will be prepended to the specified pathname before
* assigning it to window.location.pathname.
* @param {string} hashParam - Optional hash param to assign to
* window.location.hash.
* @returns {Function}
*/
export function redirectToStaticPage(pathname: string) {
export function redirectToStaticPage(pathname: string, hashParam: ?string) {
return () => {
const windowLocation = window.location;
let newPathname = pathname;
@@ -184,6 +187,10 @@ export function redirectToStaticPage(pathname: string) {
newPathname = getLocationContextRoot(windowLocation) + newPathname;
}
if (hashParam) {
windowLocation.hash = hashParam;
}
windowLocation.pathname = newPathname;
};
}
@@ -285,6 +292,7 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
// if close page is enabled redirect to it, without further action
if (enableClosePage) {
const { isGuest, jwt } = getState()['features/base/jwt'];
let hashParam;
// save whether current user is guest or not, and pass auth token,
// before navigating to close page
@@ -294,12 +302,15 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
let path = 'close.html';
if (interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
if (Number(API_ID) === API_ID) {
hashParam = `#jitsi_meet_external_api_id=${API_ID}`;
}
path = 'close3.html';
} else if (!options.feedbackSubmitted) {
path = 'close2.html';
}
dispatch(redirectToStaticPage(`static/${path}`));
dispatch(redirectToStaticPage(`static/${path}`, hashParam));
return;
}

View File

@@ -17,8 +17,7 @@ import {
AVATAR_ID_COMMAND,
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
JITSI_CONFERENCE_URL_KEY,
VIDEO_QUALITY_LEVELS
JITSI_CONFERENCE_URL_KEY
} from './constants';
import logger from './logger';
@@ -214,38 +213,6 @@ export function getCurrentConference(stateful: Function | Object) {
return joining || passwordRequired || membersOnly;
}
/**
* Finds the nearest match for the passed in {@link availableHeight} to am
* enumerated value in {@code VIDEO_QUALITY_LEVELS}.
*
* @param {number} availableHeight - The height to which a matching video
* quality level should be found.
* @returns {number} The closest matching value from
* {@code VIDEO_QUALITY_LEVELS}.
*/
export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
const qualityLevels = [
VIDEO_QUALITY_LEVELS.HIGH,
VIDEO_QUALITY_LEVELS.STANDARD,
VIDEO_QUALITY_LEVELS.LOW
];
let selectedLevel = qualityLevels[0];
for (let i = 1; i < qualityLevels.length; i++) {
const previousValue = qualityLevels[i - 1];
const currentValue = qualityLevels[i];
const diffWithCurrent = Math.abs(availableHeight - currentValue);
const diffWithPrevious = Math.abs(availableHeight - previousValue);
if (diffWithCurrent < diffWithPrevious) {
selectedLevel = currentValue;
}
}
return selectedLevel;
}
/**
* Returns the stored room name.
*

View File

@@ -624,7 +624,7 @@ function _updateLocalParticipantInConference({ dispatch, getState }, next, actio
// When the local user role is updated to moderator and we have a pending subject change
// which was not reflected we need to set it (the first time we tried was before becoming moderator).
if (pendingSubjectChange !== subject) {
if (typeof pendingSubjectChange !== 'undefined' && pendingSubjectChange !== subject) {
dispatch(setSubject(pendingSubjectChange));
}
}

View File

@@ -43,8 +43,8 @@ export const SET_CONFIG = 'SET_CONFIG';
* and the passed object.
*
* {
* type: _UPDATE_CONFIG,
* type: UPDATE_CONFIG,
* config: Object
* }
*/
export const _UPDATE_CONFIG = '_UPDATE_CONFIG';
export const UPDATE_CONFIG = 'UPDATE_CONFIG';

View File

@@ -6,10 +6,24 @@ import type { Dispatch } from 'redux';
import { addKnownDomains } from '../known-domains';
import { parseURIString } from '../util';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG, UPDATE_CONFIG } from './actionTypes';
import { _CONFIG_STORE_PREFIX } from './constants';
import { setConfigFromURLParams } from './functions';
/**
* Updates the config with new options.
*
* @param {Object} config - The new options (to add).
* @returns {Function}
*/
export function updateConfig(config: Object) {
return {
type: UPDATE_CONFIG,
config
};
}
/**
* Signals that the configuration (commonly known in Jitsi Meet as config.js)
* for a specific locationURL will be loaded now.

View File

@@ -8,7 +8,8 @@ import { addKnownDomains } from '../known-domains';
import { MiddlewareRegistry } from '../redux';
import { parseURIString } from '../util';
import { _UPDATE_CONFIG, SET_CONFIG } from './actionTypes';
import { SET_CONFIG } from './actionTypes';
import { updateConfig } from './actions';
import { _CONFIG_STORE_PREFIX } from './constants';
/**
@@ -114,10 +115,7 @@ function _setConfig({ dispatch, getState }, next, action) {
config.resolution = resolutionFlag;
}
dispatch({
type: _UPDATE_CONFIG,
config
});
dispatch(updateConfig(config));
// FIXME On Web we rely on the global 'config' variable which gets altered
// multiple times, before it makes it to the reducer. At some point it may

View File

@@ -4,7 +4,7 @@ import _ from 'lodash';
import { equals, ReducerRegistry, set } from '../redux';
import { _UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { _cleanupConfig } from './functions';
/**
@@ -50,7 +50,7 @@ const INITIAL_RN_STATE = {
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
switch (action.type) {
case _UPDATE_CONFIG:
case UPDATE_CONFIG:
return _updateConfig(state, action);
case CONFIG_WILL_LOAD:

View File

@@ -80,12 +80,8 @@ export function connect(id: ?string, password: ?string) {
const state = getState();
const options = _constructOptions(state);
const { locationURL } = state['features/base/connection'];
const { issuer, jwt } = state['features/base/jwt'];
const connection
= new JitsiMeetJS.JitsiConnection(
options.appId,
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
options);
const { jwt } = state['features/base/jwt'];
const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
connection[JITSI_CONNECTION_URL_KEY] = locationURL;

View File

@@ -98,4 +98,7 @@ export { default as IconVolume } from './volume.svg';
export { default as IconVolumeEmpty } from './volume-empty.svg';
export { default as IconVolumeOff } from './volume-off.svg';
export { default as IconWarning } from './warning.svg';
export { default as IconWifi1Bar } from './wifi-1.svg';
export { default as IconWifi2Bars } from './wifi-2.svg';
export { default as IconWifi3Bars } from './wifi-3.svg';
export { default as IconYahoo } from './yahoo.svg';

View File

@@ -0,0 +1,5 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.4" d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94212C9.88182 4.55812 8.94553 4.36048 7.99997 4.36048C7.05442 4.36048 6.11813 4.55812 5.24456 4.94212C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
<path opacity="0.4" d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53041 9.46623 9.30297 9.11328 9.1478C8.76032 8.99263 8.38201 8.91276 7.99996 8.91276C7.6179 8.91276 7.23959 8.99263 6.88663 9.1478C6.53368 9.30297 6.21298 9.53041 5.94287 9.81713Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,5 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94211C9.88182 4.55811 8.94553 4.36047 7.99997 4.36047C7.05442 4.36047 6.11813 4.55811 5.24456 4.94211C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
<path opacity="0.4" d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53042 9.46623 9.30298 9.11328 9.14781C8.76032 8.99263 8.38201 8.91277 7.99996 8.91277C7.6179 8.91277 7.23959 8.99263 6.88663 9.14781C6.53368 9.30298 6.21298 9.53042 5.94287 9.81713Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,5 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0913 6.59847C12.4227 5.88894 11.629 5.32611 10.7554 4.94211C9.88182 4.55811 8.94553 4.36047 7.99997 4.36047C7.05442 4.36047 6.11813 4.55811 5.24456 4.94211C4.371 5.32611 3.57726 5.88894 2.90869 6.59847L4.36305 8.14176C4.84061 7.63486 5.4076 7.23276 6.03163 6.95842C6.65566 6.68408 7.32451 6.54288 7.99997 6.54288C8.67544 6.54288 9.34429 6.68408 9.96832 6.95842C10.5923 7.23276 11.1593 7.63486 11.6369 8.14176L13.0913 6.59847Z" fill="white"/>
<path d="M16 3.51081C13.8766 1.26261 10.9996 0 8 0C5.00044 0 2.12337 1.26261 0 3.51081L1.45436 5.0541C3.19156 3.21432 5.54565 2.18105 8 2.18105C10.4543 2.18105 12.8084 3.21432 14.5456 5.0541L16 3.51081Z" fill="white"/>
<path d="M5.94287 9.81713L7.99996 12L10.057 9.81713C9.78693 9.53042 9.46623 9.30298 9.11328 9.14781C8.76032 8.99263 8.38201 8.91277 7.99996 8.91277C7.6179 8.91277 7.23959 8.99263 6.88663 9.14781C6.53368 9.30298 6.21298 9.53042 5.94287 9.81713Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/jwt');

View File

@@ -13,6 +13,7 @@ import { MiddlewareRegistry } from '../redux';
import { SET_JWT } from './actionTypes';
import { setJWT } from './actions';
import { parseJWTFromURLParams } from './functions';
import logger from './logger';
declare var APP: Object;
@@ -133,7 +134,13 @@ function _setJWT(store, next, action) {
action.isGuest = !enableUserRolesBasedOnToken;
const jwtPayload = jwtDecode(jwt);
let jwtPayload;
try {
jwtPayload = jwtDecode(jwt);
} catch (e) {
logger.error(e);
}
if (jwtPayload) {
const { context, iss } = jwtPayload;

View File

@@ -1,103 +0,0 @@
import { limitLastN, validateLastNLimits } from './functions';
describe('limitLastN', () => {
it('handles undefined mapping', () => {
expect(limitLastN(0, undefined)).toBe(undefined);
});
describe('when a correct limit mapping is given', () => {
const limits = new Map();
limits.set(5, -1);
limits.set(10, 8);
limits.set(20, 5);
it('returns undefined when less participants that the first limit', () => {
expect(limitLastN(2, limits)).toBe(undefined);
});
it('picks the first limit correctly', () => {
expect(limitLastN(5, limits)).toBe(-1);
expect(limitLastN(9, limits)).toBe(-1);
});
it('picks the middle limit correctly', () => {
expect(limitLastN(10, limits)).toBe(8);
expect(limitLastN(13, limits)).toBe(8);
expect(limitLastN(19, limits)).toBe(8);
});
it('picks the top limit correctly', () => {
expect(limitLastN(20, limits)).toBe(5);
expect(limitLastN(23, limits)).toBe(5);
expect(limitLastN(100, limits)).toBe(5);
});
});
});
describe('validateLastNLimits', () => {
describe('validates the input by returning undefined', () => {
it('if lastNLimits param is not an Object', () => {
expect(validateLastNLimits(5)).toBe(undefined);
});
it('if any key is not a number', () => {
const limits = {
'abc': 8,
5: -1,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if any value is not a number', () => {
const limits = {
8: 'something',
5: -1,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if any value is null', () => {
const limits = {
1: 1,
5: null,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if any value is undefined', () => {
const limits = {
1: 1,
5: undefined,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if the map is empty', () => {
expect(validateLastNLimits({})).toBe(undefined);
});
});
it('sorts by the keys', () => {
const mappingKeys = validateLastNLimits({
10: 5,
3: 3,
5: 4
}).keys();
expect(mappingKeys.next().value).toBe(3);
expect(mappingKeys.next().value).toBe(5);
expect(mappingKeys.next().value).toBe(10);
expect(mappingKeys.next().done).toBe(true);
});
it('converts keys and values to numbers', () => {
const mapping = validateLastNLimits({
3: 3,
5: 4,
10: 5
});
for (const key of mapping.keys()) {
expect(typeof key).toBe('number');
expect(typeof mapping.get(key)).toBe('number');
}
});
});

View File

@@ -70,6 +70,7 @@ function ActionButton({
{children}
{hasOptions && <div
className = 'options'
data-testid = 'prejoin.joinOptions'
onClick = { disabled ? undefined : onOptionsClick }>
<Icon
className = 'icon'

View File

@@ -0,0 +1,104 @@
// @flow
import React, { useState } from 'react';
import { translate } from '../../../i18n';
import { Icon, IconArrowDownSmall, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons';
import { connect } from '../../../redux';
import { CONNECTION_TYPE } from '../../constants';
import { getConnectionData } from '../../functions';
type Props = {
/**
* List of strings with details about the connection.
*/
connectionDetails: string[],
/**
* The type of the connection. Can be: 'none', 'poor', 'nonOptimal' or 'good'.
*/
connectionType: string,
/**
* Used for translation.
*/
t: Function
}
const CONNECTION_TYPE_MAP = {
[CONNECTION_TYPE.POOR]: {
connectionClass: 'con-status--poor',
icon: IconWifi1Bar,
connectionText: 'prejoin.connection.poor'
},
[CONNECTION_TYPE.NON_OPTIMAL]: {
connectionClass: 'con-status--non-optimal',
icon: IconWifi2Bars,
connectionText: 'prejoin.connection.nonOptimal'
},
[CONNECTION_TYPE.GOOD]: {
connectionClass: 'con-status--good',
icon: IconWifi3Bars,
connectionText: 'prejoin.connection.good'
}
};
/**
* Component displaying information related to the connection & audio/video quality.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
if (connectionType === CONNECTION_TYPE.NONE) {
return null;
}
const { connectionClass, icon, connectionText } = CONNECTION_TYPE_MAP[connectionType];
const [ showDetails, toggleDetails ] = useState(false);
const arrowClassName = showDetails
? 'con-status-arrow con-status-arrow--up'
: 'con-status-arrow';
const detailsText = connectionDetails.map(t).join(' ');
return (
<div className = 'con-status'>
<div className = 'con-status-container'>
<div className = 'con-status-header'>
<div className = { `con-status-circle ${connectionClass}` }>
<Icon
size = { 16 }
src = { icon } />
</div>
<span className = 'con-status-text'>{t(connectionText)}</span>
<Icon
className = { arrowClassName }
// eslint-disable-next-line react/jsx-no-bind
onClick = { () => toggleDetails(!showDetails) }
size = { 24 }
src = { IconArrowDownSmall } />
</div>
{ showDetails
&& <div className = 'con-status-details'>{detailsText}</div> }
</div>
</div>
);
}
/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
function mapStateToProps(state): Object {
const { connectionDetails, connectionType } = getConnectionData(state);
return {
connectionDetails,
connectionType
};
}
export default translate(connect(mapStateToProps)(ConnectionStatus));

View File

@@ -6,6 +6,11 @@ import { getFieldValue } from '../../../react';
type Props = {
/**
* If the input should be focused on display.
*/
autoFocus?: boolean,
/**
* Class name to be appended to the default class list.
*/
@@ -109,6 +114,7 @@ export default class InputField extends PureComponent<Props, State> {
render() {
return (
<input
autoFocus = { this.props.autoFocus }
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
data-testid = { this.props.testId ? this.props.testId : undefined }
onBlur = { this._onBlur }

View File

@@ -4,6 +4,7 @@ import React, { PureComponent } from 'react';
import { AudioSettingsButton, VideoSettingsButton } from '../../../../toolbox/components/web';
import ConnectionStatus from './ConnectionStatus';
import CopyMeetingUrl from './CopyMeetingUrl';
import Preview from './Preview';
@@ -82,6 +83,7 @@ export default class PreMeetingScreen extends PureComponent<Props> {
<div
className = 'premeeting-screen'
id = 'lobby-screen'>
<ConnectionStatus />
<Preview
name = { name }
showAvatar = { showAvatar }

View File

@@ -0,0 +1,8 @@
// @flow
export const CONNECTION_TYPE = {
GOOD: 'good',
NON_OPTIMAL: 'nonOptimal',
NONE: 'none',
POOR: 'poor'
};

View File

@@ -0,0 +1,142 @@
import { findIndex } from 'lodash';
import { CONNECTION_TYPE } from './constants';
const LOSS_AUDIO_THRESHOLDS = [ 0.33, 0.05 ];
const LOSS_VIDEO_THRESHOLDS = [ 0.33, 0.1, 0.05 ];
const THROUGHPUT_AUDIO_THRESHOLDS = [ 8, 20 ];
const THROUGHPUT_VIDEO_THRESHOLDS = [ 60, 750 ];
/**
* Returns the level based on a list of thresholds.
*
* @param {number[]} thresholds - The thresholds array.
* @param {number} value - The value against which the level is calculated.
* @param {boolean} descending - The order based on which the level is calculated.
*
* @returns {number}
*/
function _getLevel(thresholds, value, descending = true) {
let predicate;
if (descending) {
predicate = function(threshold) {
return value > threshold;
};
} else {
predicate = function(threshold) {
return value < threshold;
};
}
const i = findIndex(thresholds, predicate);
if (i === -1) {
return thresholds.length;
}
return i;
}
/**
* Returns the connection details from the test results.
*
* @param {{
* fractionalLoss: number,
* throughput: number
* }} testResults - The state of the app.
*
* @returns {{
* connectionType: string,
* connectionDetails: string[]
* }}
*/
function _getConnectionDataFromTestResults({ fractionalLoss: l, throughput: t }) {
const loss = {
audioQuality: _getLevel(LOSS_AUDIO_THRESHOLDS, l),
videoQuality: _getLevel(LOSS_VIDEO_THRESHOLDS, l)
};
const throughput = {
audioQuality: _getLevel(THROUGHPUT_AUDIO_THRESHOLDS, t, false),
videoQuality: _getLevel(THROUGHPUT_VIDEO_THRESHOLDS, t, false)
};
let connectionType = CONNECTION_TYPE.NONE;
const connectionDetails = [];
if (throughput.audioQuality === 0 || loss.audioQuality === 0) {
// Calls are impossible.
connectionType = CONNECTION_TYPE.POOR;
connectionDetails.push('prejoin.connectionDetails.veryPoorConnection');
} else if (
throughput.audioQuality === 2
&& throughput.videoQuality === 2
&& loss.audioQuality === 2
&& loss.videoQuality === 3
) {
// Ideal conditions for both audio and video. Show only one message.
connectionType = CONNECTION_TYPE.GOOD;
connectionDetails.push('prejoin.connectionDetails.goodQuality');
} else {
connectionType = CONNECTION_TYPE.NON_OPTIMAL;
if (throughput.audioQuality === 1) {
// Minimum requirements for a call are met.
connectionDetails.push('prejoin.connectionDetails.audioLowNoVideo');
} else {
// There are two paragraphs: one saying something about audio and the other about video.
if (loss.audioQuality === 1) {
connectionDetails.push('prejoin.connectionDetails.audioClipping');
} else {
connectionDetails.push('prejoin.connectionDetails.audioHighQuality');
}
if (throughput.videoQuality === 0 || loss.videoQuality === 0) {
connectionDetails.push('prejoin.connectionDetails.noVideo');
} else if (throughput.videoQuality === 1) {
connectionDetails.push('prejoin.connectionDetails.videoLowQuality');
} else if (loss.videoQuality === 1) {
connectionDetails.push('prejoin.connectionDetails.videoFreezing');
} else if (loss.videoQuality === 2) {
connectionDetails.push('prejoin.connectionDetails.videoTearing');
} else {
connectionDetails.push('prejoin.connectionDetails.videoHighQuality');
}
}
connectionDetails.push('prejoin.connectionDetails.undetectable');
}
return {
connectionType,
connectionDetails
};
}
/**
* Selector for determining the connection type & details.
*
* @param {Object} state - The state of the app.
* @returns {{
* connectionType: string,
* connectionDetails: string[]
* }}
*/
export function getConnectionData(state) {
const { precallTestResults } = state['features/prejoin'];
if (precallTestResults) {
if (precallTestResults.mediaConnectivity) {
return _getConnectionDataFromTestResults(precallTestResults);
}
return {
connectionType: CONNECTION_TYPE.POOR,
connectionDetails: [ 'prejoin.connectionDetails.noMediaConnectivity' ]
};
}
return {
connectionType: CONNECTION_TYPE.NONE,
connectionDetails: []
};
}

View File

@@ -340,6 +340,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
bandwidth,
bitrate,
bridgeCount,
codec,
e2eRtt,
framerate,
maxEnabledResolution,
@@ -355,6 +356,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
bandwidth = { bandwidth }
bitrate = { bitrate }
bridgeCount = { bridgeCount }
codec = { codec }
connectionSummary = { this._getConnectionStatusTip() }
e2eRtt = { e2eRtt }
framerate = { framerate }

View File

@@ -122,6 +122,7 @@ const statsEmitter = {
_onStatsUpdated(localUserId: string, stats: Object) {
const allUserFramerates = stats.framerate || {};
const allUserResolutions = stats.resolution || {};
const allUserCodecs = stats.codec || {};
// FIXME resolution and framerate are maps keyed off of user ids with
// stat values. Receivers of stats expect resolution and framerate to
@@ -129,7 +130,8 @@ const statsEmitter = {
// stats objects.
const modifiedLocalStats = Object.assign({}, stats, {
framerate: allUserFramerates[localUserId],
resolution: allUserResolutions[localUserId]
resolution: allUserResolutions[localUserId],
codec: allUserCodecs[localUserId]
});
this._emitStatsUpdate(localUserId, modifiedLocalStats);
@@ -138,8 +140,9 @@ const statsEmitter = {
// and update remote user stats as needed.
const framerateUserIds = Object.keys(allUserFramerates);
const resolutionUserIds = Object.keys(allUserResolutions);
const codecUserIds = Object.keys(allUserCodecs);
_.union(framerateUserIds, resolutionUserIds)
_.union(framerateUserIds, resolutionUserIds, codecUserIds)
.filter(id => id !== localUserId)
.forEach(id => {
const remoteUserStats = {};
@@ -156,6 +159,12 @@ const statsEmitter = {
remoteUserStats.resolution = resolution;
}
const codec = allUserCodecs[id];
if (codec) {
remoteUserStats.codec = codec;
}
this._emitStatsUpdate(id, remoteUserStats);
});
}

View File

@@ -34,6 +34,11 @@ type Props = {
*/
bridgeCount: number,
/**
* Audio/video codecs in use for the connection.
*/
codec: Object,
/**
* A message describing the connection quality.
*/
@@ -219,6 +224,45 @@ class ConnectionStatsTable extends Component<Props> {
);
}
/**
* Creates a a table row as a ReactElement for displaying codec, if present.
* This will typically be something like "Codecs (A/V): opus, vp8".
*
* @private
* @returns {ReactElement}
*/
_renderCodecs() {
const { codec, t } = this.props;
if (!codec) {
return;
}
let codecString;
// Only report one codec, in case there are multiple for a user.
Object.keys(codec || {})
.forEach(ssrc => {
const { audio, video } = codec[ssrc];
codecString = `${audio}, ${video}`;
});
if (!codecString) {
codecString = 'N/A';
}
return (
<tr>
<td>
<span>{ t('connectionindicator.codecs') }</span>
</td>
<td>{ codecString }</td>
</tr>
);
}
/**
* Creates a table row as a ReactElement for displaying a summary message
* about the current connection status.
@@ -452,6 +496,7 @@ class ConnectionStatsTable extends Component<Props> {
{ isRemoteVideo ? this._renderRegion() : null }
{ this._renderResolution() }
{ this._renderFrameRate() }
{ this._renderCodecs() }
{ isRemoteVideo ? null : this._renderBridgeCount() }
</tbody>
</table>

View File

@@ -39,6 +39,11 @@ export const SET_DIALOUT_STATUS = 'SET_DIALOUT_STATUS';
*/
export const SET_JOIN_BY_PHONE_DIALOG_VISIBLITY = 'SET_JOIN_BY_PHONE_DIALOG_VISIBLITY';
/**
* Action type to set the precall test data.
*/
export const SET_PRECALL_TEST_RESULTS = 'SET_PRECALL_TEST_RESULTS';
/**
* Action type to disable the audio while on prejoin page.
*/

View File

@@ -1,5 +1,7 @@
// @flow
declare var JitsiMeetJS: Object;
import uuid from 'uuid';
import { getRoomName } from '../base/conference';
@@ -24,6 +26,7 @@ import {
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
SET_SKIP_PREJOIN,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PRECALL_TEST_RESULTS,
SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_PAGE_VISIBILITY
} from './actionTypes';
@@ -201,11 +204,13 @@ export function initPrejoin(tracks: Object[], errors: Object) {
/**
* Action used to start the conference.
*
* @param {Object} options - The config options that override the default ones (if any).
* @returns {Function}
*/
export function joinConference() {
export function joinConference(options?: Object) {
return {
type: PREJOIN_START_CONFERENCE
type: PREJOIN_START_CONFERENCE,
options
};
}
@@ -222,7 +227,26 @@ export function joinConferenceWithoutAudio() {
if (audioTrack) {
await dispatch(replaceLocalTrack(audioTrack, null));
}
dispatch(joinConference());
dispatch(joinConference({
startSilent: true
}));
};
}
/**
* Initializes the 'precallTest' and executes one test, storing the results.
*
* @param {Object} conferenceOptions - The conference options.
* @returns {Function}
*/
export function makePrecallTest(conferenceOptions: Object) {
return async function(dispatch: Function) {
await JitsiMeetJS.precallTest.init(conferenceOptions);
const results = await JitsiMeetJS.precallTest.execute();
dispatch(setPrecallTestResults(results));
};
}
@@ -392,6 +416,19 @@ export function setJoinByPhoneDialogVisiblity(value: boolean) {
};
}
/**
* Action used to set data from precall test.
*
* @param {Object} value - The precall test results.
* @returns {Object}
*/
export function setPrecallTestResults(value: Object) {
return {
type: SET_PRECALL_TEST_RESULTS,
value
};
}
/**
* Action used to set the initial errors after creating the tracks.
*

View File

@@ -48,11 +48,6 @@ type Props = {
*/
hasJoinByPhoneButton: boolean,
/**
* If join button is disabled or not.
*/
joinButtonDisabled: boolean,
/**
* Joins the current meeting.
*/
@@ -98,6 +93,11 @@ type Props = {
*/
showCameraPreview: boolean,
/**
* If should show an error when joining without a name.
*/
showErrorOnJoin: boolean,
/**
* Flag signaling the visibility of join label, input and buttons
*/
@@ -131,6 +131,11 @@ type Props = {
type State = {
/**
* Flag controlling the visibility of the error label.
*/
showError: boolean,
/**
* Flag controlling the visibility of the 'join by phone' buttons.
*/
@@ -161,16 +166,38 @@ class Prejoin extends Component<Props, State> {
super(props);
this.state = {
showError: false,
showJoinByPhoneButtons: false
};
this._closeDialog = this._closeDialog.bind(this);
this._showDialog = this._showDialog.bind(this);
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
this._onToggleButtonClick = this._onToggleButtonClick.bind(this);
this._onDropdownClose = this._onDropdownClose.bind(this);
this._onOptionsClick = this._onOptionsClick.bind(this);
this._setName = this._setName.bind(this);
}
_onJoinButtonClick: () => void;
/**
* Handler for the join button.
*
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onJoinButtonClick() {
if (this.props.showErrorOnJoin) {
this.setState({
showError: true
});
return;
}
this.setState({ showError: false });
this.props.joinConference();
}
_onToggleButtonClick: () => void;
@@ -258,7 +285,6 @@ class Prejoin extends Component<Props, State> {
*/
render() {
const {
joinButtonDisabled,
hasJoinByPhoneButton,
joinConference,
joinConferenceWithoutAudio,
@@ -272,8 +298,8 @@ class Prejoin extends Component<Props, State> {
videoTrack
} = this.props;
const { _closeDialog, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
const { showJoinByPhoneButtons } = this.state;
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onOptionsClick, _setName, _showDialog } = this;
const { showJoinByPhoneButtons, showError } = this.state;
return (
<PreMeetingScreen
@@ -289,16 +315,22 @@ class Prejoin extends Component<Props, State> {
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<InputField
autoFocus = { true }
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
{showError && <div
className = 'prejoin-error'
data-testid = 'prejoin.errorMessage'>{t('prejoin.errorMissingName')}</div>}
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { <div className = 'prejoin-preview-dropdown-btns'>
<div
className = 'prejoin-preview-dropdown-btn'
data-testid = 'prejoin.joinWithoutAudio'
onClick = { joinConferenceWithoutAudio }>
<Icon
className = 'prejoin-preview-dropdown-icon'
@@ -311,6 +343,7 @@ class Prejoin extends Component<Props, State> {
onClick = { _showDialog }>
<Icon
className = 'prejoin-preview-dropdown-icon'
data-testid = 'prejoin.joinByPhone'
size = { 24 }
src = { IconPhone } />
{ t('prejoin.joinAudioByPhone') }
@@ -319,9 +352,8 @@ class Prejoin extends Component<Props, State> {
isOpen = { showJoinByPhoneButtons }
onClose = { _onDropdownClose }>
<ActionButton
disabled = { joinButtonDisabled }
hasOptions = { true }
onClick = { joinConference }
onClick = { _onJoinButtonClick }
onOptionsClick = { _onOptionsClick }
testId = 'prejoin.joinMeeting'
type = 'primary'>
@@ -383,7 +415,7 @@ class Prejoin extends Component<Props, State> {
*/
function mapStateToProps(state, ownProps): Object {
const name = getDisplayName(state);
const joinButtonDisabled = isDisplayNameRequired(state) && !name;
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
const { showJoinActions } = ownProps;
const isInviteButtonEnabled = isButtonEnabled('invite');
@@ -397,11 +429,11 @@ function mapStateToProps(state, ownProps): Object {
return {
buttonIsToggled: isPrejoinSkipped(state),
joinButtonDisabled,
name,
deviceStatusVisible: isDeviceStatusVisible(state),
roomName: getRoomName(state),
showDialog: isJoinByPhoneDialogVisible(state),
showErrorOnJoin,
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
showCameraPreview: !isVideoMutedByUser(state),
showConferenceInfo,

View File

@@ -1,5 +1,6 @@
// @flow
import { updateConfig } from '../base/config';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
import { MiddlewareRegistry } from '../base/redux';
import { updateSettings } from '../base/settings';
@@ -24,6 +25,9 @@ MiddlewareRegistry.register(store => next => async action => {
const state = getState();
const { userSelectedSkipPrejoin } = state['features/prejoin'];
const localVideoTrack = getLocalVideoTrack(state['features/base/tracks']);
const { options } = action;
options && store.dispatch(updateConfig(options));
userSelectedSkipPrejoin && dispatch(updateSettings({
userSelectedSkipPrejoin

View File

@@ -6,6 +6,7 @@ import {
SET_DIALOUT_NUMBER,
SET_DIALOUT_STATUS,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PRECALL_TEST_RESULTS,
SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_DISPLAY_NAME_REQUIRED,
SET_PREJOIN_PAGE_VISIBILITY,
@@ -45,6 +46,12 @@ ReducerRegistry.register(
};
}
case SET_PRECALL_TEST_RESULTS:
return {
...state,
precallTestResults: action.value
};
case SET_PREJOIN_PAGE_VISIBILITY:
return {
...state,

View File

@@ -25,7 +25,7 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case LIB_WILL_INIT: {
if (analytics.rtcstatsEndpoint) {
if (analytics.rtcstatsEnabled) {
// RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global
// window functions. Because lib-jitsi-meet uses references to those functions that are taken on
// init, we need to add these proxies before it initializes, otherwise lib-jitsi-meet will use the
@@ -47,7 +47,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case CONFERENCE_JOINED: {
if (analytics.rtcstatsEndpoint && RTCStats.isInitialized()) {
if (analytics.rtcstatsEnabled && RTCStats.isInitialized()) {
// Once the conference started connect to the rtcstats server and send data.
try {
RTCStats.connect();

View File

@@ -0,0 +1,12 @@
import { VIDEO_QUALITY_LEVELS } from '../base/conference';
/**
* Maps quality level names used in the config.videoQuality.minHeightForQualityLvl to the quality level constants used
* by the application.
* @type {Object}
*/
export const CFG_LVL_TO_APP_QUALITY_LVL = {
'low': VIDEO_QUALITY_LEVELS.LOW,
'standard': VIDEO_QUALITY_LEVELS.STANDARD,
'high': VIDEO_QUALITY_LEVELS.HIGH
};

View File

@@ -0,0 +1,62 @@
// @flow
import { VIDEO_QUALITY_LEVELS } from '../base/conference';
import { CFG_LVL_TO_APP_QUALITY_LVL } from './constants';
/**
* Selects {@code VIDEO_QUALITY_LEVELS} for the given {@link availableHeight} and threshold to quality mapping.
*
* @param {number} availableHeight - The height to which a matching video quality level should be found.
* @param {Map<number, number>} heightToLevel - The threshold to quality level mapping. The keys are sorted in the
* ascending order.
* @returns {number} The matching value from {@code VIDEO_QUALITY_LEVELS}.
*/
export function getReceiverVideoQualityLevel(availableHeight: number, heightToLevel: Map<number, number>): number {
let selectedLevel = VIDEO_QUALITY_LEVELS.LOW;
for (const [ levelThreshold, level ] of heightToLevel.entries()) {
if (availableHeight >= levelThreshold) {
selectedLevel = level;
}
}
return selectedLevel;
}
/**
* Converts {@code Object} passed in the config which represents height thresholds to vide quality level mapping to
* a {@code Map}.
*
* @param {Object} minHeightForQualityLvl - The 'config.videoQuality.minHeightForQualityLvl' Object from
* the configuration. See config.js for more details.
* @returns {Map<number, number>|undefined} - A mapping of minimal thumbnail height required for given quality level or
* {@code undefined} if the map contains invalid values.
*/
export function validateMinHeightForQualityLvl(minHeightForQualityLvl: Object): ?Map<number, number> {
if (typeof minHeightForQualityLvl !== 'object'
|| Object.keys(minHeightForQualityLvl).map(lvl => Number(lvl))
.find(lvl => lvl === null || isNaN(lvl) || lvl < 0)) {
return undefined;
}
const levelsSorted
= Object.keys(minHeightForQualityLvl)
.map(k => Number(k))
.sort((a, b) => a - b);
const map = new Map();
for (const level of levelsSorted) {
const configQuality = minHeightForQualityLvl[level];
const appQuality = CFG_LVL_TO_APP_QUALITY_LVL[configQuality];
if (!appQuality) {
return undefined;
}
map.set(level, appQuality);
}
return map;
}

View File

@@ -1,2 +1,4 @@
export * from './components';
export * from './actions';
import './reducer';

View File

@@ -3,7 +3,6 @@
import {
CONFERENCE_JOINED,
VIDEO_QUALITY_LEVELS,
getNearestReceiverVideoQualityLevel,
setMaxReceiverVideoQuality,
setPreferredVideoQuality
} from '../base/conference';
@@ -11,7 +10,9 @@ import { getParticipantCount } from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { shouldDisplayTileView } from '../video-layout';
import { getReceiverVideoQualityLevel } from './functions';
import logger from './logger';
import { getMinHeightForQualityLvlMap } from './selector';
/**
* Implements the middleware of the feature video-quality.
@@ -66,7 +67,7 @@ StateListenerRegistry.register(
if (reducedUI) {
newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
} else if (displayTileView && !Number.isNaN(thumbnailHeight)) {
newMaxRecvVideoQuality = getNearestReceiverVideoQualityLevel(thumbnailHeight);
newMaxRecvVideoQuality = getReceiverVideoQualityLevel(thumbnailHeight, getMinHeightForQualityLvlMap(state));
// Override HD level calculated for the thumbnail height when # of participants threshold is exceeded
if (maxReceiverVideoQuality !== newMaxRecvVideoQuality && maxFullResolutionParticipants !== -1) {
@@ -74,7 +75,7 @@ StateListenerRegistry.register(
= participantCount > maxFullResolutionParticipants
&& newMaxRecvVideoQuality > VIDEO_QUALITY_LEVELS.STANDARD;
logger.info(`The nearest receiver video quality level for thumbnail height: ${thumbnailHeight}, `
logger.info(`Video quality level for thumbnail height: ${thumbnailHeight}, `
+ `is: ${newMaxRecvVideoQuality}, `
+ `override: ${String(override)}, `
+ `max full res N: ${maxFullResolutionParticipants}`);

View File

@@ -0,0 +1,41 @@
import { VIDEO_QUALITY_LEVELS } from '../base/conference';
import { SET_CONFIG } from '../base/config';
import { ReducerRegistry, set } from '../base/redux';
import { validateMinHeightForQualityLvl } from './functions';
import logger from './logger';
const DEFAULT_STATE = {
minHeightForQualityLvl: new Map()
};
DEFAULT_STATE.minHeightForQualityLvl.set(360, VIDEO_QUALITY_LEVELS.STANDARD);
DEFAULT_STATE.minHeightForQualityLvl.set(720, VIDEO_QUALITY_LEVELS.HIGH);
ReducerRegistry.register('features/base/videoquality', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_CONFIG:
return _setConfig(state, action);
}
return state;
});
/**
* Extracts the height to quality level mapping from the new config.
*
* @param {Object} state - The Redux state of feature base/lastn.
* @param {Action} action - The Redux action SET_CONFIG to reduce.
* @private
* @returns {Object} The new state after the reduction of the specified action.
*/
function _setConfig(state, { config }) {
const configuredMap = config?.videoQuality?.minHeightForQualityLvl;
const convertedMap = validateMinHeightForQualityLvl(configuredMap);
if (configuredMap && !convertedMap) {
logger.error('Invalid config value videoQuality.minHeightForQualityLvl');
}
return convertedMap ? set(state, 'minHeightForQualityLvl', convertedMap) : state;
}

View File

@@ -0,0 +1,11 @@
// @flow
/**
* Selects the thumbnail height to the quality level mapping from the config.
*
* @param {Object} state - The redux state.
* @returns {Map<number,number>}
*/
export function getMinHeightForQualityLvlMap(state: Object): Map<number, number> {
return state['features/base/videoquality'].minHeightForQualityLvl;
}

View File

@@ -0,0 +1,5 @@
local jibri_queue_component
= module:get_option_string(
"jibri_queue_component", "jibriqueue"..module.host);
module:add_identity("component", "jibri-queue", jibri_queue_component);

View File

@@ -0,0 +1,559 @@
local st = require "util.stanza";
local jid = require "util.jid";
local http = require "net.http";
local json = require "cjson";
local inspect = require('inspect');
local socket = require "socket";
local uuid_gen = require "util.uuid".generate;
local jwt = require "luajwtjitsi";
local it = require "util.iterators";
local neturl = require "net.url";
local parse = neturl.parseQuery;
local get_room_from_jid = module:require "util".get_room_from_jid;
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
local room_jid_split_subdomain = module:require "util".room_jid_split_subdomain;
local internal_room_jid_match_rewrite = module:require "util".internal_room_jid_match_rewrite;
local async_handler_wrapper = module:require "util".async_handler_wrapper;
-- this basically strips the domain from the conference.domain address
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
if parentHostName == nil then
log("error", "Failed to start - unable to get parent hostname");
return;
end
local parentCtx = module:context(parentHostName);
if parentCtx == nil then
log("error",
"Failed to start - unable to get parent context for host: %s",
tostring(parentHostName));
return;
end
local token_util = module:require "token/util".new(parentCtx);
local ASAPKeyServer;
local ASAPKeyPath;
local ASAPKeyId;
local ASAPIssuer;
local ASAPAudience;
local ASAPAcceptedIssuers;
local ASAPAcceptedAudiences;
local ASAPTTL;
local ASAPTTL_THRESHOLD;
local ASAPKey;
local JibriRegion;
local disableTokenVerification;
local muc_component_host;
local external_api_url;
local jwtKeyCacheSize;
local jwtKeyCache;
local function load_config()
ASAPKeyServer = module:get_option_string("asap_key_server");
if ASAPKeyServer then
module:log("debug", "ASAP Public Key URL %s", ASAPKeyServer);
token_util:set_asap_key_server(ASAPKeyServer);
end
ASAPKeyPath
= module:get_option_string("asap_key_path", '/etc/prosody/certs/asap.key');
ASAPKeyId
= module:get_option_string("asap_key_id", 'jitsi');
ASAPIssuer
= module:get_option_string("asap_issuer", 'jitsi');
ASAPAudience
= module:get_option_string("asap_audience", 'jibri-queue');
ASAPAcceptedIssuers
= module:get_option_array('asap_accepted_issuers',{'jibri-queue'});
module:log("debug", "ASAP Accepted Issuers %s", ASAPAcceptedIssuers);
token_util:set_asap_accepted_issuers(ASAPAcceptedIssuers);
ASAPAcceptedAudiences
= module:get_option_array('asap_accepted_audiences',{'*'});
module:log("debug", "ASAP Accepted Audiences %s", ASAPAcceptedAudiences);
token_util:set_asap_accepted_audiences(ASAPAcceptedAudiences);
-- do not require room to be set on tokens for jibri queue
token_util:set_asap_require_room_claim(false);
ASAPTTL
= module:get_option_number("asap_ttl", 3600);
ASAPTTL_THRESHOLD
= module:get_option_number("asap_ttl_threshold", 600);
queueServiceURL
= module:get_option_string("jibri_queue_url");
JibriRegion
= module:get_option_string("jibri_region", 'default');
-- option to enable/disable token verifications
disableTokenVerification
= module:get_option_boolean("disable_jibri_queue_token_verification", false);
muc_component_host
= module:get_option_string("muc_component");
external_api_url = module:get_option_string("external_api_url",tostring(parentHostName));
module:log("debug", "External advertised API URL", external_api_url);
-- TODO: Figure out a less arbitrary default cache size.
jwtKeyCacheSize
= module:get_option_number("jwt_pubkey_cache_size", 128);
jwtKeyCache = require"util.cache".new(jwtKeyCacheSize);
if queueServiceURL == nil then
log("error", "No jibri_queue_url specified. No service to contact!");
return;
end
if muc_component_host == nil then
log("error", "No muc_component specified. No muc to operate on for jibri queue!");
return;
end
-- Read ASAP key once on module startup
local f = io.open(ASAPKeyPath, "r");
if f then
ASAPKey = f:read("*all");
f:close();
if not ASAPKey then
module:log("warn", "No ASAP Key read from %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
else
module:log("warn", "Error reading ASAP Key %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
return true;
end
local function reload_config()
module:log("info", "Reloading configuration for jibri queue component");
local config_success = load_config();
-- clear ASAP public key cache on config reload
token_util:clear_asap_cache();
if not config_success then
log("error", "Unsuccessful reconfiguration, jibri queue component may misbehave");
end
end
local config_success = load_config();
if not config_success then
log("error", "Unsuccessful configuration step, jibri queue component disabled")
return;
end
local http_headers = {
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")",
["Content-Type"] = "application/json"
};
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, "util.async");
if not have_async then
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
return;
end
log("info", "Starting jibri queue handling for %s", muc_component_host);
local function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
local function generateToken(audience)
audience = audience or ASAPAudience
local t = os.time()
local err
local exp_key = 'asap_exp.'..audience
local token_key = 'asap_token.'..audience
local exp = jwtKeyCache:get(exp_key)
local token = jwtKeyCache:get(token_key)
--if we find a token and it isn't too far from expiry, then use it
if token ~= nil and exp ~= nil then
exp = tonumber(exp)
if (exp - t) > ASAPTTL_THRESHOLD then
return token
end
end
--expiry is the current time plus TTL
exp = t + ASAPTTL
local payload = {
iss = ASAPIssuer,
aud = audience,
nbf = t,
exp = exp,
}
-- encode
local alg = "RS256"
token, err = jwt.encode(payload, ASAPKey, alg, {kid = ASAPKeyId})
if not err then
token = 'Bearer '..token
jwtKeyCache:set(exp_key,exp)
jwtKeyCache:set(token_key,token)
return token
else
return ''
end
end
local function sendIq(participant,action,requestId,time,position,token)
local iqId = uuid_gen();
local from = module:get_host();
local outStanza = st.iq({type = 'set', from = from, to = participant, id = iqId}):tag("jibri-queue",
{ xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId, action = action });
if token then
outStanza:tag("token"):text(token):up()
end
if time then
outStanza:tag("time"):text(tostring(time)):up()
end
if position then
outStanza:tag("position"):text(tostring(position)):up()
end
module:send(outStanza);
end
local function cb(content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
end
end
local function sendEvent(type,room_address,participant,requestId,replyIq,replyError)
local event_ts = round(socket.gettime()*1000);
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_address);
local room_param = '';
if target_subdomain then
room_param = target_subdomain..'/'..node;
else
room_param = node;
end
local out_event = {
["conference"] = room_address,
["roomParam"] = room_param,
["eventType"] = type,
["participant"] = participant,
["externalApiUrl"] = external_api_url.."/jibriqueue/update",
["requestId"] = requestId,
["region"] = JibriRegion,
}
module:log("debug","Sending event %s",inspect(out_event));
local headers = http_headers or {}
headers['Authorization'] = generateToken()
module:log("debug","Sending headers %s",inspect(headers));
local requestURL = queueServiceURL.."/job/recording"
if type=="LeaveQueue" then
requestURL = requestURL .."/cancel"
end
local request = http.request(requestURL, {
headers = headers,
method = "POST",
body = json.encode(out_event)
}, function (content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
if (replyIq) then
module:log("debug", "sending reply IQ %s",inspect(replyIq));
module:send(replyIq);
end
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
if (replyError) then
module:log("warn", "sending reply error IQ %s",inspect(replyError));
module:send(replyError);
end
end
end);
end
function clearRoomQueueByOccupant(room, occupant)
room.jibriQueue[occupant.jid] = nil;
end
function addRoomQueueByOccupant(room, occupant, requestId)
room.jibriQueue[occupant.jid] = requestId;
end
-- receives iq from client currently connected to the room
function on_iq(event)
local requestId;
-- Check the type of the incoming stanza to avoid loops:
if event.stanza.attr.type == "error" then
return; -- We do not want to reply to these, so leave.
end
if event.stanza.attr.to == module:get_host() then
if event.stanza.attr.type == "set" then
local reply = st.reply(event.stanza);
local replyError = st.error_reply(event.stanza,'cancel','internal-server-error',"Queue Server Error");
local jibriQueue
= event.stanza:get_child('jibri-queue', 'http://jitsi.org/protocol/jibri-queue');
if jibriQueue then
module:log("debug", "Received Jibri Queue Request: %s ",inspect(jibriQueue));
local roomAddress = jibriQueue.attr.room;
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
if not room then
module:log("warn", "No room found %s", roomAddress);
return false;
end
local from = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
module:log("warn", "No occupant %s found for %s", from, roomAddress);
return false;
end
local action = jibriQueue.attr.action;
if action == 'join' then
-- join action, so send event out
requestId = uuid_gen();
module:log("debug","Received join queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- now handle new jibri queue message
addRoomQueueByOccupant(room, occupant, requestId);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending JoinQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('JoinQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
if action == 'leave' then
requestId = jibriQueue.attr.requestId;
module:log("debug","Received leave queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- TODO: check that requestId is the same as cached value
clearRoomQueueByOccupant(room, occupant);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending LeaveQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('LeaveQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
else
module:log("warn","Jibri Queue Stanza missing child %s",inspect(event.stanza))
end
end
end
return true
end
-- create recorder queue cache for the room
function room_created(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
room.jibriQueue = {};
end
-- Conference ended, clear all queue cache jids
function room_destroyed(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
for jid, x in pairs(room.jibriQueue) do
if x then
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),jid,x);
end
end
end
-- Occupant left remove it from the queue if it joined the queue
function occupant_leaving(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
local occupant = event.occupant;
local requestId = room.jibriQueue[occupant.jid];
-- check if user has cached queue request
if requestId then
-- remove occupant from queue cache, signal backend
room.jibriQueue[occupant.jid] = nil;
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),occupant.jid,requestId);
end
end
module:hook("iq/host", on_iq);
-- executed on every host added internally in prosody, including components
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("debug","Hook to muc events on %s", host);
local muc_module = module:context(host);
muc_module:hook("muc-room-created", room_created, -1);
-- muc_module:hook("muc-occupant-joined", occupant_joined, -1);
muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
muc_module:hook("muc-room-destroyed", room_destroyed, -1);
end
end
if prosody.hosts[muc_component_host] == nil then
module:log("debug","No muc component found, will listen for it: %s", muc_component_host)
-- when a host or component is added
prosody.events.add_handler("host-activated", process_host);
else
process_host(muc_component_host);
end
module:log("info", "Loading jibri_queue_component");
--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_name the room name
-- @param group name of the group (optional)
-- @param session the session to use for storing token specific fields
-- @return true if values are ok or false otherwise
function verify_token(token, room_jid, session)
if disableTokenVerification then
return true;
end
-- if not disableTokenVerification and we do not have token
-- stop here, cause the main virtual host can have guest access enabled
-- (allowEmptyToken = true) and we will allow access to rooms info without
-- a token
if token == nil then
log("warn", "no token provided");
return false;
end
session.auth_token = token;
local verified, reason, message = token_util:process_and_verify_token(session);
if not verified then
log("warn", "not a valid token %s: %s", tostring(reason), tostring(message));
log("debug", "invalid token %s", token);
return false;
end
return true;
end
--- Handles request for updating jibri queue status
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_update_jibri_queue(event)
local body = json.decode(event.request.body);
module:log("debug","Update Jibri Queue Event Received: %s",inspect(body));
local token = event.request.headers["authorization"];
if not token then
token = ''
else
local prefixStart, prefixEnd = token:find("Bearer ");
if prefixStart ~= 1 then
module:log("error", "REST event: Invalid authorization header format. The header must start with the string 'Bearer '");
return { status_code = 403; };
end
token = token:sub(prefixEnd + 1);
end
local user_jid = body["participant"];
local roomAddress = body["conference"];
local userJWT = body["token"];
local action = body["action"];
local time = body["time"];
local position = body["position"];
local requestId = body["requestId"];
if not action then
if userJWT then
action = 'token';
else
action = 'info';
end
end
local room_jid = room_jid_match_rewrite(roomAddress);
if not verify_token(token, room_jid, {}) then
log("error", "REST event: Invalid token for room %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 403; };
end
local room = get_room_from_jid(room_jid);
if (not room) then
log("error", "REST event: no room found %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 404; };
end
local occupant = room:get_occupant_by_real_jid(user_jid);
if not occupant then
log("warn", "REST event: No occupant %s found for %s to route action %s for requestId %s", user_jid, roomAddress, action, requestId);
return { status_code = 404; };
end
if not room.jibriQueue[occupant.jid] then
log("warn", "REST event: No queue request found for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId)
return { status_code = 404; };
end
if not requestId then
requestId = room.jibriQueue[occupant.jid];
end
if action == 'token' and userJWT then
log("debug", "REST event: Token received for occupant %s in conference %s requestId %s, clearing room queue");
clearRoomQueueByOccupant(room, occupant);
end
log("debug", "REST event: Sending update for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId);
sendIq(occupant.jid,action,requestId,time,position,userJWT);
return { status_code = 200; };
end
module:depends("http");
module:provides("http", {
default_path = "/";
name = "jibriqueue";
route = {
["POST /jibriqueue/update"] = function (event) return async_handler_wrapper(event,handle_update_jibri_queue) end;
};
});
module:hook_global('config-reloaded', reload_config);

View File

@@ -132,7 +132,7 @@ function filter_stanza(stanza)
-- check is an owner, only owners can receive the presence
local room = main_muc_service.get_room_from_jid(jid_bare(node .. '@' .. main_muc_component_config));
if room.get_affiliation(room, stanza.attr.to) == 'owner' then
if not room or room.get_affiliation(room, stanza.attr.to) == 'owner' then
return stanza;
end
@@ -159,6 +159,11 @@ function attach_lobby_room(room)
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
local new_room = lobby_muc_service.create_room(lobby_room_jid);
-- set persistent the lobby room to avoid it to be destroyed
-- there are cases like when selecting new moderator after the current one leaves
-- which can leave the room with no occupants and it will be destroyed and we want to
-- avoid lobby destroy while it is enabled
new_room:set_persistent(true);
module:log("debug","Lobby room jid = %s created",lobby_room_jid);
new_room.main_room = room;
room._data.lobbyroom = new_room;
@@ -168,6 +173,18 @@ function attach_lobby_room(room)
return false
end
-- destroys lobby room for the supplied main room
function destroy_lobby_room(room, newjid, message)
if not message then
message = 'Lobby room closed.';
end
if room and room._data.lobbyroom then
room._data.lobbyroom:set_persistent(false);
room._data.lobbyroom:destroy(newjid, message);
room._data.lobbyroom = nil;
end
end
-- process a host module directly if loaded or hooks to wait for its load
function process_host_module(name, callback)
local function process_host(host)
@@ -280,16 +297,14 @@ process_host_module(main_muc_component_config, function(host_module, host)
notify_lobby_enabled(room, actor, true);
end
elseif room._data.lobbyroom then
room._data.lobbyroom:destroy(room.jid, 'Lobby room closed.');
room._data.lobbyroom = nil;
destroy_lobby_room(room, room.jid);
notify_lobby_enabled(room, actor, false);
end
end);
host_module:hook('muc-room-destroyed',function(event)
local room = event.room;
if room._data.lobbyroom then
room._data.lobbyroom:destroy(nil, 'Lobby room closed.');
room._data.lobbyroom = nil;
destroy_lobby_room(room, nil);
end
end);
host_module:hook('muc-disco#info', function (event)
@@ -399,7 +414,12 @@ function handle_create_lobby(event)
attach_lobby_room(room)
end
function handle_destroy_lobby(event)
destroy_lobby_room(event.room, event.newjid, event.message);
end
module:hook_global('bosh-session', update_session);
module:hook_global('websocket-session', update_session);
module:hook_global('config-reloaded', load_config);
module:hook_global('create-lobby-room', handle_create_lobby);
module:hook_global('destroy-lobby-room', handle_destroy_lobby);

View File

@@ -93,6 +93,8 @@ function Util.new(module)
--array of accepted audiences: by default only includes our appId
self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'})
self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
if self.asapKeyServer and not have_async then
module:log("error", "requires a version of Prosody with util.async");
return nil;
@@ -102,7 +104,23 @@ function Util.new(module)
end
function Util:set_asap_key_server(asapKeyServer)
self.asapKeyServer = asapKeyServer
self.asapKeyServer = asapKeyServer;
end
function Util:set_asap_accepted_issuers(acceptedIssuers)
self.acceptedIssuers = acceptedIssuers;
end
function Util:set_asap_accepted_audiences(acceptedAudiences)
self.acceptedAudiences = acceptedAudiences;
end
function Util:set_asap_require_room_claim(checkRoom)
self.requireRoomClaim = checkRoom;
end
function Util:clear_asap_cache()
self.cache = require"util.cache".new(cacheSize);
end
--- Returns the public key by keyID
@@ -114,18 +132,41 @@ function Util:get_public_key(keyId)
-- If the key is not found in the cache.
module:log("debug", "Cache miss for key: "..keyId);
local code;
local timeout_occurred;
local wait, done = async.waiter();
local function cb(content_, code_, response_, request_)
content, code = content_, code_;
if code == 200 or code == 204 then
self.cache:set(keyId, content);
else
module:log("warn", "Error on public key request: Code %s, Content %s",
code_, content_);
end
done();
end
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
local function cb(content_, code_, response_, request_)
if timeout_occurred == nil then
content, code = content_, code_;
if code == 200 or code == 204 then
self.cache:set(keyId, content);
else
module:log("warn", "Error on public key request: Code %s, Content %s",
code_, content_);
end
done();
else
module:log("warn", "public key reply delivered after timeout from: %s",keyurl);
end
end
-- TODO: Is the done() call racey? Can we cancel this if the request
-- succeedes?
local function cancel()
-- TODO: This check is racey. Not likely to be a problem, but we should
-- still stick a mutex on content / code at some point.
if code == nil then
timeout_occurred = true;
module:log("warn", "Timeout %s seconds fetching public key from: %s",http_timeout,keyurl);
if http.destroy_request ~= nil then
http.destroy_request(request);
end
done();
end
end
module:log("debug", "Fetching public key from: "..keyurl);
-- We hash the key ID to work around some legacy behavior and make
@@ -136,19 +177,6 @@ function Util:get_public_key(keyId)
method = "GET"
}, cb);
-- TODO: Is the done() call racey? Can we cancel this if the request
-- succeedes?
local function cancel()
-- TODO: This check is racey. Not likely to be a problem, but we should
-- still stick a mutex on content / code at some point.
if code == nil then
-- no longer present in prosody 0.11, so check before calling
if http.destroy_request ~= nil then
http.destroy_request(request);
end
done();
end
end
timer.add_task(http_timeout, cancel);
wait();
@@ -169,6 +197,10 @@ end
-- @param 'acceptedIssuers' list of issuers to check
-- @return nil and error string or true for accepted claim
function Util:verify_issuer(issClaim, acceptedIssuers)
if not acceptedIssuers then
acceptedIssuers = self.acceptedIssuers
end
module:log("debug","verify_issuer claim: %s against accepted: %s",issClaim, acceptedIssuers);
for i, iss in ipairs(acceptedIssuers) do
if issClaim == iss then
--claim matches an accepted issuer so return success
@@ -183,6 +215,7 @@ end
-- @param 'aud' claim from the token to verify
-- @return nil and error string or true for accepted claim
function Util:verify_audience(audClaim)
module:log("debug","verify_audience claim: %s against accepted: %s",audClaim, self.acceptedAudiences);
for i, aud in ipairs(self.acceptedAudiences) do
if aud == '*' then
--* indicates to accept any audience in the claims so return success
@@ -223,9 +256,11 @@ function Util:verify_token(token, secret, acceptedIssuers)
return nil, issCheckErr;
end
local roomClaim = claims["room"];
if roomClaim == nil then
return nil, "'room' claim is missing";
if self.requireRoomClaim then
local roomClaim = claims["room"];
if roomClaim == nil then
return nil, "'room' claim is missing";
end
end
local audClaim = claims["aud"];

View File

@@ -18,14 +18,20 @@ local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
local target_subdomain_pattern
= "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
-- Utility function to split room JID to include room name and subdomain
local function room_jid_split_subdomain(room_jid)
local node, host, resource = jid.split(room_jid);
local target_subdomain = host and host:match(target_subdomain_pattern);
return node, host, resource, target_subdomain
end
--- Utility function to check and convert a room JID from
-- virtual room1@muc.foo.example.com to real [foo]room1@muc.example.com
-- @param room_jid the room jid to match and rewrite if needed
-- @return returns room jid [foo]room1@muc.example.com when it has subdomain
-- otherwise room1@muc.example.com(the room_jid value untouched)
local function room_jid_match_rewrite(room_jid)
local node, host, resource = jid.split(room_jid);
local target_subdomain = host and host:match(target_subdomain_pattern);
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_jid);
if not target_subdomain then
module:log("debug", "No need to rewrite out 'to' %s", room_jid);
return room_jid;
@@ -38,6 +44,23 @@ local function room_jid_match_rewrite(room_jid)
return room_jid
end
local function internal_room_jid_match_rewrite(room_jid)
local node, host, resource = jid.split(room_jid);
if host ~= muc_domain or not node then
module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
if not (target_node and target_subdomain) then
module:log("debug", "Not rewriting... unexpected node format: %s", node);
return room_jid;
end
-- Ok, rewrite room_jid address to pretty format
local new_node, new_host, new_resource = target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource;
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
--- Finds and returns room by its jid
-- @param room_jid the room jid to search in the muc component
@@ -191,5 +214,7 @@ return {
get_room_from_jid = get_room_from_jid;
async_handler_wrapper = async_handler_wrapper;
room_jid_match_rewrite = room_jid_match_rewrite;
room_jid_split_subdomain = room_jid_split_subdomain;
internal_room_jid_match_rewrite = internal_room_jid_match_rewrite;
update_presence_identity = update_presence_identity;
};

0
static/close3.js Normal file
View File

View File

@@ -34,8 +34,8 @@
<!--#include virtual="/title.html" -->
<script><!--#include virtual="/config.js" --></script>
<script><!--#include virtual="/interface_config.js" --></script>
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/app.bundle.min.js?v=139"></script>
<script src="libs/lib-jitsi-meet.min.js"></script>
<script src="libs/app.bundle.min.js"></script>
</head>
<body>
<div id="react"></div>

View File

@@ -225,6 +225,12 @@ module.exports = [
},
performance: getPerformanceHints(5 * 1024)
}),
Object.assign({}, config, {
entry: {
'close3': './static/close3.js'
},
performance: getPerformanceHints(128 * 1024)
}),
// Because both video-blur-effect and rnnoise-processor modules are loaded
// in a lazy manner using the loadScript function with a hard coded name,