Compare commits

...

89 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
4e3da8e61f chore(ios,version) bump SDK 2020-09-18 11:40:39 +02:00
Saúl Ibarra Corretgé
8979352334 chore(ios) bump cocoapods version 2020-09-18 10:27:54 +02:00
Saúl Ibarra Corretgé
c0e1c277f9 chore(ios,version) bump SDK 2020-09-18 09:20:10 +02:00
Saúl Ibarra Corretgé
d895ccdeff chore(android,version) bump 2020-09-18 09:07:00 +02:00
Saúl Ibarra Corretgé
1594ae89db fix(android,calendar) avoid crash 2020-09-18 08:56:30 +02:00
Saúl Ibarra Corretgé
c4870c9159 android: fix crash when requesting permissions
The RN Permissions module calls this in a non-UI thread. What we observe is a
crash in ViewGroup.dispatchCancelPendingInputEvents, which is called on the
calling (ie, non-UI) thread. This doesn't look very safe, so try to avoid a
crash by pretending the permission was denied.
2020-09-18 08:54:02 +02:00
Hristo Terezov
fc75d45c6c feat(video-quality): add iframe event and getter. 2020-09-09 18:04:17 -05:00
Hristo Terezov
25839b18d2 feat(video-quality): persist. 2020-09-09 18:04:17 -05:00
Hristo Terezov
43f36c8cfd fix(ifarme-api): set-video-quality to use redux. 2020-09-09 18:04:17 -05:00
Hristo Terezov
b02d96231c ref(video-quality): Move all related code. 2020-09-09 18:04:17 -05:00
Дамян Минков
651d713206 feat: Allows jvb to control DTLS/SRTP protection profile. (#7626)
* feat: Allows jvb to control DTLS/SRTP protection profile.

* test: Adds dominant speaker change print for large in testing mode.
2020-09-09 16:14:53 -05:00
Saúl Ibarra Corretgé
9e5f469e0c deps: run npm audit fix
The amplitude-js dependency required a major bump.
2020-09-09 18:59:40 +02:00
Saúl Ibarra Corretgé
493ce8249e web,small-video: don't show screen content on thumbnails
This applies just to large view, not tile view.
2020-09-09 18:59:12 +02:00
Saúl Ibarra Corretgé
fdffb688c1 web,small-video: introduce screen-sharing indicator 2020-09-09 18:59:12 +02:00
Saúl Ibarra Corretgé
4807badac8 rn,thumbnail: introduce screen-sharing indicator 2020-09-09 18:59:12 +02:00
Saúl Ibarra Corretgé
5e3bd746e9 config: fix missing comma (#7667)
Fixes: https://github.com/jitsi/jitsi-meet/issues/7665
2020-09-09 07:18:54 -05:00
Jon Leren Schøpzinsky
8fa41bebb7 rn: don't start native call integration multiple times
When you join a conference that needs an authenticated moderator, as a guest, Jitsi Meet will continuously try and connect to the meeting every 5 seconds. Avoid starting the native call integration more than once.

Fixes: https://github.com/jitsi/jitsi-meet/issues/6260
2020-09-09 09:22:34 +02:00
paweldomas
cb7c280da6 fix(RN): crash on undefined state['features/dynamic-branding'] 2020-09-08 11:39:49 +02:00
emrah
0e50f1887e fix: enable token_verification during installation of jitsi-meet-tokens (#7630) 2020-09-04 10:17:54 -05:00
emrah
476ca54711 fix: keep plugin_paths while removing jitsi-meet-tokens (#7632) 2020-09-04 08:01:40 -05:00
emrah
70aa19e6d9 fix: disable token_verification while removing jitsi-meet-tokens (#7631) 2020-09-04 08:01:14 -05:00
emrah
7778a17b90 fix: added libssl1.0-dev to the dependencies of jitsi-meet-tokens (#7629) 2020-09-04 08:00:54 -05:00
Tudor-Ovidiu Avram
7ff41217ac feat(vpaas) disable deeplinking page 2020-09-03 10:45:51 -05:00
emrah
e8c44c10dd jitsi-meet-tokens: added git to the dependency list 2020-09-02 12:23:26 -05:00
damencho
b087b22d4f feat: Whitelist option to hide lobby button. 2020-09-02 11:49:15 -05:00
emrah
e988bf6565 fix: jitsi-meet-tokens - the first installation check (#7618) 2020-09-02 11:46:32 -05:00
Дамян Минков
d169bd5007 feat: Adds interface config to hide lobby button. (#7619)
* feat: Adds interface config to hide lobby button.

* squash: Moves the config to config.js and add it to mobile.
2020-09-02 10:28:22 -05:00
Boris Grozev
ac17db9df5 Update lib-jitsi-meet and add the RED option to config.js. 2020-09-01 11:49:23 -05:00
Felix C. Stegerman
322618357c jitsi-meet-tokens.postinst: fix tests 2020-09-01 07:51:37 -05:00
RabeeAbuBaker
79c1358f4b FEAT: Automatically copy invite URL after creating a room (#7581)
* Resolves #7501
- Automatically copy invite URL after creating a room

* Resolves #7501
- Automatically copy invite URL after creating a room

* - Adding config flag to enable the feature
2020-08-30 09:36:52 -05:00
Hristo Terezov
5e85b5f63a fix(close3): Add close3.js 2020-08-28 11:33:19 -05:00
vp8x8
74f7c4141f fix(vpaas): Fix billing counter auth (#7595) 2020-08-28 15:43:14 +03:00
Vlad Piersec
4866ddc2ad fix(vpaas): Fix tenant typo 2020-08-28 11:08:59 +03:00
Vlad Piersec
71d0577a49 feat(vpaas): Add endpoint counter & remove branding on vpaas meetings 2020-08-27 14:49:03 -05:00
Hristo Terezov
b7529863d5 fix(iframe-api): setDevice. 2020-08-25 18:37:03 -05:00
Hristo Terezov
4ded94d130 fix(settings): store url display name and email. 2020-08-25 18:37:03 -05:00
Jaya Allamsetty
eb8b730227 deps: update lib-jitsi-meet to latest.
Update config.js to include the new codec preference options under videoQuality settings.
2020-08-25 16:52:48 -04:00
Vlad Piersec
4bd57692b7 feat(prejoin): Show warning if audio device does not receive data 2020-08-25 11:39:59 -05:00
Aaron van Meerten
5d012c24a7 Merge pull request #7508 from abora8x8/abora/vpass
Add pre and post validation for users that want to use their own publ…
2020-08-24 09:45:21 -05:00
Vlad Piersec
4f52a29120 fix(prejoin): Make avatar resizable 2020-08-21 14:10:24 -05:00
Tudor-Ovidiu Avram
8a4fb72eae feat(branding) allow invite links to be branded 2020-08-21 11:00:12 -05:00
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
Andrei Bora
af71d80150 Fix call after timeout 2020-08-19 17:38:40 +03:00
Andrei Bora
b765adca75 Solve review issues and add retries for http call 2020-08-19 17:11:18 +03:00
Andrei Bora
92e6cf7618 Add pre and post validation for users that want to use their own public keys 2020-08-19 16:50:24 +03: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
114 changed files with 2657 additions and 7960 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

@@ -51,6 +51,8 @@ deploy-appbundle:
$(BUILD_DIR)/video-blur-effect.min.map \
$(BUILD_DIR)/rnnoise-processor.min.js \
$(BUILD_DIR)/rnnoise-processor.min.map \
$(BUILD_DIR)/close3.min.js \
$(BUILD_DIR)/close3.min.map \
$(DEPLOY_DIR)
deploy-lib-jitsi-meet:

View File

@@ -20,5 +20,5 @@
android.useAndroidX=true
android.enableJetifier=true
appVersion=20.4.0
appVersion=20.4.1
sdkVersion=2.10.0

View File

@@ -24,6 +24,8 @@ import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.PermissionListener;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* Helper class to encapsulate the work which needs to be done on
* {@link Activity} lifecycle methods in order for the React side to be aware of
@@ -177,6 +179,16 @@ public class JitsiMeetActivityDelegate {
public static void requestPermissions(Activity activity, String[] permissions, int requestCode, PermissionListener listener) {
permissionListener = listener;
activity.requestPermissions(permissions, requestCode);
// The RN Permissions module calls this in a non-UI thread. What we observe is a crash in ViewGroup.dispatchCancelPendingInputEvents,
// which is called on the calling (ie, non-UI) thread. This doesn't look very safe, so try to avoid a crash by pretending the permission
// was denied.
try {
activity.requestPermissions(permissions, requestCode);
} catch (Exception e) {
JitsiMeetLogger.e(e, "Error requesting permissions");
onRequestPermissionsResult(requestCode, permissions, new int[0]);
}
}
}

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

@@ -118,6 +118,9 @@ var config = {
// Valid values are in the range 6000 to 510000
// opusMaxAverageBitrate: 20000,
// Enables redundancy for Opus
// enableOpusRed: false
// Video
// Sets the preferred resolution (height) for local video. Defaults to 720.
@@ -125,7 +128,7 @@ var config = {
// How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
// Use -1 to disable.
// maxFullResolutionParticipants: 2
// maxFullResolutionParticipants: 2,
// w3c spec-compliant video constraints to use for video capture. Currently
// used by browsers that return true from lib-jitsi-meet's
@@ -161,6 +164,7 @@ var config = {
// Note that it's not recommended to do this because simulcast is not
// supported when using H.264. For 1-to-1 calls this setting is enabled by
// default and can be toggled in the p2p section.
// This option has been deprecated, use preferredCodec under videoQuality section instead.
// preferH264: true,
// If set to true, disable H.264 video codec by stripping it out of the
@@ -234,6 +238,18 @@ var config = {
// Specify the settings for video quality optimizations on the client.
// videoQuality: {
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified
// // here will be removed from the list of codecs present in the SDP answer generated by the client. If the
// // same codec is specified for both the disabled and preferred option, the disable settings will prevail.
// // Note that 'VP8' cannot be disabled since it's a mandatory codec, the setting will be ignored in this case.
// disabledCodec: 'H264',
//
// // Provides a way to set a preferred video codec for the JVB connection. If 'H264' is specified here,
// // simulcast will be automatically disabled since JVB doesn't support H264 simulcast yet. This will only
// // rearrange the the preference order of the codecs in the SDP answer generated by the browser only if the
// // preferred codec specified here is present. Please ensure that the JVB offers the specified codec for this
// // to take effect.
// preferredCodec: 'VP8',
//
// // Provides a way to configure the maximum bitrates that will be enforced on the simulcast streams for
// // video tracks. The keys in the object represent the type of the stream (LD, SD or HD) and the values
@@ -244,6 +260,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'
// }
// },
@@ -309,6 +340,9 @@ var config = {
// UI
//
// Hides lobby button
// hideLobbyButton: false,
// Require users to always specify a display name.
// requireDisplayName: true,
@@ -357,6 +391,10 @@ var config = {
// set or the lobby is not enabled.
// enableInsecureRoomNameWarning: false,
// Whether to automatically copy invitation URL after creating a room.
// Document should be focused for this option to work
// enableAutomaticUrlCopy: false,
// Stats
//
@@ -420,13 +458,20 @@ var config = {
// iceTransportPolicy: 'all',
// If set to true, it will prefer to use H.264 for P2P calls (if H.264
// is supported).
// is supported). This setting is deprecated, use preferredCodec instead.
// preferH264: true
// Provides a way to set the video codec preference on the p2p connection. Acceptable
// codec values are 'VP8', 'VP9' and 'H264'.
// preferredCodec: 'H264',
// If set to true, disable H.264 video codec by stripping it out of the
// SDP.
// SDP. This setting is deprecated, use disabledCodec instead.
// disableH264: false,
// Provides a way to prevent a video codec from being negotiated on the p2p connection.
// disabledCodec: '',
// How long we're going to wait, before going back to P2P after the 3rd
// participant has left the conference (to filter out page reload).
// backToP2PDelay: 5
@@ -444,6 +489,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/,
@@ -605,6 +656,13 @@ var config = {
tokenAuthUrl
*/
/**
* This property can be used to alter the generated meeting invite links (in combination with a branding domain
* which is retrieved internally by jitsi meet) (e.g. https://meet.jit.si/someMeeting
* can become https://brandedDomain/roomAlias)
*/
// brandingRoomAlias: null,
// List of undocumented settings used in lib-jitsi-meet
/**
_peerConnStatusOutOfLastNTimeout

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

@@ -197,16 +197,9 @@
text-align: center;
}
.preview-avatar-container {
width: 100%;
height: 80%;
display: flex;
align-items: center;
justify-content: center;
}
.avatar {
background: #A4B8D1;
margin: 0 auto;
}
video {

View File

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

2
debian/control vendored
View File

@@ -47,7 +47,7 @@ Description: Prosody configuration for Jitsi Meet
Package: jitsi-meet-tokens
Architecture: all
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly747) | prosody-0.11 | prosody (>= 0.11.2), libssl-dev, luarocks, jitsi-meet-prosody
Depends: ${misc:Depends}, prosody-trunk (>= 1nightly747) | prosody-0.11 | prosody (>= 0.11.2), libssl1.0-dev | libssl-dev, luarocks, jitsi-meet-prosody, git
Description: Prosody token authentication plugin for Jitsi Meet
Package: jitsi-meet-turnserver

View File

@@ -48,9 +48,9 @@ case "$1" in
db_stop
if [ -f "$PROSODY_HOST_CONFIG" ] ; then
# search for --plugin_paths, if this is not enabled this is the
# search for the token auth, if this is not enabled this is the
# first time we install tokens package and needs a config change
if grep -q "\-\-plugin_paths" "$PROSODY_HOST_CONFIG"; then
if ! egrep -q '^\s*authentication\s*=\s*"token"' "$PROSODY_HOST_CONFIG"; then
# enable tokens in prosody host config
sed -i 's/--plugin_paths/plugin_paths/g' $PROSODY_HOST_CONFIG
sed -i 's/authentication = "anonymous"/authentication = "token"/g' $PROSODY_HOST_CONFIG
@@ -58,6 +58,7 @@ case "$1" in
sed -i "s/ --app_id=\"example_app_id\"/ app_id=\"$APP_ID\"/g" $PROSODY_HOST_CONFIG
sed -i "s/ --app_secret=\"example_app_secret\"/ app_secret=\"$APP_SECRET\"/g" $PROSODY_HOST_CONFIG
sed -i 's/ --modules_enabled = { "token_verification" }/ modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
sed -i '/^\s*--\s*"token_verification"/ s/--\s*//' $PROSODY_HOST_CONFIG
# Install luajwt
if ! luarocks install luajwtjitsi; then
@@ -73,9 +74,9 @@ case "$1" in
PRTRUNK_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'prosody-trunk' 2>/dev/null | awk '{print $3}' || true)"
PR_VER_INSTALLED=$(dpkg-query -f='${Version}\n' --show prosody 2>/dev/null || true)
if [ "$PR10_INSTALL_CHECK" = "installed" ] \
|| "$PR10_INSTALL_CHECK" = "unpacked" \
|| "$PRTRUNK_INSTALL_CHECK" = "installed" \
|| "$PRTRUNK_INSTALL_CHECK" = "unpacked" \
|| [ "$PR10_INSTALL_CHECK" = "unpacked" ] \
|| [ "$PRTRUNK_INSTALL_CHECK" = "installed" ] \
|| [ "$PRTRUNK_INSTALL_CHECK" = "unpacked" ] \
|| dpkg --compare-versions "$PR_VER_INSTALLED" lt "0.11" ; then
sed -i 's/module:hook_global(/module:hook(/g' /usr/share/jitsi-meet/prosody-plugins/mod_auth_token.lua
fi

View File

@@ -37,11 +37,10 @@ case "$1" in
APP_SECRET=$RET
# Revert prosody config
sed -i 's/plugin_paths/--plugin_paths/g' $PROSODY_HOST_CONFIG
sed -i 's/authentication = "token"/authentication = "anonymous"/g' $PROSODY_HOST_CONFIG
sed -i "s/ app_id=\"$APP_ID\"/ --app_id=\"example_app_id\"/g" $PROSODY_HOST_CONFIG
sed -i "s/ app_secret=\"$APP_SECRET\"/ --app_secret=\"example_app_secret\"/g" $PROSODY_HOST_CONFIG
sed -i 's/ -- "token_verification"/ "token_verification"/g' $PROSODY_HOST_CONFIG
sed -i '/^\s*"token_verification"/ s/"token_verification"/-- "token_verification"/' $PROSODY_HOST_CONFIG
if [ -x "/etc/init.d/prosody" ]; then
invoke-rc.d prosody restart || true

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

@@ -591,4 +591,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 082858daebbe170e7a490de433e7f2a99e0c3701
COCOAPODS: 1.9.1
COCOAPODS: 1.9.3

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.10.0</string>
<string>2.10.2</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

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:",
@@ -503,6 +504,7 @@
"poweredby": "powered by",
"prejoin": {
"audioAndVideoError": "Audio and video error:",
"audioDeviceProblem": "There is a problem with your audio device",
"audioOnlyError": "Audio error:",
"audioTrackError": "Could not create audio track.",
"calling": "Calling",
@@ -510,6 +512,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 +540,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",

View File

@@ -524,6 +524,19 @@ class API {
});
}
/**
* Notify external application that the video quality setting has changed.
*
* @param {number} videoQuality - The video quality. The number represents the maximum height of the video streams.
* @returns {void}
*/
notifyVideoQualityChanged(videoQuality: number) {
this._sendEvent({
name: 'video-quality-changed',
videoQuality
});
}
/**
* Notify external application (if API is enabled) that message was
* received.

View File

@@ -80,6 +80,7 @@ const events = {
'video-conference-left': 'videoConferenceLeft',
'video-availability-changed': 'videoAvailabilityChanged',
'video-mute-status-changed': 'videoMuteStatusChanged',
'video-quality-changed': 'videoQualityChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'subject-change': 'subjectChange',
@@ -503,6 +504,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
changeParticipantNumber(this, -1);
delete this._participants[this._myUserID];
break;
case 'video-quality-changed':
this._videoQuality = data.videoQuality;
break;
}
const eventName = events[name];
@@ -689,6 +693,15 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return getCurrentDevices(this._transport);
}
/**
* Returns the current video quality setting.
*
* @returns {number}
*/
getVideoQuality() {
return this._videoQuality;
}
/**
* Check if the audio is available.
*

View File

@@ -373,6 +373,7 @@ export default class RemoteVideo extends SmallVideo {
if (stream === this.videoStream) {
this.videoStream = null;
this.videoType = undefined;
}
this.updateView();
@@ -481,7 +482,12 @@ export default class RemoteVideo extends SmallVideo {
const isVideo = stream.isVideoTrack();
isVideo ? this.videoStream = stream : this.audioStream = stream;
if (isVideo) {
this.videoStream = stream;
this.videoType = stream.videoType;
} else {
this.audioStream = stream;
}
if (!stream.getOriginalStream()) {
logger.debug('Remote video stream has no original stream');

View File

@@ -83,10 +83,12 @@ export default class SmallVideo {
constructor(VideoLayout) {
this.isAudioMuted = false;
this.isVideoMuted = false;
this.isScreenSharing = false;
this.videoStream = null;
this.audioStream = null;
this.VideoLayout = VideoLayout;
this.videoIsHovered = false;
this.videoType = undefined;
/**
* The current state of the user's bridge connection. The value should be
@@ -234,6 +236,18 @@ export default class SmallVideo {
this.updateStatusBar();
}
/**
* Shows / hides the screen-share indicator over small videos.
*
* @param {boolean} isScreenSharing indicates if the screen-share element should be shown
* or hidden
*/
setScreenSharing(isScreenSharing) {
this.isScreenSharing = isScreenSharing;
this.updateView();
this.updateStatusBar();
}
/**
* Shows video muted indicator over small videos and disables/enables avatar
* if video muted.
@@ -265,6 +279,7 @@ export default class SmallVideo {
<I18nextProvider i18n = { i18next }>
<StatusIndicators
showAudioMutedIndicator = { this.isAudioMuted }
showScreenShareIndicator = { this.isScreenSharing }
showVideoMutedIndicator = { this.isVideoMuted }
participantID = { this.id } />
</I18nextProvider>
@@ -450,8 +465,10 @@ export default class SmallVideo {
* or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
*/
selectDisplayMode(input) {
// Display name is always and only displayed when user is on the stage
if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
if (!input.tileViewActive && input.isScreenSharing) {
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
} else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
// Display name is always and only displayed when user is on the stage
return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
} else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
// check hovering and change state to video with name
@@ -480,6 +497,7 @@ export default class SmallVideo {
canPlayEventReceived: this._canPlayEventReceived,
videoStream: Boolean(this.videoStream),
isVideoMuted: this.isVideoMuted,
isScreenSharing: this.isScreenSharing,
videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
};
}

View File

@@ -177,6 +177,7 @@ const VideoLayout = {
this.onAudioMute(id, stream.isMuted());
} else {
this.onVideoMute(id, stream.isMuted());
remoteVideo.setScreenSharing(stream.videoType === 'desktop');
}
},
@@ -188,6 +189,7 @@ const VideoLayout = {
if (remoteVideo) {
remoteVideo.removeRemoteStreamElement(stream);
remoteVideo.setScreenSharing(false);
}
this.updateMutedForNoTracks(id, stream.getType());
@@ -485,13 +487,14 @@ const VideoLayout = {
},
onVideoTypeChanged(id, newVideoType) {
if (VideoLayout.getRemoteVideoType(id) === newVideoType) {
const remoteVideo = remoteVideos[id];
if (!remoteVideo || remoteVideo.videoType === newVideoType) {
return;
}
logger.info('Peer video type changed: ', id, newVideoType);
this._updateLargeVideoIfDisplayed(id, true);
remoteVideo.setScreenSharing(newVideoType === 'desktop');
},
/**

7491
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@
"@tensorflow-models/body-pix": "2.0.4",
"@tensorflow/tfjs": "1.5.1",
"@webcomponents/url": "0.7.1",
"amplitude-js": "4.5.2",
"amplitude-js": "7.1.1",
"base64-js": "1.3.1",
"bc-css-flags": "3.0.0",
"dropbox": "4.0.9",
@@ -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#94318fce12c855aefefdf8586bc8772065b505c9",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -69,7 +69,7 @@
"react-linkify": "1.0.0-alpha",
"react-native": "github:jitsi/react-native#efd2aff5661d75a230e36406b698cfe0ee545be2",
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#928a80e2ffef0d7e84936d7e7e0acc4f53ee8470",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#df48ecdc4e1e90c5352f803ddbab1fa7269b74a7",
"react-native-callstats": "3.61.0",
"react-native-collapsible": "1.5.1",
"react-native-default-preference": "1.4.2",
@@ -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,
@@ -23,6 +24,7 @@ import {
parseURIString,
toURLString
} from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
import { clearNotifications, showNotification } from '../notifications';
import { setFatalError } from '../overlay';
@@ -168,9 +170,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 +188,10 @@ export function redirectToStaticPage(pathname: string) {
newPathname = getLocationContextRoot(windowLocation) + newPathname;
}
if (hashParam) {
windowLocation.hash = hashParam;
}
windowLocation.pathname = newPathname;
};
}
@@ -284,8 +292,14 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
// if close page is enabled redirect to it, without further action
if (enableClosePage) {
if (isVpaasMeeting(getState())) {
redirectToStaticPage('/');
}
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
window.sessionStorage.setItem('guest', isGuest);
@@ -294,12 +308,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

@@ -18,6 +18,7 @@ import '../base/sounds/middleware';
import '../base/testing/middleware';
import '../base/tracks/middleware';
import '../base/user-interaction/middleware';
import '../billing-counter/middleware';
import '../calendar-sync/middleware';
import '../chat/middleware';
import '../conference/middleware';

View File

@@ -30,6 +30,7 @@ import '../chat/reducer';
import '../deep-linking/reducer';
import '../device-selection/reducer';
import '../dropbox/reducer';
import '../dynamic-branding/reducer';
import '../etherpad/reducer';
import '../filmstrip/reducer';
import '../follow-me/reducer';

View File

@@ -2,7 +2,6 @@
import React, { PureComponent } from 'react';
import { IconShareDesktop } from '../../icons';
import { getParticipantById } from '../../participants';
import { connect } from '../../redux';
import { getAvatarColor, getInitials } from '../functions';
@@ -192,17 +191,10 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
const { colorBase, displayName, participantId } = ownProps;
const _participant: ?Object = participantId && getParticipantById(state, participantId);
const _initialsBase = _participant?.name ?? displayName;
const screenShares = state['features/video-layout'].screenShares || [];
let _loadableAvatarUrl = _participant?.loadableAvatarUrl;
if (participantId && screenShares.includes(participantId)) {
_loadableAvatarUrl = IconShareDesktop;
}
return {
_initialsBase,
_loadableAvatarUrl,
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
colorBase: !colorBase && _participant ? _participant.id : colorBase
};
}

View File

@@ -163,19 +163,6 @@ export const SET_DESKTOP_SHARING_ENABLED
*/
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
/**
* The type of (redux) action which sets the maximum video height that should be
* received from remote participants, even if the user prefers a larger video
* height.
*
* {
* type: SET_MAX_RECEIVER_VIDEO_QUALITY,
* maxReceiverVideoQuality: number
* }
*/
export const SET_MAX_RECEIVER_VIDEO_QUALITY
= 'SET_MAX_RECEIVER_VIDEO_QUALITY';
/**
* The type of (redux) action which sets the password to join or lock a specific
* {@code JitsiConference}.
@@ -210,17 +197,6 @@ export const SET_PASSWORD_FAILED = 'SET_PASSWORD_FAILED';
*/
export const SET_PENDING_SUBJECT_CHANGE = 'SET_PENDING_SUBJECT_CHANGE';
/**
* The type of (redux) action which sets the preferred maximum video height that
* should be sent to and received from remote participants.
*
* {
* type: SET_PREFERRED_VIDEO_QUALITY,
* preferredVideoQuality: number
* }
*/
export const SET_PREFERRED_VIDEO_QUALITY = 'SET_PREFERRED_VIDEO_QUALITY';
/**
* The type of (redux) action which sets the name of the room of the
* conference to be joined.

View File

@@ -45,10 +45,8 @@ import {
SEND_TONES,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_MAX_RECEIVER_VIDEO_QUALITY,
SET_PASSWORD,
SET_PASSWORD_FAILED,
SET_PREFERRED_VIDEO_QUALITY,
SET_ROOM,
SET_PENDING_SUBJECT_CHANGE,
SET_START_MUTED_POLICY
@@ -615,23 +613,6 @@ export function setFollowMe(enabled: boolean) {
};
}
/**
* Sets the max frame height that should be received from remote videos.
*
* @param {number} maxReceiverVideoQuality - The max video frame height to
* receive.
* @returns {{
* type: SET_MAX_RECEIVER_VIDEO_QUALITY,
* maxReceiverVideoQuality: number
* }}
*/
export function setMaxReceiverVideoQuality(maxReceiverVideoQuality: number) {
return {
type: SET_MAX_RECEIVER_VIDEO_QUALITY,
maxReceiverVideoQuality
};
}
/**
* Sets the password to join or lock a specific JitsiConference.
*
@@ -698,24 +679,6 @@ export function setPassword(
};
}
/**
* Sets the max frame height the user prefers to send and receive from the
* remote participants.
*
* @param {number} preferredVideoQuality - The max video resolution to send and
* receive.
* @returns {{
* type: SET_PREFERRED_VIDEO_QUALITY,
* preferredVideoQuality: number
* }}
*/
export function setPreferredVideoQuality(preferredVideoQuality: number) {
return {
type: SET_PREFERRED_VIDEO_QUALITY,
preferredVideoQuality
};
}
/**
* Sets (the name of) the room of the conference to be joined.
*

View File

@@ -34,15 +34,3 @@ export const EMAIL_COMMAND = 'email';
* from the outside is not cool but it should suffice for now.
*/
export const JITSI_CONFERENCE_URL_KEY = Symbol('url');
/**
* The supported remote video resolutions. The values are currently based on
* available simulcast layers.
*
* @type {object}
*/
export const VIDEO_QUALITY_LEVELS = {
HIGH: 720,
STANDARD: 360,
LOW: 180
};

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

@@ -20,7 +20,7 @@ import {
PARTICIPANT_UPDATED,
PIN_PARTICIPANT
} from '../participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import { MiddlewareRegistry } from '../redux';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
@@ -28,7 +28,6 @@ import {
CONFERENCE_JOINED,
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
SEND_TONES,
SET_PENDING_SUBJECT_CHANGE,
SET_ROOM
@@ -81,9 +80,6 @@ MiddlewareRegistry.register(store => next => action => {
_conferenceWillLeave();
break;
case DATA_CHANNEL_OPENED:
return _syncReceiveVideoQuality(store, next, action);
case PARTICIPANT_UPDATED:
return _updateLocalParticipantInConference(store, next, action);
@@ -104,31 +100,6 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Registers a change handler for state['features/base/conference'] to update
* the preferred video quality levels based on user preferred and internal
* settings.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'],
/* listener */ (currentState, store, previousState = {}) => {
const {
conference,
maxReceiverVideoQuality,
preferredVideoQuality
} = currentState;
const changedConference = conference !== previousState.conference;
const changedPreferredVideoQuality
= preferredVideoQuality !== previousState.preferredVideoQuality;
const changedMaxVideoQuality = maxReceiverVideoQuality !== previousState.maxReceiverVideoQuality;
if (changedConference || changedPreferredVideoQuality || changedMaxVideoQuality) {
_setReceiverVideoConstraint(conference, preferredVideoQuality, maxReceiverVideoQuality);
}
if (changedConference || changedPreferredVideoQuality) {
_setSenderVideoConstraint(conference, preferredVideoQuality);
}
});
/**
* Makes sure to leave a failed conference in order to release any allocated
@@ -448,44 +419,6 @@ function _sendTones({ getState }, next, action) {
return next(action);
}
/**
* Helper function for updating the preferred receiver video constraint, based
* on the user preference and the internal maximum.
*
* @param {JitsiConference} conference - The JitsiConference instance for the
* current call.
* @param {number} preferred - The user preferred max frame height.
* @param {number} max - The maximum frame height the application should
* receive.
* @returns {void}
*/
function _setReceiverVideoConstraint(conference, preferred, max) {
if (conference) {
const value = Math.min(preferred, max);
conference.setReceiverVideoConstraint(value);
logger.info(`setReceiverVideoConstraint: ${value}`);
}
}
/**
* Helper function for updating the preferred sender video constraint, based
* on the user preference.
*
* @param {JitsiConference} conference - The JitsiConference instance for the
* current call.
* @param {number} preferred - The user preferred max frame height.
* @returns {void}
*/
function _setSenderVideoConstraint(conference, preferred) {
if (conference) {
conference.setSenderVideoConstraint(preferred)
.catch(err => {
logger.error(`Changing sender resolution to ${preferred} failed - ${err} `);
});
}
}
/**
* Notifies the feature base/conference that the action
* {@code SET_ROOM} is being dispatched within a specific
@@ -539,33 +472,6 @@ function _syncConferenceLocalTracksWithState({ getState }, action) {
return promise || Promise.resolve();
}
/**
* Sets the maximum receive video quality.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} to the specified {@code store}.
* @param {Action} action - The redux action {@code DATA_CHANNEL_STATUS_CHANGED}
* which is being dispatched in the specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _syncReceiveVideoQuality({ getState }, next, action) {
const {
conference,
maxReceiverVideoQuality,
preferredVideoQuality
} = getState()['features/base/conference'];
_setReceiverVideoConstraint(
conference,
preferredVideoQuality,
maxReceiverVideoQuality);
return next(action);
}
/**
* Notifies the feature base/conference that the action {@code TRACK_ADDED}
* or {@code TRACK_REMOVED} is being dispatched within a specific redux store.
@@ -624,7 +530,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

@@ -18,15 +18,12 @@ import {
P2P_STATUS_CHANGED,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_MAX_RECEIVER_VIDEO_QUALITY,
SET_PASSWORD,
SET_PENDING_SUBJECT_CHANGE,
SET_PREFERRED_VIDEO_QUALITY,
SET_ROOM,
SET_SIP_GATEWAY_ENABLED,
SET_START_MUTED_POLICY
} from './actionTypes';
import { VIDEO_QUALITY_LEVELS } from './constants';
import { isRoomValid } from './functions';
const DEFAULT_STATE = {
@@ -35,11 +32,9 @@ const DEFAULT_STATE = {
joining: undefined,
leaving: undefined,
locked: undefined,
maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
membersOnly: undefined,
password: undefined,
passwordRequired: undefined,
preferredVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
passwordRequired: undefined
};
/**
@@ -90,24 +85,12 @@ ReducerRegistry.register(
case SET_LOCATION_URL:
return set(state, 'room', undefined);
case SET_MAX_RECEIVER_VIDEO_QUALITY:
return set(
state,
'maxReceiverVideoQuality',
action.maxReceiverVideoQuality);
case SET_PASSWORD:
return _setPassword(state, action);
case SET_PENDING_SUBJECT_CHANGE:
return set(state, 'pendingSubjectChange', action.subject);
case SET_PREFERRED_VIDEO_QUALITY:
return set(
state,
'preferredVideoQuality',
action.preferredVideoQuality);
case SET_ROOM:
return _setRoom(state, action);

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

@@ -69,6 +69,7 @@ export default [
'channelLastN',
'constraints',
'brandingRoomAlias',
'debug',
'debugAudioLevels',
'defaultLanguage',
@@ -100,12 +101,14 @@ export default [
'enableInsecureRoomNameWarning',
'enableLayerSuspension',
'enableLipSync',
'enableOpusRed',
'enableRemb',
'enableScreenshotCapture',
'enableTalkWhileMuted',
'enableNoAudioDetection',
'enableNoisyMicDetection',
'enableTcc',
'enableAutomaticUrlCopy',
'etherpad_base',
'failICE',
'feedbackPercentage',
@@ -115,6 +118,7 @@ export default [
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hideLobbyButton',
'hosts',
'iAmRecorder',
'iAmSipGateway',
@@ -148,6 +152,7 @@ export default [
'testing',
'useStunTurn',
'useTurnUdp',
'videoQuality.persist',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable'
].concat(extraConfigWhitelist);

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

@@ -54,7 +54,17 @@ export function getInviteURL(stateOrGetState: Function | Object): string {
throw new Error('Can not get invite URL - the app is not ready');
}
return getURLWithoutParams(locationURL).href;
const { inviteDomain } = state['features/dynamic-branding'];
const urlWithoutParams = getURLWithoutParams(locationURL);
if (inviteDomain) {
const meetingId
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname;
return `${inviteDomain}/${meetingId}`;
}
return urlWithoutParams.href;
}
/**

View File

@@ -5,7 +5,6 @@ import { processExternalDeviceRequest } from '../../device-selection';
import { showNotification, showWarningNotification } from '../../notifications';
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { CONFERENCE_JOINED } from '../conference';
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { updateSettings } from '../settings';
@@ -24,6 +23,7 @@ import {
setVideoInputDevice
} from './actions';
import {
areDeviceLabelsInitialized,
formatDeviceLabel,
groupDevicesByKind,
setAudioOutputDeviceId
@@ -73,8 +73,6 @@ function logDeviceList(deviceList) {
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case NOTIFY_CAMERA_ERROR: {
if (typeof APP !== 'object' || !action.error) {
break;
@@ -148,6 +146,9 @@ MiddlewareRegistry.register(store => next => action => {
break;
case UPDATE_DEVICE_LIST:
logDeviceList(groupDevicesByKind(action.devices));
if (areDeviceLabelsInitialized(store.getState())) {
return _processPendingRequests(store, next, action);
}
break;
case CHECK_AND_NOTIFY_FOR_NEW_DEVICE:
_checkAndNotifyForNewDevice(store, action.newDevices, action.oldDevices);
@@ -170,11 +171,15 @@ MiddlewareRegistry.register(store => next => action => {
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _conferenceJoined({ dispatch, getState }, next, action) {
function _processPendingRequests({ dispatch, getState }, next, action) {
const result = next(action);
const state = getState();
const { pendingRequests } = state['features/base/devices'];
if (!pendingRequests || pendingRequests.length === 0) {
return result;
}
pendingRequests.forEach(request => {
processExternalDeviceRequest(
dispatch,

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,61 @@
// @flow
import React from 'react';
import { Avatar } from '../../../avatar';
import { connect } from '../../../redux';
import { calculateAvatarDimensions } from '../../functions';
type Props = {
/**
* The height of the window.
*/
height: number,
/**
* The name of the participant (if any).
*/
name: string
}
/**
* Component displaying the avatar for the premeeting screen.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
function PremeetingAvatar({ height, name }: Props) {
const { marginTop, size } = calculateAvatarDimensions(height);
if (size <= 5) {
return null;
}
return (
<div style = {{ marginTop }}>
<Avatar
className = 'preview-avatar'
displayName = { name }
participantId = 'local'
size = { size } />
</div>
);
}
/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {{
* height: number
* }}
*/
function mapStateToProps(state) {
return {
height: state['features/base/responsive-ui'].clientHeight
};
}
export default connect(mapStateToProps)(PremeetingAvatar);

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

@@ -18,7 +18,13 @@ type Props = {
/**
* Used for translation.
*/
t: Function
t: Function,
/**
* Used to determine if invitation link should be automatically copied
* after creating a meeting.
*/
_enableAutomaticUrlCopy: boolean,
};
type State = {
@@ -58,6 +64,7 @@ class CopyMeetingUrl extends Component<Props, State> {
this._hideLinkCopied = this._hideLinkCopied.bind(this);
this._showCopyLink = this._showCopyLink.bind(this);
this._showLinkCopied = this._showLinkCopied.bind(this);
this._copyUrlAutomatically = this._copyUrlAutomatically.bind(this);
}
_copyUrl: () => void;
@@ -135,6 +142,37 @@ class CopyMeetingUrl extends Component<Props, State> {
});
}
_copyUrlAutomatically: () => void;
/**
* Attempts to automatically copy invitation URL.
* Document has to be focused in order for this to work.
*
* @private
* @returns {void}
*/
_copyUrlAutomatically() {
navigator.clipboard.writeText(this.props.url)
.then(() => {
this._showLinkCopied();
window.setTimeout(this._hideLinkCopied, COPY_TIMEOUT);
});
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately before mounting occurs.
*
* @inheritdoc
*/
componentDidMount() {
const { _enableAutomaticUrlCopy } = this.props;
if (_enableAutomaticUrlCopy) {
setTimeout(this._copyUrlAutomatically, 2000);
}
}
/**
* Implements React's {@link Component#render()}.
*
@@ -177,8 +215,11 @@ class CopyMeetingUrl extends Component<Props, State> {
* @returns {Object}
*/
function mapStateToProps(state) {
const { enableAutomaticUrlCopy } = state['features/base/config'];
return {
url: getCurrentConferenceUrl(state)
url: getCurrentConferenceUrl(state),
_enableAutomaticUrlCopy: enableAutomaticUrlCopy || false
};
}

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

@@ -2,11 +2,12 @@
import React from 'react';
import { Avatar } from '../../../avatar';
import { Video } from '../../../media';
import { connect } from '../../../redux';
import { getLocalVideoTrack } from '../../../tracks';
import PreviewAvatar from './Avatar';
export type Props = {
/**
@@ -54,13 +55,7 @@ function Preview(props: Props) {
<div
className = 'no-video'
id = 'preview'>
<div className = 'preview-avatar-container'>
<Avatar
className = 'preview-avatar'
displayName = { name }
participantId = 'local'
size = { 200 } />
</div>
<PreviewAvatar name = { name } />
</div>
);
}

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,213 @@
// @flow
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 ];
/**
* The avatar size to container size ration.
*/
const ratio = 1 / 3;
/**
* The max avatar size.
*/
const maxSize = 190;
/**
* The window limit hight over which the avatar should have the default dimension.
*/
const upperHeightLimit = 760;
/**
* The window limit hight under which the avatar should not be resized anymore.
*/
const lowerHeightLimit = 460;
/**
* The default top margin of the avatar.
*/
const defaultMarginTop = '10%';
/**
* The top margin of the avatar when its dimension is small.
*/
const smallMarginTop = '5%';
/**
* Calculates avatar dimensions based on window height and position.
*
* @param {number} height - The window height.
* @returns {{
* marginTop: string,
* size: number
* }}
*/
export function calculateAvatarDimensions(height: number) {
if (height > upperHeightLimit) {
return {
size: maxSize,
marginTop: defaultMarginTop
};
}
if (height > lowerHeightLimit) {
const diff = height - lowerHeightLimit;
const percent = diff * ratio;
const size = Math.floor(maxSize * percent / 100);
let marginTop = defaultMarginTop;
if (height < 600) {
marginTop = smallMarginTop;
}
return {
size,
marginTop
};
}
return {
size: 0,
marginTop: '0'
};
}
/**
* 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: Object) {
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

@@ -2,9 +2,11 @@
import React, { Component } from 'react';
import { isVpaasMeeting } from '../../../../billing-counter/functions';
import { translate } from '../../../i18n';
import { connect } from '../../../redux';
declare var interfaceConfig: Object;
/**
@@ -36,6 +38,11 @@ type Props = {
*/
_isGuest: boolean,
/**
* Whether or not the current meeting is a vpaas one.
*/
_isVpaas: boolean,
/**
* Flag used to signal that the logo can be displayed.
* It becomes true after the user customization options are fetched.
@@ -181,6 +188,33 @@ class Watermarks extends Component<Props, State> {
|| _welcomePageIsVisible;
}
/**
* Returns the background image style.
*
* @private
* @returns {string}
*/
_getBackgroundImageStyle() {
const {
_customLogoUrl,
_isVpaas,
defaultJitsiLogoURL
} = this.props;
let style = 'none';
if (_isVpaas) {
if (_customLogoUrl) {
style = `url(${_customLogoUrl})`;
}
} else {
style = `url(${_customLogoUrl
|| defaultJitsiLogoURL
|| interfaceConfig.DEFAULT_LOGO_URL})`;
}
return style;
}
/**
* Renders a brand watermark if it is enabled.
*
@@ -221,18 +255,22 @@ class Watermarks extends Component<Props, State> {
*/
_renderJitsiWatermark() {
let reactElement = null;
const {
_customLogoUrl,
_customLogoLink,
defaultJitsiLogoURL
} = this.props;
if (this._canDisplayJitsiWatermark()) {
const link = _customLogoLink || this.state.jitsiWatermarkLink;
const backgroundImage = this._getBackgroundImageStyle();
const link = this.props._customLogoLink || this.state.jitsiWatermarkLink;
const additionalStyles = {};
if (backgroundImage === 'none') {
additionalStyles.height = 0;
additionalStyles.width = 0;
}
const style = {
backgroundImage: `url(${_customLogoUrl || defaultJitsiLogoURL || interfaceConfig.DEFAULT_LOGO_URL})`,
backgroundImage,
maxWidth: 140,
maxHeight: 70
maxHeight: 70,
...additionalStyles
};
reactElement = (<div
@@ -299,6 +337,7 @@ function _mapStateToProps(state) {
_customLogoLink: logoClickUrl,
_customLogoUrl: logoImageUrl,
_isGuest: isGuest,
_isVpaas: isVpaasMeeting(state),
_readyToDisplayJitsiWatermark: customizationReady,
_welcomePageIsVisible: !room
};

View File

@@ -9,6 +9,7 @@ import { MiddlewareRegistry } from '../redux';
import { parseURLParams } from '../util';
import { SETTINGS_UPDATED } from './actionTypes';
import { updateSettings } from './actions';
import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
/**
@@ -160,10 +161,18 @@ function _updateLocalParticipantFromUrl({ dispatch, getState }) {
const localParticipant = getLocalParticipant(getState());
if (localParticipant) {
const displayName = _.escape(urlDisplayName);
const email = _.escape(urlEmail);
dispatch(participantUpdated({
...localParticipant,
email: _.escape(urlEmail),
name: _.escape(urlDisplayName)
email,
name: displayName
}));
dispatch(updateSettings({
displayName,
email
}));
}
}

View File

@@ -0,0 +1,4 @@
/**
* Action used to store the billing id.
*/
export const SET_BILLING_ID = 'SET_BILLING_ID';

View File

@@ -0,0 +1,51 @@
// @flow
import uuid from 'uuid';
import { SET_BILLING_ID } from './actionTypes';
import { extractVpaasTenantFromPath, getBillingId, sendCountRequest } from './functions';
/**
* Sends a billing count request when needed.
* If there is no billingId, it presists one first and sends the request after.
*
* @returns {Function}
*/
export function countEndpoint() {
return function(dispatch: Function, getState: Function) {
const state = getState();
const baseUrl = state['features/base/config'].billingCounterUrl;
const jwt = state['features/base/jwt'].jwt;
const tenant = extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
const shouldSendRequest = Boolean(baseUrl && jwt && tenant);
if (shouldSendRequest) {
let billingId = getBillingId();
if (!billingId) {
billingId = uuid.v4();
dispatch(setBillingId(billingId));
}
sendCountRequest({
baseUrl,
billingId,
jwt,
tenant
});
}
};
}
/**
* Action used to set the user billing id.
*
* @param {string} value - The uid.
* @returns {Object}
*/
function setBillingId(value) {
return {
type: SET_BILLING_ID,
value
};
}

View File

@@ -0,0 +1,9 @@
/**
* The key for the billing id stored in localStorage.
*/
export const BILLING_ID = 'billingId';
/**
* The prefix for the vpaas tenant.
*/
export const VPAAS_TENANT_PREFIX = 'vpaas-magic-cookie';

View File

@@ -0,0 +1,91 @@
// @flow
import { jitsiLocalStorage } from '@jitsi/js-utils';
import { BILLING_ID, VPAAS_TENANT_PREFIX } from './constants';
import logger from './logger';
/**
* Returns the full vpaas tenant if available, given a path.
*
* @param {string} path - The meeting url path.
* @returns {string}
*/
export function extractVpaasTenantFromPath(path: string) {
const [ , tenant ] = path.split('/');
if (tenant.startsWith(VPAAS_TENANT_PREFIX)) {
return tenant;
}
return '';
}
/**
* Returns true if the current meeting is a vpaas one.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isVpaasMeeting(state: Object) {
return Boolean(
state['features/base/config'].billingCounterUrl
&& state['features/base/jwt'].jwt
&& extractVpaasTenantFromPath(
state['features/base/connection'].locationURL.pathname)
);
}
/**
* Sends a billing counter request.
*
* @param {Object} reqData - The request info.
* @param {string} reqData.baseUrl - The base url for the request.
* @param {string} billingId - The unique id of the client.
* @param {string} jwt - The JWT token.
* @param {string} tenat - The client tenant.
* @returns {void}
*/
export async function sendCountRequest({ baseUrl, billingId, jwt, tenant }: {
baseUrl: string,
billingId: string,
jwt: string,
tenant: string
}) {
const fullUrl = `${baseUrl}/${encodeURIComponent(tenant)}/${billingId}`;
const headers = {
'Authorization': `Bearer ${jwt}`
};
try {
const res = await fetch(fullUrl, {
method: 'GET',
headers
});
if (!res.ok) {
logger.error('Status error:', res.status);
}
} catch (err) {
logger.error('Could not send request', err);
}
}
/**
* Returns the stored billing id.
*
* @returns {string}
*/
export function getBillingId() {
return jitsiLocalStorage.getItem(BILLING_ID);
}
/**
* Stores the billing id.
*
* @param {string} value - The id to be stored.
* @returns {void}
*/
export function setBillingId(value: string) {
jitsiLocalStorage.setItem(BILLING_ID, value);
}

View File

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

View File

@@ -0,0 +1,31 @@
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { MiddlewareRegistry } from '../base/redux';
import { SET_BILLING_ID } from './actionTypes';
import { countEndpoint } from './actions';
import { setBillingId } from './functions';
/**
* The redux middleware for billing counter.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => async action => {
switch (action.type) {
case SET_BILLING_ID: {
setBillingId(action.value);
break;
}
case CONFERENCE_JOINED: {
store.dispatch(countEndpoint());
break;
}
}
return next(action);
});

View File

@@ -16,6 +16,7 @@ import { translate } from '../../base/i18n';
import { Icon, IconClose } from '../../base/icons';
import { browser } from '../../base/lib-jitsi-meet';
import { connect } from '../../base/redux';
import { isVpaasMeeting } from '../../billing-counter/functions';
import logger from '../logger';
@@ -50,6 +51,11 @@ type Props = {
*/
iAmRecorder: boolean,
/**
* Whether it's a vpaas meeting or not.
*/
isVpaas: boolean,
/**
* Invoked to obtain translated strings.
*/
@@ -146,7 +152,8 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
_isSupportedEnvironment() {
return interfaceConfig.SHOW_CHROME_EXTENSION_BANNER
&& browser.isChrome()
&& !isMobileBrowser();
&& !isMobileBrowser()
&& !this.props.isVpaas;
}
_onClosePressed: () => void;
@@ -280,7 +287,8 @@ const _mapStateToProps = state => {
// Using emptyObject so that we don't change the reference every time when _mapStateToProps is called.
bannerCfg: state['features/base/config'].chromeExtensionBanner || emptyObject,
conference: getCurrentConference(state),
iAmRecorder: state['features/base/config'].iAmRecorder
iAmRecorder: state['features/base/config'].iAmRecorder,
isVpaas: isVpaasMeeting(state)
};
};

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

@@ -3,6 +3,7 @@
import { isMobileBrowser } from '../base/environment/utils';
import { Platform } from '../base/react';
import { URI_PROTOCOL_PATTERN } from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
import {
DeepLinkingDesktopPage,
@@ -53,7 +54,7 @@ export function getDeepLinkingPage(state) {
const { launchInWeb } = state['features/deep-linking'];
// Show only if we are about to join a conference.
if (launchInWeb || !room || state['features/base/config'].disableDeepLinking) {
if (launchInWeb || !room || state['features/base/config'].disableDeepLinking || isVpaasMeeting(state)) {
return Promise.resolve();
}

View File

@@ -86,7 +86,6 @@ export function processExternalDeviceRequest( // eslint-disable-line max-params
}
const state = getState();
const settings = state['features/base/settings'];
const { conference } = state['features/base/conference'];
let result = true;
switch (request.name) {
@@ -165,7 +164,7 @@ export function processExternalDeviceRequest( // eslint-disable-line max-params
case 'setDevice': {
const { device } = request;
if (!conference) {
if (!areDeviceLabelsInitialized(state)) {
dispatch(addPendingDeviceRequest({
type: 'devices',
name: 'setDevice',

View File

@@ -53,7 +53,6 @@ function setDynamicBrandingData(value) {
};
}
/**
* Action used to signal the branding elements are ready to be displayed.
*

View File

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

View File

@@ -14,6 +14,7 @@ const DEFAULT_STATE = {
backgroundColor: '',
backgroundImageUrl: '',
customizationReady: false,
inviteDomain: '',
logoClickUrl: '',
logoImageUrl: ''
};
@@ -24,11 +25,12 @@ const DEFAULT_STATE = {
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_DYNAMIC_BRANDING_DATA: {
const { backgroundColor, backgroundImageUrl, logoClickUrl, logoImageUrl } = action.value;
const { backgroundColor, backgroundImageUrl, inviteDomain, logoClickUrl, logoImageUrl } = action.value;
return {
backgroundColor,
backgroundImageUrl,
inviteDomain,
logoClickUrl,
logoImageUrl,
customizationReady: true

View File

@@ -0,0 +1,19 @@
// @flow
import React from 'react';
import { IconShareDesktop } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
/**
* Thumbnail badge for displaying if a participant is sharing their screen.
*
* @returns {React$Element<any>}
*/
export default function ScreenShareIndicator() {
return (
<BaseIndicator
highlight = { false }
icon = { IconShareDesktop } />
);
}

View File

@@ -27,6 +27,7 @@ import AudioMutedIndicator from './AudioMutedIndicator';
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
import ModeratorIndicator from './ModeratorIndicator';
import RaisedHandIndicator from './RaisedHandIndicator';
import ScreenShareIndicator from './ScreenShareIndicator';
import VideoMutedIndicator from './VideoMutedIndicator';
import styles, { AVATAR_SIZE } from './styles';
@@ -186,9 +187,10 @@ function Thumbnail(props: Props) {
{ !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
{ audioMuted
&& <AudioMutedIndicator /> }
{ videoMuted
&& <VideoMutedIndicator /> }
{ isScreenShare
&& <ScreenShareIndicator /> }
</Container> }
</Container>

View File

@@ -0,0 +1,33 @@
// @flow
import React from 'react';
import { IconShareDesktop } from '../../../base/icons';
import { BaseIndicator } from '../../../base/react';
type Props = {
/**
* From which side of the indicator the tooltip should appear from.
*/
tooltipPosition: string
};
/**
* React {@code Component} for showing a screen-sharing icon with a tooltip.
*
* @param {Props} props - React props passed to this component.
* @returns {React$Element<any>}
*/
export default function ScreenShareIndicator(props: Props) {
return (
<BaseIndicator
className = 'screenShare toolbar-icon'
icon = { IconShareDesktop }
iconId = 'share-desktop'
iconSize = { 13 }
tooltipKey = 'videothumbnail.videomute'
tooltipPosition = { props.tooltipPosition } />
);
}

View File

@@ -8,6 +8,7 @@ import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import AudioMutedIndicator from './AudioMutedIndicator';
import ModeratorIndicator from './ModeratorIndicator';
import ScreenShareIndicator from './ScreenShareIndicator';
import VideoMutedIndicator from './VideoMutedIndicator';
declare var interfaceConfig: Object;
@@ -32,6 +33,11 @@ type Props = {
*/
showAudioMutedIndicator: Boolean,
/**
* Indicates if the screen share indicator should be visible or not.
*/
showScreenShareIndicator: Boolean,
/**
* Indicates if the video muted indicator should be visible or not.
*/
@@ -60,6 +66,7 @@ class StatusIndicators extends Component<Props> {
_currentLayout,
_showModeratorIndicator,
showAudioMutedIndicator,
showScreenShareIndicator,
showVideoMutedIndicator
} = this.props;
let tooltipPosition;
@@ -78,6 +85,7 @@ class StatusIndicators extends Component<Props> {
return (
<div>
{ showAudioMutedIndicator ? <AudioMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
{ showScreenShareIndicator ? <ScreenShareIndicator tooltipPosition = { tooltipPosition } /> : null }
{ showVideoMutedIndicator ? <VideoMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
{ _showModeratorIndicator ? <ModeratorIndicator tooltipPosition = { tooltipPosition } /> : null }
</div>

View File

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

View File

@@ -9,6 +9,7 @@ import {
getLocalParticipant
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { isTestModeEnabled } from '../base/testing';
import {
getTrackByJitsiTrack,
TRACK_ADDED,
@@ -17,6 +18,7 @@ import {
} from '../base/tracks';
import { selectParticipant, selectParticipantInLargeVideo } from './actions';
import logger from './logger';
import './subscriber';
@@ -32,7 +34,12 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case DOMINANT_SPEAKER_CHANGED: {
const localParticipant = getLocalParticipant(store.getState());
const state = store.getState();
const localParticipant = getLocalParticipant(state);
if (isTestModeEnabled(state)) {
logger.info(`Dominant speaker changed event for: ${action.participant.id}`);
}
if (localParticipant && localParticipant.id !== action.participant.id) {
store.dispatch(selectParticipantInLargeVideo());

View File

@@ -66,11 +66,12 @@ class LobbyModeButton extends AbstractButton<Props, any> {
export function _mapStateToProps(state: Object): $Shape<Props> {
const conference = getCurrentConference(state);
const { lobbyEnabled } = state['features/lobby'];
const { hideLobbyButton } = state['features/base/config'];
const lobbySupported = conference && conference.isLobbySupported();
return {
lobbyEnabled,
visible: lobbySupported && isLocalParticipantModerator(state)
visible: lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton
};
}

View File

@@ -132,10 +132,12 @@ class LobbySection extends PureComponent<Props, State> {
*/
function mapStateToProps(state: Object): $Shape<Props> {
const { conference } = state['features/base/conference'];
const { hideLobbyButton } = state['features/base/config'];
return {
_lobbyEnabled: state['features/lobby'].lobbyEnabled,
_visible: conference && conference.isLobbySupported() && isLocalParticipantModerator(state)
&& !hideLobbyButton
};
}

View File

@@ -264,6 +264,11 @@ function _conferenceWillJoin({ dispatch, getState }, next, action) {
const handle = callHandle || url.toString();
const hasVideo = !isVideoMutedByAudioOnly(state);
// If we already have a callUUID set, don't start a new call.
if (conference.callUUID) {
return result;
}
// When assigning the call UUID, do so in upper case, since iOS will return
// it upper cased.
conference.callUUID = (callUUID || uuid.v4()).toUpperCase();

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,12 +1,22 @@
// @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';
import { getLocalVideoTrack, replaceLocalTrack } from '../base/tracks';
import {
getLocalVideoTrack,
replaceLocalTrack,
TRACK_ADDED,
TRACK_NO_DATA_FROM_SOURCE
} from '../base/tracks';
import { PREJOIN_START_CONFERENCE } from './actionTypes';
import { setPrejoinPageVisibility } from './actions';
import {
setDeviceStatusOk,
setDeviceStatusWarning,
setPrejoinPageVisibility
} from './actions';
import { isPrejoinPageVisible } from './functions';
declare var APP: Object;
@@ -24,6 +34,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
@@ -59,6 +72,30 @@ MiddlewareRegistry.register(store => next => async action => {
break;
}
case TRACK_ADDED:
case TRACK_NO_DATA_FROM_SOURCE: {
const state = store.getState();
if (isPrejoinPageVisible(state)) {
const { track: { jitsiTrack: track } } = action;
const { deviceStatusType, deviceStatusText } = state['features/prejoin'];
if (!track.isAudioTrack()) {
break;
}
if (track.isReceivingData()) {
if (deviceStatusType === 'warning'
&& deviceStatusText === 'prejoin.audioDeviceProblem') {
store.dispatch(setDeviceStatusOk('prejoin.lookGood'));
}
} else if (deviceStatusType === 'ok') {
store.dispatch(setDeviceStatusWarning('prejoin.audioDeviceProblem'));
}
}
break;
}
}
return next(action);

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,
@@ -61,10 +68,12 @@ ReducerRegistry.register(
}
case SET_DEVICE_STATUS: {
const { deviceStatusType, deviceStatusText } = action.value;
return {
...state,
deviceStatusText: action.text,
deviceStatusType: action.type
deviceStatusText,
deviceStatusType
};
}

View File

@@ -21,6 +21,7 @@ import {
} from '../../../base/react';
import { connect } from '../../../base/redux';
import { ColorPalette, StyleType } from '../../../base/styles';
import { isVpaasMeeting } from '../../../billing-counter/functions';
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox';
import { RECORDING_TYPES } from '../../constants';
import { getRecordingDurationEstimation } from '../../functions';
@@ -72,6 +73,11 @@ type Props = {
*/
isValidating: boolean,
/**
* Whether or not the current meeting is a vpaas one.
*/
isVpaas: boolean,
/**
* The function will be called when there are changes related to the
* switches.
@@ -226,7 +232,7 @@ class StartRecordingDialogContent extends Component<Props> {
return null;
}
const { _dialogStyles, _styles: styles, isValidating, t } = this.props;
const { _dialogStyles, _styles: styles, isValidating, isVpaas, t } = this.props;
const switchContent
= this.props.integrationsEnabled
@@ -240,6 +246,8 @@ class StartRecordingDialogContent extends Component<Props> {
value = { this.props.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
) : null;
const icon = isVpaas ? ICON_SHARE : JITSI_LOGO;
return (
<Container
className = 'recording-header'
@@ -248,7 +256,7 @@ class StartRecordingDialogContent extends Component<Props> {
<Container className = 'recording-icon-container'>
<Image
className = 'recording-icon'
src = { JITSI_LOGO }
src = { icon }
style = { styles.recordingIcon } />
</Container>
<Text
@@ -484,6 +492,7 @@ class StartRecordingDialogContent extends Component<Props> {
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
isVpaas: isVpaasMeeting(state),
_styles: ColorSchemeRegistry.get(state, 'StartRecordingDialogContent')
};
}

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,23 @@
/**
* The type of (redux) action which sets the preferred maximum video height that
* should be sent to and received from remote participants.
*
* {
* type: SET_PREFERRED_VIDEO_QUALITY,
* preferredVideoQuality: number
* }
*/
export const SET_PREFERRED_VIDEO_QUALITY = 'SET_PREFERRED_VIDEO_QUALITY';
/**
* The type of (redux) action which sets the maximum video height that should be
* received from remote participants, even if the user prefers a larger video
* height.
*
* {
* type: SET_MAX_RECEIVER_VIDEO_QUALITY,
* maxReceiverVideoQuality: number
* }
*/
export const SET_MAX_RECEIVER_VIDEO_QUALITY = 'SET_MAX_RECEIVER_VIDEO_QUALITY';

View File

@@ -2,10 +2,45 @@
import type { Dispatch } from 'redux';
import { VIDEO_QUALITY_LEVELS } from '../base/conference';
import { SET_MAX_RECEIVER_VIDEO_QUALITY, SET_PREFERRED_VIDEO_QUALITY } from './actionTypes';
import { VIDEO_QUALITY_LEVELS } from './constants';
import logger from './logger';
/**
* Sets the max frame height the user prefers to send and receive from the
* remote participants.
*
* @param {number} preferredVideoQuality - The max video resolution to send and
* receive.
* @returns {{
* type: SET_PREFERRED_VIDEO_QUALITY,
* preferredVideoQuality: number
* }}
*/
export function setPreferredVideoQuality(preferredVideoQuality: number) {
return {
type: SET_PREFERRED_VIDEO_QUALITY,
preferredVideoQuality
};
}
/**
* Sets the max frame height that should be received from remote videos.
*
* @param {number} maxReceiverVideoQuality - The max video frame height to
* receive.
* @returns {{
* type: SET_MAX_RECEIVER_VIDEO_QUALITY,
* maxReceiverVideoQuality: number
* }}
*/
export function setMaxReceiverVideoQuality(maxReceiverVideoQuality: number) {
return {
type: SET_MAX_RECEIVER_VIDEO_QUALITY,
maxReceiverVideoQuality
};
}
/**
* Sets the maximum video size the local participant should send and receive from
@@ -16,18 +51,13 @@ import logger from './logger';
* @returns {void}
*/
export function setVideoQuality(frameHeight: number) {
return (dispatch: Dispatch<any>, getState: Function) => {
const { conference, maxReceiverVideoQuality } = getState()['features/base/conference'];
return (dispatch: Dispatch<any>) => {
if (frameHeight < VIDEO_QUALITY_LEVELS.LOW) {
logger.error(`Invalid frame height for video quality - ${frameHeight}`);
return;
}
conference.setReceiverVideoConstraint(Math.min(frameHeight, maxReceiverVideoQuality));
conference.setSenderVideoConstraint(Math.min(frameHeight, VIDEO_QUALITY_LEVELS.HIGH))
.catch(err => {
logger.error(`Set video quality command failed - ${err}`);
});
dispatch(setPreferredVideoQuality(Math.min(frameHeight, VIDEO_QUALITY_LEVELS.HIGH)));
};
}

View File

@@ -2,7 +2,6 @@
import React, { Component } from 'react';
import { VIDEO_QUALITY_LEVELS } from '../../base/conference/constants';
import { translate } from '../../base/i18n';
import {
Icon,
@@ -12,6 +11,8 @@ import {
IconVideoQualitySD
} from '../../base/icons';
import { connect } from '../../base/redux';
import { VIDEO_QUALITY_LEVELS } from '../constants';
import { findNearestQualityLevel } from '../functions';
/**
* A map of of selectable receive resolutions to corresponding icons.
@@ -69,9 +70,10 @@ class OverflowMenuVideoQualityItem extends Component<Props> {
*/
render() {
const { _audioOnly, _videoQuality } = this.props;
const icon = _audioOnly || !_videoQuality
const videoQualityLevel = findNearestQualityLevel(_videoQuality);
const icon = _audioOnly || !videoQualityLevel
? IconVideoQualityAudioOnly
: VIDEO_QUALITY_TO_ICON[_videoQuality];
: VIDEO_QUALITY_TO_ICON[videoQualityLevel];
return (
<li
@@ -104,7 +106,7 @@ class OverflowMenuVideoQualityItem extends Component<Props> {
function _mapStateToProps(state) {
return {
_audioOnly: state['features/base/audio-only'].enabled,
_videoQuality: state['features/base/conference'].preferredVideoQuality
_videoQuality: state['features/video-quality'].preferredVideoQuality
};
}

View File

@@ -6,10 +6,11 @@ import type { Dispatch } from 'redux';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { setAudioOnly } from '../../base/audio-only';
import { VIDEO_QUALITY_LEVELS, setPreferredVideoQuality } from '../../base/conference';
import { translate } from '../../base/i18n';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import { connect } from '../../base/redux';
import { setPreferredVideoQuality } from '../actions';
import { VIDEO_QUALITY_LEVELS } from '../constants';
import logger from '../logger';
const {
@@ -315,10 +316,13 @@ class VideoQualitySlider extends Component<Props> {
return _sliderOptions.indexOf(audioOnlyOption);
}
const matchingOption = _sliderOptions.find(
({ videoQuality }) => videoQuality === _sendrecvVideoQuality);
for (let i = 0; i < _sliderOptions.length; i++) {
if (_sliderOptions[i].videoQuality >= _sendrecvVideoQuality) {
return i;
}
}
return _sliderOptions.indexOf(matchingOption);
return -1;
}
_onSliderChange: () => void;
@@ -380,7 +384,8 @@ class VideoQualitySlider extends Component<Props> {
*/
function _mapStateToProps(state) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const { p2p, preferredVideoQuality } = state['features/base/conference'];
const { p2p } = state['features/base/conference'];
const { preferredVideoQuality } = state['features/video-quality'];
return {
_audioOnly: audioOnly,

View File

@@ -0,0 +1,22 @@
/**
* The supported remote video resolutions. The values are currently based on
* available simulcast layers.
*
* @type {object}
*/
export const VIDEO_QUALITY_LEVELS = {
HIGH: 720,
STANDARD: 360,
LOW: 180
};
/**
* 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
};

Some files were not shown because too many files have changed in this diff Show More