Compare commits

...

65 Commits

Author SHA1 Message Date
damencho
8cf76b20c7 debug 2025-03-05 07:40:32 -06:00
damencho
44c1633952 feat(tests): Fixes the checks when to use token.
We have few options:
- iframeAPI tests generating tokens via jwtPrivateKeyPath
- tests that just use provided JWT_ACCESS_TOKEN for the first participant to avoid deployments where initial authentication is required
- tests that does not use iframeAPI, but want to use the jwtPrivateKeyPath for a meeting (invite test as JWT_ACCESS_TOKEN does not satisfy some services)
2025-03-04 19:08:04 -06:00
damencho
f83ad5af27 fix: Fix invite tests. 2025-03-04 12:27:18 -06:00
Saúl Ibarra Corretgé
a5afd011a1 fix(breakout-rooms) fix processing commands (#15695)
* fix(breakout-rooms) fix processing commands

* squash: fix wrong var name.

* squash: fix move to breakout room.

It can be from breakout to main or from main to breakout.

---------

Co-authored-by: damencho <damencho@jitsi.org>
2025-03-04 12:27:12 -06:00
damencho
c88891da5b feat(tests): Adds some jaas related checks. 2025-03-04 09:46:22 -06:00
damencho
b1af0c800b fix(tests): Lobby wait more for the load. 2025-03-04 09:46:22 -06:00
damencho
146d2c8b66 fix(tests): Locked dialog wait to be stable. 2025-03-04 09:46:22 -06:00
damencho
a18e193611 feat(tests): Fixes race who will join first in start muted. 2025-03-04 09:46:22 -06:00
damencho
6ae0bc36cc feat(tests): Adds wait for locked state. 2025-03-04 09:46:22 -06:00
damencho
b1410c34e0 feat(tests): Bumps time waiting for joining lobby room. 2025-03-04 09:46:22 -06:00
damencho
070991d7ef feat(tests): Bumps time waiting for breakout rooms update.
Sometimes leaving can take some time.
2025-03-04 09:46:22 -06:00
damencho
874f59f0ff fix(tests): Use lower resolution when possible. 2025-03-04 09:46:22 -06:00
damencho
fa547b5aac fix(tests): Avatar do not ignore token when joining second time. 2025-03-04 09:46:22 -06:00
damencho
d27580c016 fix(tests): Adds time to the keep-alive print. 2025-03-04 09:46:22 -06:00
damencho
2093ef1ea2 fix(tests): Skip iframeAPI if it is disabled. 2025-03-04 09:46:22 -06:00
damencho
75540a588d fix(tests): Fixes avatar tests when providing jwt token. 2025-03-04 09:46:22 -06:00
damencho
fe51b4c56a fix(tests): Fixes audio only test when using jwt for joining. 2025-03-04 09:46:22 -06:00
damencho
c7c42f6983 feat(tests): Adds an option to use a different tenant for iframeAPI. 2025-03-04 09:46:22 -06:00
damencho
68df1b1281 feat(tests): Adds an option to append room name suffixes. 2025-03-04 09:46:22 -06:00
damencho
00efcfaae5 feat(tests): Adds an option for an access jwt token.
Used only for the first participant joining/creating the room.
2025-03-04 09:46:22 -06:00
ahmedasad236
c6b194a073 fix: padding in the welcome page and overflow of in small screens (#15690)
* fix: padding in the welcome page and overflow of meeting list

- Add padding to the header of the welcome page in small screens.
- Fix the tab content in the welcome page, because there was an ugly overflow.
- Create new color variables for welcome page button.
2025-03-04 15:59:49 +02:00
Saúl Ibarra Corretgé
8ac44dfbb3 fix(ios) remove orphaned file references from the project 2025-03-04 13:36:27 +01:00
Saúl Ibarra Corretgé
ea2ab9edc0 fix(ios) fix exporting JitsiMeetView.{start,stop}Recording to Swift 2025-03-04 13:36:27 +01:00
Saúl Ibarra Corretgé
9d27c705f6 feat(android) use hardware video decoders by default
In addition, add the ability to initialize custom video encoder /
decoder factories.
2025-03-04 09:45:24 +01:00
Mihaela Dumitru
ebdd9755ba feat(recordings) add consent dialog (#15673) 2025-03-04 10:30:12 +02:00
Stefan Zugal
fa2a8c5084 chore(lang): update German translations (#15500)
Rename: Meeting -> Konferenz and Teilnehmer -> Personen
2025-03-03 23:24:16 +01:00
Calinteodor
ac2d73b57c feat(conference/large-video): hide display name label based on config
Hide display name label based on config for web and mobile.
2025-03-03 19:47:34 +02:00
Hristo Terezov
93902e6364 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1916.0.0+f5964f9b...v1919.0.0+d4a47d0e
2025-02-28 11:56:39 -06:00
gayatriii0803
42163731b3 fix(rn) fix displaying meeting times for yesterday 2025-02-28 15:48:08 +01:00
Kevin Vikström
01ce04fe9b lang: norwegian language added (#15669)
* added language norwegian bokmal

* added norwegian bokmål to languages.json

* added norwegian main-no.json

* added norwegian to languages.json
2025-02-27 06:33:15 -06:00
Calin-Teodor
5d29363764 feat(conference): hide toggle camera and audio device selection btns 2025-02-26 14:03:09 +02:00
Calin-Teodor
bfe8bc9b73 feat(conference): hide LonelyMeetingExperience if add people feat is disabled 2025-02-26 13:50:31 +02:00
Jaya Allamsetty
a6f6235dd0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1915.0.0+6e9b9c01...v1916.0.0+f5964f9b
2025-02-25 14:53:28 -05:00
Hristo Terezov
ee6bf011e9 feat(config): Add logger.warn for depricated params.
This includes interfaceConfig.SUPPORT_URL, interfaceConfig.LIVE_STREAMING_HELP_LINK, config.defaultLogoUrl, config.deploymentUrls, config.liveStreaming and config.customToolbarButtons.
2025-02-25 07:41:53 -06:00
Hristo Terezov
bea8a7f984 fix(configWhitelist): Remove customToolbarButtons. 2025-02-25 07:41:53 -06:00
Hristo Terezov
2edca5dacb fix(analytics): overwritesCustomButtonsWithURL metric
Count all customButtons overrides not only the ones that are not including data URLs.
2025-02-25 07:41:53 -06:00
Hristo Terezov
69ac73c556 feat(dynamic-branding): Add customToolbarButtons. 2025-02-25 07:41:53 -06:00
Hristo Terezov
89556ecd66 feat(dynamic-branding): Add customParticipantMenuButtons 2025-02-25 07:41:53 -06:00
Hristo Terezov
462f91f070 feat(dynamic-branding): Add etherpadBase 2025-02-25 07:41:53 -06:00
Hristo Terezov
d29a77b15f feat(dynamic-branding): Add peopleSearchUrl 2025-02-25 07:41:53 -06:00
Hristo Terezov
c31fe521c4 feat(analytics): remove overwritesPrejoinConfigICEUrl 2025-02-25 07:41:53 -06:00
Hristo Terezov
8f6f542e9c feat(inIframe-whitelists): Implement.
Now we are able to have a whitelist for config and interface config that will be used only for the case where jitsi-meet is loaded in an IFrame.
2025-02-25 07:41:53 -06:00
Hristo Terezov
69d9e7d405 ref(analytics): remove overwritesHosts 2025-02-25 07:41:53 -06:00
Hristo Terezov
5e6748a88a ref(analytics): remove overwritesIceServers 2025-02-25 07:41:53 -06:00
Hristo Terezov
8bc70f9c87 fix(iceServers): Restrict iceServers url param to iframe only. 2025-02-25 07:41:53 -06:00
Avram Tudor
357d226987 feat: allow specifying actions in custom notifications (#15666)
Co-authored-by: Avram Tudor <tudor.avram@8x8.com>
2025-02-25 12:43:18 +02:00
bgrozev
6b1f7138c6 fix: Check for ICE connected as part of ensureXParticipants. (#15664)
* fix: Check for ICE connected as part of ensureXParticipants.

* squash: Move waitForIceConnected and waitForSendReceiveData to ensure methods.

* squash: Check ICE first, then "send receive data", then remote streams. Report the correct failure.

---------

Co-authored-by: damencho <damencho@jitsi.org>
2025-02-24 21:08:33 -06:00
damencho
55219dc51b fix(tests): Fix test name in FF excludes. 2025-02-24 10:29:02 -06:00
damencho
0eb3a9a43c fix(tests): Temporary disable one check when FF is involved. 2025-02-21 15:28:57 -06:00
damencho
4d7136b7a7 fix(tests): AV moderation UI changes. 2025-02-21 15:28:57 -06:00
damencho
b7d9e1d85d fix(tests): Fix avatar test adding FF condition. 2025-02-21 15:28:57 -06:00
damencho
a714058328 fix(tests): Fixes Lobby disabled wait. 2025-02-21 15:28:57 -06:00
damencho
02ff4a1bac feat(tests): Drops unused field for setting password.
We require digit input and do not have a custom validation.
2025-02-21 15:28:57 -06:00
damencho
7833e1337e feat(tests): Adds keep-alive to newly created sessions.
Tests that take time (desktopSharing) before they use one of the browsers (the 4th one), by the time we use it backend may have timed out  the websocket (60 seconds). Add every 20 second and execute a print to keep it alive.
2025-02-21 15:28:57 -06:00
damencho
18e0e64ca0 fix(tests): Disable lastN test for FF. 2025-02-21 15:28:57 -06:00
damencho
80a3d88359 fix(tests): Disable AV moderation for FF. 2025-02-21 15:28:57 -06:00
damencho
5d72028872 feat(tests): Adds debug logs on failure. 2025-02-21 15:28:57 -06:00
damencho
e89776848c fix(tests): Use worker id to create console log files.
Avoid accumulating large files and keeping them per test.
2025-02-21 15:28:57 -06:00
damencho
70bc78e765 fix(tests): Disable startMuted on FF. 2025-02-21 15:28:57 -06:00
damencho
4fceae7733 fix(tests): Bumps global timeout for tests.
Desktop sharing is a long one.
2025-02-21 15:28:57 -06:00
damencho
23b7dd4abf fix(tests): Adds undefined checks. 2025-02-21 15:28:57 -06:00
damencho
0216bbd1d9 feat(tests): Adds an option to specify max instances. 2025-02-21 15:28:57 -06:00
damencho
15a4fa45e0 feat(tests): Adds target for grid ff tests. 2025-02-21 15:28:57 -06:00
damencho
f2d9ffd5f6 feat(tests): Handle checking for grid by updating merged config. 2025-02-21 15:28:57 -06:00
Rahul Vishwakarma
b0ba7c8671 lang: Update Italian. 2025-02-21 15:28:39 -06:00
76 changed files with 2545 additions and 325 deletions

View File

@@ -30,9 +30,12 @@ import android.view.KeyEvent;
import androidx.annotation.Nullable;
import com.oney.WebRTCModule.WebRTCModuleOptions;
import org.jitsi.meet.sdk.JitsiMeet;
import org.jitsi.meet.sdk.JitsiMeetActivity;
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions;
import org.webrtc.Logging;
import java.lang.reflect.Method;
import java.net.URL;
@@ -79,6 +82,10 @@ public class MainActivity extends JitsiMeetActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
JitsiMeet.showSplashScreen(this);
WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
options.loggingSeverity = Logging.Severity.LS_ERROR;
super.onCreate(null);
}

View File

@@ -89,9 +89,11 @@ dependencies {
implementation project(':react-native-splash-screen')
implementation project(':react-native-svg')
implementation project(':react-native-video')
implementation project(':react-native-webrtc')
implementation project(':react-native-webview')
// Use `api` here so consumers can use WebRTCModuleOptions.
api project(':react-native-webrtc')
testImplementation 'junit:junit:4.12'
}

View File

@@ -0,0 +1,80 @@
package org.jitsi.meet.sdk;
/*
* Copyright 2017 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
import androidx.annotation.Nullable;
import com.oney.WebRTCModule.webrtcutils.SoftwareVideoDecoderFactoryProxy;
import org.webrtc.EglBase;
import org.webrtc.HardwareVideoDecoderFactory;
import org.webrtc.PlatformSoftwareVideoDecoderFactory;
import org.webrtc.VideoCodecInfo;
import org.webrtc.VideoDecoder;
import org.webrtc.VideoDecoderFactory;
import org.webrtc.VideoDecoderFallback;
import java.util.Arrays;
import java.util.LinkedHashSet;
/**
* Custom decoder factory which uses HW decoders and falls back to SW.
*/
public class JitsiVideoDecoderFactory implements VideoDecoderFactory {
private final VideoDecoderFactory hardwareVideoDecoderFactory;
private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactoryProxy();
private final @Nullable VideoDecoderFactory platformSoftwareVideoDecoderFactory;
/**
* Create decoder factory using default hardware decoder factory.
*/
public JitsiVideoDecoderFactory(@Nullable EglBase.Context eglContext) {
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext);
this.platformSoftwareVideoDecoderFactory = new PlatformSoftwareVideoDecoderFactory(eglContext);
}
/**
* Create decoder factory using explicit hardware decoder factory.
*/
JitsiVideoDecoderFactory(VideoDecoderFactory hardwareVideoDecoderFactory) {
this.hardwareVideoDecoderFactory = hardwareVideoDecoderFactory;
this.platformSoftwareVideoDecoderFactory = null;
}
@Override
public @Nullable VideoDecoder createDecoder(VideoCodecInfo codecType) {
VideoDecoder softwareDecoder = softwareVideoDecoderFactory.createDecoder(codecType);
final VideoDecoder hardwareDecoder = hardwareVideoDecoderFactory.createDecoder(codecType);
if (softwareDecoder == null && platformSoftwareVideoDecoderFactory != null) {
softwareDecoder = platformSoftwareVideoDecoderFactory.createDecoder(codecType);
}
if (hardwareDecoder != null && softwareDecoder != null) {
// Both hardware and software supported, wrap it in a software fallback
return new VideoDecoderFallback(
/* fallback= */ softwareDecoder, /* primary= */ hardwareDecoder);
}
return hardwareDecoder != null ? hardwareDecoder : softwareDecoder;
}
@Override
public VideoCodecInfo[] getSupportedCodecs() {
LinkedHashSet<VideoCodecInfo> supportedCodecInfos = new LinkedHashSet<>();
supportedCodecInfos.addAll(Arrays.asList(softwareVideoDecoderFactory.getSupportedCodecs()));
supportedCodecInfos.addAll(Arrays.asList(hardwareVideoDecoderFactory.getSupportedCodecs()));
if (platformSoftwareVideoDecoderFactory != null) {
supportedCodecInfos.addAll(
Arrays.asList(platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
}
return supportedCodecInfos.toArray(new VideoCodecInfo[supportedCodecInfos.size()]);
}
}

View File

@@ -0,0 +1,16 @@
package org.jitsi.meet.sdk;
import androidx.annotation.Nullable;
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
import org.webrtc.EglBase;
/**
* Custom encoder factory which uses HW for H.264 and SW for everything else.
*/
public class JitsiVideoEncoderFactory extends H264AndSoftwareVideoEncoderFactory {
public JitsiVideoEncoderFactory(@Nullable EglBase.Context eglContext) {
super(eglContext);
}
}

View File

@@ -16,8 +16,8 @@
package org.jitsi.meet.sdk;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.util.Log;
import androidx.annotation.Nullable;
@@ -32,12 +32,10 @@ import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.oney.WebRTCModule.EglUtils;
import com.oney.WebRTCModule.WebRTCModuleOptions;
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoDecoderFactory;
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
import org.devio.rn.splashscreen.SplashScreenModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import org.webrtc.EglBase;
import org.webrtc.Logging;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
@@ -126,31 +124,31 @@ class ReactInstanceManagerHolder {
// AmplitudeReactNativePackage
try {
Class<?> amplitudePackageClass = Class.forName("com.amplitude.reactnative.AmplitudeReactNativePackage");
Constructor constructor = amplitudePackageClass.getConstructor();
Constructor<?> constructor = amplitudePackageClass.getConstructor();
packages.add((ReactPackage)constructor.newInstance());
} catch (Exception e) {
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
Log.d(TAG, "Not loading AmplitudeReactNativePackage");
JitsiMeetLogger.d(TAG, "Not loading AmplitudeReactNativePackage");
}
// GiphyReactNativeSdkPackage
try {
Class<?> giphyPackageClass = Class.forName("com.giphyreactnativesdk.GiphyReactNativeSdkPackage");
Constructor constructor = giphyPackageClass.getConstructor();
Constructor<?> constructor = giphyPackageClass.getConstructor();
packages.add((ReactPackage)constructor.newInstance());
} catch (Exception e) {
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
Log.d(TAG, "Not loading GiphyReactNativeSdkPackage");
JitsiMeetLogger.d(TAG, "Not loading GiphyReactNativeSdkPackage");
}
// RNGoogleSignInPackage
try {
Class<?> googlePackageClass = Class.forName("com.reactnativegooglesignin.RNGoogleSigninPackage");
Constructor constructor = googlePackageClass.getConstructor();
Constructor<?> constructor = googlePackageClass.getConstructor();
packages.add((ReactPackage)constructor.newInstance());
} catch (Exception e) {
// Ignore any error, the module is not compiled when LIBRE_BUILD is enabled.
Log.d(TAG, "Not loading RNGoogleSignInPackage");
JitsiMeetLogger.d(TAG, "Not loading RNGoogleSignInPackage");
}
return packages;
@@ -169,7 +167,7 @@ class ReactInstanceManagerHolder {
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
ReactContext reactContext
@SuppressLint("VisibleForTests") ReactContext reactContext
= reactInstanceManager.getCurrentReactContext();
if (reactContext != null) {
@@ -192,7 +190,7 @@ class ReactInstanceManagerHolder {
*/
static <T extends NativeModule> T getNativeModule(
Class<T> nativeModuleClass) {
ReactContext reactContext
@SuppressLint("VisibleForTests") ReactContext reactContext
= reactInstanceManager != null
? reactInstanceManager.getCurrentReactContext() : null;
@@ -219,15 +217,18 @@ class ReactInstanceManagerHolder {
// Initialize the WebRTC module options.
WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
options.videoDecoderFactory = new H264AndSoftwareVideoDecoderFactory(eglContext);
options.videoEncoderFactory = new H264AndSoftwareVideoEncoderFactory(eglContext);
options.enableMediaProjectionService = true;
// options.loggingSeverity = Logging.Severity.LS_INFO;
if (options.videoDecoderFactory == null || options.videoEncoderFactory == null) {
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
if (options.videoDecoderFactory == null) {
options.videoDecoderFactory = new JitsiVideoDecoderFactory(eglContext);
}
if (options.videoEncoderFactory == null) {
options.videoEncoderFactory = new JitsiVideoEncoderFactory(eglContext);
}
}
Log.d(TAG, "initializing RN with Activity");
JitsiMeetLogger.d(TAG, "initializing RN");
reactInstanceManager
= ReactInstanceManager.builder()

View File

@@ -393,6 +393,9 @@ var config = {
// // showPrejoinWarning: true,
// // If true, the notification for recording start will display a link to download the cloud recording.
// // showRecordingLink: true,
// // If true, mutes audio and video when a recording begins and displays a dialog
// // explaining the effect of unmuting.
// // requireConsent: true,
// },
// recordingService: {

View File

@@ -7,8 +7,8 @@
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
width: 100%;
overflow-y: auto;
flex-grow: 1;
.meetings-list-empty {
text-align: center;

View File

@@ -75,9 +75,12 @@ $welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0,
$welcomePageHeaderBackgroundPosition: center;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: cover;
$welcomePageHeaderPaddingBottom: 15px;
$welcomePageHeaderPadding: 1rem;
$welcomePageHeaderTitleMaxWidth: initial;
$welcomePageHeaderTextAlign: center;
$welcomePageButtonBg: #0074E0;
$welcomePageButtonHoverBg: #4687ED;
$welcomePageButtonFocusOutline: #00225A;
$welcomePageHeaderContainerMarginTop: 104px;
$welcomePageHeaderContainerDisplay: flex;

View File

@@ -18,7 +18,7 @@ body.welcome-page {
background-position: $welcomePageHeaderBackgroundPosition;
background-repeat: $welcomePageHeaderBackgroundRepeat;
background-size: $welcomePageHeaderBackgroundSize;
padding-bottom: $welcomePageHeaderPaddingBottom;
padding: $welcomePageHeaderPadding;
background-color: #131519;
overflow: hidden;
position: relative;
@@ -219,14 +219,18 @@ body.welcome-page {
.welcome-page-button {
border: 0;
font-size: 14px;
background: #0074E0;
background: $welcomePageButtonBg;
border-radius: 3px;
color: #FFFFFF;
cursor: pointer;
padding: 16px 20px;
transition: all 0.2s;
&:focus-within {
outline: auto 2px #022e61;
outline: auto 2px $welcomePageButtonFocusOutline;
}
&:hover {
background-color: $welcomePageButtonHoverBg;
}
}
@@ -264,8 +268,7 @@ body.welcome-page {
&.without-content {
.welcome-card {
min-width: 500px;
max-width: 580px;
max-width: 100dvw;
}
}

View File

@@ -157,7 +157,6 @@
4EB0603B260E09D000F524C5 /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = "<group>"; };
4EC49B8625BED71300E76218 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
5C1BE20ECD5DEEB48FED90B5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
756FCE06C08D9B947653C98A /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = "<group>"; };
D6152FF9E9F7B0E86F70A21D /* libPods-JitsiMeet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JitsiMeet.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -302,7 +301,6 @@
0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */,
4EB06025260E026600F524C5 /* JitsiMeetBroadcast Extension */,
CDD71F5E1157E9F283DF92A8 /* Pods */,
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */,
5C1BE20ECD5DEEB48FED90B5 /* PrivacyInfo.xcprivacy */,
);
indentWidth = 2;

View File

@@ -145,7 +145,6 @@
4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiAudioSession.h; sourceTree = "<group>"; };
4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiAudioSession.m; sourceTree = "<group>"; };
4ED4FFF52721BAE10074E620 /* JitsiAudioSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiAudioSession+Private.h"; sourceTree = "<group>"; };
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
86389F55993FAAF6AEB3FA3E /* Pods-JitsiMeetSDKLite.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDKLite.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDKLite/Pods-JitsiMeetSDKLite.release.xcconfig"; sourceTree = "<group>"; };
891FE43DAD30BC8976683100 /* Pods-JitsiMeetSDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDK.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDK/Pods-JitsiMeetSDK.release.xcconfig"; sourceTree = "<group>"; };
8F48C340DE0D91D1012976C5 /* Pods-JitsiMeetSDKLite.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDKLite.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDKLite/Pods-JitsiMeetSDKLite.debug.xcconfig"; sourceTree = "<group>"; };
@@ -231,7 +230,6 @@
0BD906E61EC0C00300C8C18E /* Products */,
0BCA49681EC4BBE500B793EE /* Resources */,
0BD906E71EC0C00300C8C18E /* src */,
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */,
);
sourceTree = "<group>";
};

View File

@@ -18,11 +18,6 @@
static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
typedef NS_ENUM(NSInteger, RecordingMode) {
RecordingModeFile,
RecordingModeStream
};
@interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
- (void)sendHangUp;
@@ -38,7 +33,7 @@ typedef NS_ENUM(NSInteger, RecordingMode) {
- (void)toggleCamera;
- (void)showNotification:(NSString*)appearance :(NSString*)description :(NSString*)timeout :(NSString*)title :(NSString*)uid;
- (void)hideNotification:(NSString*)uid;
- (void)startRecording:(RecordingMode)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription;
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription;
- (void)startRecording:(NSString*)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription;
- (void)stopRecording:(NSString*)mode :(BOOL)transcription;
@end

View File

@@ -38,7 +38,7 @@ static NSString * const stopRecordingAction = @"org.jitsi.meet.STOP_RECORDING";
static NSMapTable<NSString*, void (^)(NSArray* participantsInfo)> *participantInfoCompletionHandlers;
__attribute__((constructor))
static void initializeViewsMap() {
static void initializeViewsMap(void) {
participantInfoCompletionHandlers = [NSMapTable strongToStrongObjectsMapTable];
}
@@ -210,21 +210,9 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:hideNotificationAction body:data];
}
static inline NSString *RecordingModeToString(RecordingMode mode) {
switch (mode) {
case RecordingModeFile:
return @"file";
case RecordingModeStream:
return @"stream";
default:
return nil;
}
}
- (void)startRecording:(RecordingMode)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription {
NSString *modeString = RecordingModeToString(mode);
- (void)startRecording:(NSString*)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription {
NSDictionary *data = @{
@"mode": modeString,
@"mode": mode,
@"dropboxToken": dropboxToken,
@"shouldShare": @(shouldShare),
@"rtmpStreamKey": rtmpStreamKey,
@@ -238,10 +226,9 @@ static inline NSString *RecordingModeToString(RecordingMode mode) {
[self sendEventWithName:startRecordingAction body:data];
}
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription {
NSString *modeString = RecordingModeToString(mode);
- (void)stopRecording:(NSString*)mode :(BOOL)transcription {
NSDictionary *data = @{
@"mode": modeString,
@"mode": mode,
@"transcription": @(transcription)
};

View File

@@ -21,7 +21,10 @@
#import "JitsiMeetConferenceOptions.h"
#import "JitsiMeetViewDelegate.h"
typedef NS_ENUM(NSInteger, RecordingMode);
typedef NS_ENUM(NSInteger, RecordingMode) {
RecordingModeFile,
RecordingModeStream
};
@interface JitsiMeetView : UIView

View File

@@ -30,6 +30,11 @@
*/
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
/**
* Forward declarations.
*/
static NSString *recordingModeToString(RecordingMode mode);
@implementation JitsiMeetView {
/**
@@ -153,15 +158,15 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
[externalAPI hideNotification:uid];
}
- (void)startRecording:(RecordingMode)mode :(NSString *)dropboxToken :(BOOL)shouldShare :(NSString *)rtmpStreamKey :(NSString *)rtmpBroadcastID :(NSString *)youtubeStreamKey :(NSString *)youtubeBroadcastID :(NSString *)extraMetadata :(BOOL)transcription {
- (void)startRecording:(RecordingMode)mode :(NSString *)dropboxToken :(BOOL)shouldShare :(NSString *)rtmpStreamKey :(NSString *)rtmpBroadcastID :(NSString *)youtubeStreamKey :(NSString *)youtubeBroadcastID :(NSDictionary *)extraMetadata :(BOOL)transcription {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI startRecording:mode :dropboxToken :shouldShare :rtmpStreamKey :rtmpBroadcastID :youtubeStreamKey :youtubeBroadcastID :extraMetadata :transcription];
[externalAPI startRecording:recordingModeToString(mode) :dropboxToken :shouldShare :rtmpStreamKey :rtmpBroadcastID :youtubeStreamKey :youtubeBroadcastID :extraMetadata :transcription];
}
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI stopRecording:mode :transcription];
}
[externalAPI stopRecording:recordingModeToString(mode) :transcription];
}
#pragma mark Private methods
@@ -257,3 +262,14 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
}
@end
static NSString *recordingModeToString(RecordingMode mode) {
switch (mode) {
case RecordingModeFile:
return @"file";
case RecordingModeStream:
return @"stream";
default:
return nil;
}
}

View File

@@ -39,6 +39,7 @@
"mr": "मराठी",
"nb": "Norsk bokmål",
"nl": "Nederlands",
"no": "Norsk",
"oc": "Occitan",
"pl": "Polski",
"pt": "Português",

View File

@@ -70,14 +70,14 @@
"breakoutList": "Breakout-Liste",
"buttonLabel": "Breakout-Räume",
"defaultName": "Breakout-Raum #{{index}}",
"hideParticipantList": "Teilnehmerliste ausblenden",
"hideParticipantList": "Personenliste ausblenden",
"mainRoom": "Hauptraum",
"notifications": {
"joined": "Breakout-Raum \"{{name}}\" betreten",
"joinedMainRoom": "Hauptraum betreten",
"joinedTitle": "Breakout-Räume"
},
"showParticipantList": "Teilnehmerliste anzeigen",
"showParticipantList": "Personenliste anzeigen",
"title": "Breakout-Räume"
},
"calendarSync": {
@@ -89,7 +89,7 @@
"notSignedIn": "Ein Fehler ist während der Authentifizierung zur Anzeige von Kalenderterminen aufgetreten. Prüfen Sie Ihre Kalendereinstellungen oder versuchen Sie, sich erneut anzumelden."
},
"join": "Teilnehmen",
"joinTooltip": "Am Konferenz teilnehmen",
"joinTooltip": "An Konferenz teilnehmen",
"nextMeeting": "Nächste Konferenz",
"noEvents": "Es gibt keine bevorstehenden Termine.",
"ongoingMeeting": "Laufende Konferenz",
@@ -363,18 +363,18 @@
"muteEveryoneDialogModerationOn": "Die Anwesenden können eine Anfrage zum Sprechen jederzeit senden.",
"muteEveryoneElseDialog": "Einmal stummgeschaltet, können Sie deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
"muteEveryoneElsesVideoDialog": "Sobald die Kamera deaktiviert ist, können Sie sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteEveryoneElsesVideoDialog": "Sobald die Kamera für alle anderen Personen deaktiviert ist, können Sie diese nicht wieder für alle einschalten, die anderen Personen können ihre Kamera aber jederzeit wieder einschalten.",
"muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?",
"muteEveryoneSelf": "sich selbst",
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
"muteEveryoneTitle": "Alle stummschalten?",
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Teilnehmern deaktivieren möchten? Sie können sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Personen deaktivieren möchten? Sie können dies nicht wieder rückgängig machen, jede Personen kann ihre Kamera aber jederzeit wieder einschalten.",
"muteEveryonesVideoDialogModerationOn": "Die Anwesenden können jederzeit eine Anfrage senden, um ihre Kamera einzuschalten.",
"muteEveryonesVideoDialogOk": "deaktivieren",
"muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder einschalten, die Person kann ihre Kamera aber jederzeit wieder einschalten.",
"muteParticipantsVideoBodyModerationOn": "Sie können die Kamera nicht wieder aktivieren und die Person selbst auch nicht.",
"muteParticipantsVideoButton": "Kamera ausschalten",
"muteParticipantsVideoDialog": "Wollen Sie die Kamera dieser Person wirklich deaktivieren? Sie können die Kamera nicht wieder aktivieren, die Person kann dies aber jederzeit selbst tun.",
@@ -562,7 +562,7 @@
"inviteTextiOSPhone": "Nutzen Sie folgende Nummer um via Telefon teilzunehmen: {{number}},,{{conferenceID}}#. Wenn Sie nach einer anderen Einwahlnummer suchen, finden Sie die vollständige Liste hier: {{didUrl}}.",
"inviteURLFirstPartGeneral": "Sie wurden zur Teilnahme an einer Konferenz eingeladen.",
"inviteURLFirstPartPersonal": "{{name}} lädt Sie zu einer Konferenz ein.\n",
"inviteURLSecondPart": "\nAm Konferenz teilnehmen:\n{{url}}\n",
"inviteURLSecondPart": "\nAn Konferenz teilnehmen:\n{{url}}\n",
"label": "Einwahlinformationen",
"liveStreamURL": "Livestream:",
"moreNumbers": "Weitere Telefonnummern",
@@ -642,6 +642,7 @@
"on": "Livestream",
"onBy": "{{name}} startete den Livestream",
"pending": "Livestream wird gestartet …",
"policyError": "Sie haben den Livestream zu schnell gestartet. Bitte versuchen Sie es später noch einmal!",
"serviceName": "Livestreaming-Dienst",
"sessionAlreadyActive": "Diese Konferenz wird bereits als Livestream übertragen.",
"signIn": "Mit Google anmelden",
@@ -742,7 +743,7 @@
"connectedOneMember": "{{name}} nimmt an der Konferenz teil",
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen an der Konferenz teil",
"connectedTwoMembers": "{{first}} und {{second}} nehmen an der Konferenz teil",
"connectionFailed": "Verbindung fehlgeschlagen. Bitte versuchen Sie es später noch einmal.",
"connectionFailed": "Verbindung fehlgeschlagen. Bitte versuchen Sie es später noch einmal!",
"dataChannelClosed": "Schlechte Videoqualität",
"dataChannelClosedDescription": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher ist die Videoqulität auf die schlechteste Stufe limitiert.",
"dataChannelClosedDescriptionWithAudio": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher können Video- und Tonprobleme auftreten.",
@@ -757,9 +758,9 @@
"gifsMenu": "GIPHY",
"groupTitle": "Benachrichtigungen",
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
"invalidTenant": "Ungültiger Tenant",
"invalidTenantHyphenDescription": "Der von Ihnen genutzte Tenantname ist unfültig (beginnt oder endet mit '-').",
"invalidTenantLengthDescription": "Der von Ihnen genutzte Tenantname ist zu lang.",
"invalidTenant": "Ungültiger Mandant",
"invalidTenantHyphenDescription": "Der gewählte Mandantenname ist ungültig (beginnt oder endet mit '-').",
"invalidTenantLengthDescription": "Der gewählte Mandantenname ist zu lang.",
"invitedOneMember": "{{name}} wurde eingeladen",
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
@@ -1059,7 +1060,7 @@
"onBy": "{{name}} startete die Aufnahme",
"onlyRecordSelf": "Nur eigenes Kamerabild und Ton aufzeichnen",
"pending": "Aufzeichnung der Konferenz wird vorbereitet…",
"policyError": "Sie haben die Aufzeichnung zu früh gestartet. Bitte versuchen Sie es später noch einmal.",
"policyError": "Sie haben die Aufzeichnung zu schnell gestartet. Bitte versuchen Sie es später noch einmal.",
"recordAudioAndVideo": "Kamera und Ton aufzeichnen",
"recordTranscription": "Transkription aufzeichnen",
"saveLocalRecording": "Aufzeichnung lokal abspeichern",
@@ -1082,10 +1083,10 @@
"pullToRefresh": "Ziehen, um zu aktualisieren"
},
"security": {
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem Passwort gesichert werden. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"insecureRoomNameWarningNative": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten. {{recommendAction}} Lernen Sie mehr über die Absicherung Ihrer Konferenz ",
"insecureRoomNameWarningWeb": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten {{recommendAction}} Lernen Sie <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">hier</a> mehr über die Absicherung Ihrer Konferenz.",
"insecureRoomNameWarningNative": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten. {{recommendAction}} Lernen Sie mehr über die Absicherung Ihrer Konferenz ",
"insecureRoomNameWarningWeb": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten {{recommendAction}} Lernen Sie <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">hier</a> mehr über die Absicherung Ihrer Konferenz.",
"title": "Sicherheitsoptionen",
"unsafeRoomActions": {
"meeting": "Erwägen Sie die Absicherung Ihrer Konferenz über den Sicherheits-Button.",
@@ -1185,6 +1186,7 @@
"fearful": "Ängstlich",
"happy": "Fröhlich",
"hours": "{{count}} Std. ",
"labelTooltip": "Anzahl der Personen: {{count}}",
"minutes": "{{count}} Min. ",
"name": "Name",
"neutral": "Neutral",

View File

@@ -371,6 +371,7 @@
"sendPrivateMessageTitle": "Invio privatamente?",
"serviceUnavailable": "Servizio non disponibile",
"sessTerminated": "Chiamata terminata",
"sessTerminatedReason": "La chiamata è stata terminata",
"sessionRestarted": "Chiamata riavviata automaticamente",
"shareAudio": "Continue",
"shareAudioTitle": "Come condividere l'audio",

1584
lang/main-no.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -263,6 +263,7 @@
"Remove": "Remove",
"Share": "Share",
"Submit": "Submit",
"Understand": "I understand",
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
"WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.",
"WaitingForHostButton": "Wait for moderator",
@@ -393,6 +394,8 @@
"recentlyUsedObjects": "Your recently used objects",
"recording": "Recording",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Not possible while a live stream is active",
"recordingInProgressDescription": "This meeting is being recorded. Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
"recordingInProgressTitle": "Recording in progress",
"rejoinNow": "Rejoin now",
"remoteControlAllowedMessage": "{{user}} accepted your remote control request!",
"remoteControlDeniedMessage": "{{user}} rejected your remote control request!",

View File

@@ -596,9 +596,13 @@ function initCommands() {
* Defaults to "normal" if not provided.
* @param { string } arg.timeout - Timeout type, either `short`, `medium`, `long` or `sticky`.
* Defaults to "short" if not provided.
* @param { Array<Object> } arg.customActions - An array of custom actions to be displayed in the notification.
* Each object should have a `label` and a `uuid` property. It should be used along a listener
* for the `customNotificationActionTriggered` event to handle the custom action.
* @returns {void}
*/
'show-notification': ({
customActions = [],
title,
description,
uid,
@@ -620,7 +624,15 @@ function initCommands() {
return;
}
const handlers = customActions.map(({ uuid }) => () => {
APP.API.notifyCustomNotificationActionTriggered(uuid);
});
const keys = customActions.map(({ label }) => label);
APP.store.dispatch(showNotification({
customActionHandler: handlers,
customActionNameKey: keys,
uid,
title,
description,
@@ -1501,6 +1513,21 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that a custom notification action has been triggered.
*
* @param {string} actionUuid - The UUID of the action that has been triggered.
* @returns {void}
*/
notifyCustomNotificationActionTriggered(actionUuid) {
this._sendEvent({
name: 'custom-notification-action-triggered',
data: {
id: actionUuid
}
});
}
/**
* Notify external application (if API is enabled) that the list of sharing participants changed.
*

View File

@@ -114,6 +114,7 @@ const events = {
'compute-pressure-changed': 'computePressureChanged',
'conference-created-timestamp': 'conferenceCreatedTimestamp',
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
'custom-notification-action-triggered': 'customNotificationActionTriggered',
'data-channel-closed': 'dataChannelClosed',
'data-channel-opened': 'dataChannelOpened',
'device-list-changed': 'deviceListChanged',

View File

@@ -120,7 +120,9 @@ UI.unbindEvents = () => {
* @param {string} name etherpad id
*/
UI.initEtherpad = name => {
const etherpadBaseUrl = sanitizeUrl(config.etherpad_base);
const { getState, dispatch } = APP.store;
const configState = getState()['features/base/config'];
const etherpadBaseUrl = sanitizeUrl(configState.etherpad_base);
if (etherpadManager || !etherpadBaseUrl || !name) {
return;
@@ -131,9 +133,9 @@ UI.initEtherpad = name => {
const url = new URL(name, etherpadBaseUrl);
APP.store.dispatch(setDocumentUrl(url.toString()));
dispatch(setDocumentUrl(url.toString()));
if (config.openSharedDocumentOnJoin) {
if (configState.openSharedDocumentOnJoin) {
etherpadManager.toggleEtherpad();
}
};

10
package-lock.json generated
View File

@@ -62,7 +62,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -16909,8 +16909,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/lib-jitsi-meet.tgz",
"integrity": "sha512-erPBz93xzWDIvW9EdvSfiraHFi0TMo1W68zxe7rKvIQWX1DCjmKxWKnxdq5WirSD7MXwoSIxgdX4PB7Wz3aTmg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
"integrity": "sha512-0/rTgoaaXwKs4J2+MY4HYh/VbZg3gjNHInhAz+smZGlWsJB8H2qkSNVU0HcTI7WG5LzrzkX4c/eTVpkq8ljLJw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -37637,8 +37637,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/lib-jitsi-meet.tgz",
"integrity": "sha512-erPBz93xzWDIvW9EdvSfiraHFi0TMo1W68zxe7rKvIQWX1DCjmKxWKnxdq5WirSD7MXwoSIxgdX4PB7Wz3aTmg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
"integrity": "sha512-0/rTgoaaXwKs4J2+MY4HYh/VbZg3gjNHInhAz+smZGlWsJB8H2qkSNVU0HcTI7WG5LzrzkX4c/eTVpkq8ljLJw==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",

View File

@@ -68,7 +68,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1919.0.0+d4a47d0e/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -228,7 +228,9 @@
"test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
"test-dev-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts --spec",
"test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts",
"test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec"
"test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec",
"test-grid-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.firefox.conf.ts",
"test-grid-ff-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.firefox.conf.ts --spec"
},
"resolutions": {
"@types/react": "17.0.14",

View File

@@ -152,30 +152,6 @@ export async function createHandlers({ getState }: IStore) {
return handlers;
}
/**
* Checks whether a url is a data URL or not.
*
* @param {string} url - The URL to be checked.
* @returns {boolean}
*/
function isDataURL(url?: string): boolean {
if (typeof url !== 'string') { // The icon will be ignored
return false;
}
try {
const urlObject = new URL(url);
if (urlObject.protocol === 'data:') {
return false;
}
} catch {
return false;
}
return true;
}
/**
* Inits JitsiMeetJS.analytics by setting permanent properties and setting the handlers from the loaded scripts.
* NOTE: Has to be used after JitsiMeetJS.init. Otherwise analytics will be null.
@@ -208,15 +184,9 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
isPromotedFromVisitor?: boolean;
isVisitor?: boolean;
overwritesCustomButtonsWithURL?: boolean;
overwritesCustomParticipantButtonsWithURL?: boolean;
overwritesDefaultLogoUrl?: boolean;
overwritesDeploymentUrls?: boolean;
overwritesEtherpadBase?: boolean;
overwritesHosts?: boolean;
overwritesIceServers?: boolean;
overwritesLiveStreamingUrls?: boolean;
overwritesPeopleSearchUrl?: boolean;
overwritesPrejoinConfigICEUrl?: boolean;
overwritesSalesforceUrl?: boolean;
overwritesSupportUrl?: boolean;
server?: string;
@@ -260,19 +230,8 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
// TODO: Temporary metric. To be removed once we don't need it.
permanentProperties.overwritesSupportUrl = 'interfaceConfig.SUPPORT_URL' in params;
permanentProperties.overwritesSalesforceUrl = 'config.salesforceUrl' in params;
permanentProperties.overwritesPeopleSearchUrl = 'config.peopleSearchUrl' in params;
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
permanentProperties.overwritesEtherpadBase = 'config.etherpad_base' in params;
const hosts = params['config.hosts'] ?? {};
const hostsProps = [ 'anonymousdomain', 'authdomain', 'domain', 'focus', 'muc', 'visitorFocus' ];
permanentProperties.overwritesHosts = 'config.hosts' in params
|| Boolean(hostsProps.find(p => `config.hosts.${p}` in params || (typeof hosts === 'object' && p in hosts)));
const prejoinConfig = params['config.prejoinConfig'] ?? {};
permanentProperties.overwritesPrejoinConfigICEUrl = ('config.prejoinConfig.preCallTestICEUrl' in params)
|| (typeof prejoinConfig === 'object' && 'preCallTestICEUrl' in prejoinConfig);
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
permanentProperties.overwritesDeploymentUrls
@@ -294,17 +253,7 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
)
);
permanentProperties.overwritesIceServers = Boolean(Object.keys(params).find(k => k.startsWith('iceServers')));
const customToolbarButtons = params['config.customToolbarButtons'] ?? [];
permanentProperties.overwritesCustomButtonsWithURL = Boolean(
customToolbarButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
const customParticipantMenuButtons = params['config.customParticipantMenuButtons'] ?? [];
permanentProperties.overwritesCustomParticipantButtonsWithURL = Boolean(
customParticipantMenuButtons.find(({ icon }: { icon: string; }) => isDataURL(icon)));
permanentProperties.overwritesCustomButtonsWithURL = 'config.customToolbarButtons' in params;
// Optionally, include local deployment information based on the
// contents of window.config.deploymentInfo.

View File

@@ -75,7 +75,7 @@ export const _getTokenAuthState = (
const params = parseURLParams(locationURL);
for (const key of Object.keys(params)) {
// we allow only config and interfaceConfig overrides in the state
// we allow only config, interfaceConfig and iceServers overrides in the state
if (key.startsWith('config.') || key.startsWith('interfaceConfig.') || key.startsWith('iceServers.')) {
// @ts-ignore
state[key] = params[key];

View File

@@ -542,6 +542,7 @@ export interface IConfig {
recordingSharingUrl?: string;
recordings?: {
recordAudioAndVideo?: boolean;
requireConsent?: boolean;
showPrejoinWarning?: boolean;
showRecordingLink?: boolean;
suggestRecording?: boolean;

View File

@@ -1,4 +1,7 @@
import { inIframe } from '../util/iframeUtils';
import extraConfigWhitelist from './extraConfigWhitelist';
import inIframeConfigWhitelist from './inIframeConfigWhitelist';
/**
* The config keys to whitelist, the keys that can be overridden.
@@ -77,7 +80,6 @@ export default [
'channelLastN',
'connectionIndicators',
'constraints',
'customToolbarButtons',
'deeplinking.disabled',
'deeplinking.desktop.enabled',
'defaultLocalDisplayName',
@@ -203,7 +205,10 @@ export default [
'remoteVideoMenu',
'roomPasswordNumberOfDigits',
'readOnlyName',
'recordings',
'recordings.recordAudioAndVideo',
'recordings.showPrejoinWarning',
'recordings.showRecordingLink',
'recordings.suggestRecording',
'replaceParticipant',
'resolution',
'screenshotCapture',
@@ -243,4 +248,4 @@ export default [
'webrtcIceTcpDisable',
'webrtcIceUdpDisable',
'whiteboard.enabled'
].concat(extraConfigWhitelist);
].concat(extraConfigWhitelist).concat(inIframe() ? inIframeConfigWhitelist : []);

View File

@@ -327,6 +327,49 @@ export function setConfigFromURLParams(
}
overrideConfigJSON(config, interfaceConfig, json);
// Print warning about depricated URL params
if ('interfaceConfig.SUPPORT_URL' in params) {
logger.warn('Using SUPPORT_URL interfaceConfig URL overwrite is deprecated.'
+ ' Please use supportUrl from advanced branding!');
}
if ('config.defaultLogoUrl' in params) {
logger.warn('Using defaultLogoUrl config URL overwrite is deprecated.'
+ ' Please use logoImageUrl from advanced branding!');
}
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
if ('config.deploymentUrls.downloadAppsUrl' in params || 'config.deploymentUrls.userDocumentationURL' in params
|| (typeof deploymentUrlsConfig === 'object'
&& ('downloadAppsUrl' in deploymentUrlsConfig || 'userDocumentationURL' in deploymentUrlsConfig))) {
logger.warn('Using deploymentUrls config URL overwrite is deprecated.'
+ ' Please use downloadAppsUrl and/or userDocumentationURL from advanced branding!');
}
const liveStreamingConfig = params['config.liveStreaming'] ?? {};
if (('interfaceConfig.LIVE_STREAMING_HELP_LINK' in params)
|| ('config.liveStreaming.termsLink' in params)
|| ('config.liveStreaming.dataPrivacyLink' in params)
|| ('config.liveStreaming.helpLink' in params)
|| (typeof params['config.liveStreaming'] === 'object' && 'config.liveStreaming' in params
&& (
'termsLink' in liveStreamingConfig
|| 'dataPrivacyLink' in liveStreamingConfig
|| 'helpLink' in liveStreamingConfig
)
)) {
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
}
if ('config.customToolbarButtons' in params) {
logger.warn('Using customToolbarButtons config URL overwrite is deprecated.'
+ ' Please use liveStreaming from advanced branding!');
}
}
/* eslint-enable max-params */

View File

@@ -0,0 +1,4 @@
/**
* Additional config whitelist extending the original whitelist in the case where jitsi-meet is loaded in an iframe.
*/
export default [];

View File

@@ -0,0 +1,5 @@
/**
* Additional interface config whitelist extending the original whitelist in the case where jitsi-meet is loaded in an
* iframe.
*/
export default [];

View File

@@ -1,4 +1,7 @@
import { inIframe } from '../util/iframeUtils';
import extraInterfaceConfigWhitelistCopy from './extraInterfaceConfigWhitelist';
import inIframeInterfaceConfigWhitelist from './inIframeInterfaceConfigWhitelist';
/**
* The interface config keys to whitelist, the keys that can be overridden.
@@ -51,4 +54,4 @@ export default [
'VERTICAL_FILMSTRIP',
'VIDEO_LAYOUT_FIT',
'VIDEO_QUALITY_LABEL_DISABLED'
].concat(extraInterfaceConfigWhitelistCopy);
].concat(extraInterfaceConfigWhitelistCopy).concat(inIframe() ? inIframeInterfaceConfigWhitelist : []);

View File

@@ -114,11 +114,15 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: AnyAction) {
const config: IConfig = {};
const {
customParticipantMenuButtons,
customToolbarButtons,
downloadAppsUrl,
etherpadBase,
liveStreamingDialogUrls = {},
preCallTest = {},
salesforceUrl,
userDocumentationUrl
userDocumentationUrl,
peopleSearchUrl,
} = action.value;
const { helpUrl, termsUrl, dataPrivacyUrl } = liveStreamingDialogUrls;
@@ -154,6 +158,10 @@ function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: A
config.salesforceUrl = salesforceUrl;
}
if (peopleSearchUrl) {
config.peopleSearchUrl = peopleSearchUrl;
}
const { enabled, iceUrl } = preCallTest;
if (typeof enabled === 'boolean') {
@@ -162,11 +170,24 @@ function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: A
};
}
if (etherpadBase) {
// eslint-disable-next-line camelcase
config.etherpad_base = etherpadBase;
}
if (iceUrl) {
config.prejoinConfig = config.prejoinConfig || {};
config.prejoinConfig.preCallTestICEUrl = iceUrl;
}
if (customToolbarButtons) {
config.customToolbarButtons = customToolbarButtons;
}
if (customParticipantMenuButtons) {
config.customParticipantMenuButtons = customParticipantMenuButtons;
}
dispatch(updateConfig(config));
return next(action);

View File

@@ -5,6 +5,7 @@ import { conferenceLeft, conferenceWillLeave, redirect } from '../conference/act
import { getCurrentConference } from '../conference/functions';
import { IConfigState } from '../config/reducer';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { inIframe } from '../util/iframeUtils';
import { parseURLParams } from '../util/parseURLParams';
import {
appendURLParam,
@@ -119,7 +120,8 @@ export function constructOptions(state: IReduxState) {
const params = parseURLParams(locationURL || '');
const iceServersOverride = params['iceServers.replace'];
if (iceServersOverride) {
// Allow iceServersOverride only when jitsi-meet is in an iframe.
if (inIframe() && iceServersOverride) {
options.iceServersOverride = iceServersOverride;
}

View File

@@ -36,6 +36,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
descriptionKey?: string | { key: string; params: string; };
/**
* Whether the cancel button is hidden.
*/
isCancelHidden?: Boolean;
/**
* Whether or not the nature of the confirm button is destructive.
*/
@@ -100,6 +105,7 @@ class ConfirmDialog extends AbstractDialog<IProps> {
cancelLabel,
children,
confirmLabel,
isCancelHidden,
isConfirmDestructive,
isConfirmHidden,
t,
@@ -121,10 +127,12 @@ class ConfirmDialog extends AbstractDialog<IProps> {
}
{ this._renderDescription() }
{ children }
<Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
{
!isCancelHidden && <Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
}
{
!isConfirmHidden && <Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }

View File

@@ -4,6 +4,12 @@
*/
export const ADD_PEOPLE_ENABLED = 'add-people.enabled';
/**
* Flag indicating if the audio device button should be displayed.
* Default: enabled (true).
*/
export const AUDIO_DEVICE_BUTTON_ENABLED = 'audio-device-button.enabled';
/**
* Flag indicating if the SDK should not require the audio focus.
* Used by apps that do not use Jitsi audio.
@@ -239,10 +245,16 @@ export const SETTINGS_ENABLED = 'settings.enabled';
/**
* Flag indicating if tile view feature should be enabled.
* Default: enabled.
* Default: enabled(true).
*/
export const TILE_VIEW_ENABLED = 'tile-view.enabled';
/**
* Flag indicating if the toggle camera button should be enabled
* Default: enabled(true).
*/
export const TOGGLE_CAMERA_BUTTON_ENABLED = 'toggle-camera-button.enabled';
/**
* Flag indicating if the toolbox should be always be visible
* Default: disabled (false).

View File

@@ -15,6 +15,7 @@ import { connect, useDispatch } from 'react-redux';
import { appNavigate } from '../../../app/actions.native';
import { IReduxState, IStore } from '../../../app/types';
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
import { isDisplayNameVisible } from '../../../base/config/functions.native';
import { FULLSCREEN_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import Container from '../../../base/react/components/native/Container';
@@ -100,6 +101,11 @@ interface IProps extends AbstractProps {
*/
_fullscreenEnabled: boolean;
/**
* The indicator which determines if the display name is visible.
*/
_isDisplayNameVisible: boolean;
/**
* The indicator which determines if the participants pane is open.
*/
@@ -364,6 +370,7 @@ class Conference extends AbstractConference<IProps, State> {
_aspectRatio,
_connecting,
_filmstripVisible,
_isDisplayNameVisible,
_largeVideoParticipantId,
_reducedUI,
_shouldDisplayTileView,
@@ -420,10 +427,12 @@ class Conference extends AbstractConference<IProps, State> {
{
_shouldDisplayTileView
|| <Container style = { styles.displayNameContainer }>
<DisplayNameLabel
participantId = { _largeVideoParticipantId } />
</Container>
|| (_isDisplayNameVisible && (
<Container style = { styles.displayNameContainer }>
<DisplayNameLabel
participantId = { _largeVideoParticipantId } />
</Container>
))
}
{ !_shouldDisplayTileView && <LonelyMeetingExperience /> }
@@ -577,6 +586,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_connecting: isConnecting(state),
_filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isDisplayNameVisible: isDisplayNameVisible(state),
_isParticipantsPaneOpen: isOpen,
_largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: isPipEnabled(state),

View File

@@ -82,6 +82,7 @@ class LonelyMeetingExperience extends PureComponent<IProps> {
render() {
const {
_inviteOthersControl,
_isAddPeopleFeatureEnabled,
_isInBreakoutRoom,
_isInviteFunctionsDisabled,
_isLonelyMeeting,
@@ -89,7 +90,7 @@ class LonelyMeetingExperience extends PureComponent<IProps> {
} = this.props;
const { color, shareDialogVisible } = _inviteOthersControl;
if (!_isLonelyMeeting) {
if (!_isLonelyMeeting || !_isAddPeopleFeatureEnabled) {
return null;
}

View File

@@ -4,13 +4,17 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getConferenceName, getConferenceTimestamp } from '../../../base/conference/functions';
import { CONFERENCE_TIMER_ENABLED } from '../../../base/flags/constants';
import {
AUDIO_DEVICE_BUTTON_ENABLED,
CONFERENCE_TIMER_ENABLED,
TOGGLE_CAMERA_BUTTON_ENABLED
} from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import AudioDeviceToggleButton from '../../../mobile/audio-mode/components/AudioDeviceToggleButton';
import PictureInPictureButton from '../../../mobile/picture-in-picture/components/PictureInPictureButton';
import ParticipantsPaneButton from '../../../participants-pane/components/native/ParticipantsPaneButton';
import { isParticipantsPaneEnabled } from '../../../participants-pane/functions';
import { isRoomNameEnabled } from '../../../prejoin/functions';
import { isRoomNameEnabled } from '../../../prejoin/functions.native';
import ToggleCameraButton from '../../../toolbox/components/native/ToggleCameraButton';
import { isToolboxVisible } from '../../../toolbox/functions.native';
import ConferenceTimer from '../ConferenceTimer';
@@ -21,6 +25,11 @@ import styles from './styles';
interface IProps {
/**
* Whether the audio device button should be displayed.
*/
_audioDeviceButtonEnabled: boolean;
/**
* Whether displaying the current conference timer is enabled or not.
*/
@@ -47,6 +56,11 @@ interface IProps {
*/
_roomNameEnabled: boolean;
/**
* Whether the toggle camera button should be displayed.
*/
_toggleCameraButtonEnabled: boolean;
/**
* True if the navigation bar should be visible.
*/
@@ -95,12 +109,18 @@ const TitleBar = (props: IProps) => {
{/* eslint-disable-next-line react/jsx-no-bind */}
<Labels createOnPress = { props._createOnPress } />
</View>
<View style = { styles.titleBarButtonContainer }>
<ToggleCameraButton styles = { styles.titleBarButton } />
</View>
<View style = { styles.titleBarButtonContainer }>
<AudioDeviceToggleButton styles = { styles.titleBarButton } />
</View>
{
props._toggleCameraButtonEnabled
&& <View style = { styles.titleBarButtonContainer }>
<ToggleCameraButton styles = { styles.titleBarButton } />
</View>
}
{
props._audioDeviceButtonEnabled
&& <View style = { styles.titleBarButtonContainer }>
<AudioDeviceToggleButton styles = { styles.titleBarButton } />
</View>
}
{
_isParticipantsPaneEnabled
&& <View style = { styles.titleBarButtonContainer }>
@@ -123,11 +143,13 @@ function _mapStateToProps(state: IReduxState) {
const startTimestamp = getConferenceTimestamp(state);
return {
_audioDeviceButtonEnabled: getFeatureFlag(state, AUDIO_DEVICE_BUTTON_ENABLED, true),
_conferenceTimerEnabled:
Boolean(getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !hideConferenceTimer && startTimestamp),
_isParticipantsPaneEnabled: isParticipantsPaneEnabled(state),
_meetingName: getConferenceName(state),
_roomNameEnabled: isRoomNameEnabled(state),
_toggleCameraButtonEnabled: getFeatureFlag(state, TOGGLE_CAMERA_BUTTON_ENABLED, true),
_visible: isToolboxVisible(state)
};
}

View File

@@ -22,15 +22,19 @@ MiddlewareRegistry.register(store => next => action => {
backgroundColor,
backgroundImageUrl,
brandedIcons,
customParticipantMenuButtons,
customToolbarButtons,
didPageUrl,
downloadAppsUrl,
etherpadBase,
inviteDomain,
labels,
liveStreamingDialogUrls,
peopleSearchUrl,
salesforceUrl,
sharedVideoAllowedURLDomains,
supportUrl,
userDocumentationUrl
userDocumentationUrl,
} = action.value;
action.value = {
@@ -38,11 +42,15 @@ MiddlewareRegistry.register(store => next => action => {
backgroundColor,
backgroundImageUrl,
brandedIcons,
customParticipantMenuButtons,
customToolbarButtons,
didPageUrl,
downloadAppsUrl,
etherpadBase,
inviteDomain,
labels,
liveStreamingDialogUrls,
peopleSearchUrl,
salesforceUrl,
sharedVideoAllowedURLDomains,
supportUrl,

View File

@@ -158,6 +158,7 @@ export interface IDynamicBrandingState {
logoImageUrl: string;
muiBrandedTheme?: boolean;
premeetingBackground: string;
requireRecordingConsent?: boolean;
sharedVideoAllowedURLDomains?: Array<string>;
showGiphyIntegration?: boolean;
supportUrl?: string;
@@ -186,6 +187,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
premeetingBackground,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
supportUrl,
virtualBackgrounds
} = action.value;
@@ -205,6 +207,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
premeetingBackground,
sharedVideoAllowedURLDomains,
showGiphyIntegration,
requireRecordingConsent,
supportUrl,
customizationFailed: false,
customizationReady: true,

View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
// @ts-expect-error
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { IReduxState, IStore } from '../../app/types';
import { isDisplayNameVisible } from '../../base/config/functions.web';
import { VIDEO_TYPE } from '../../base/media/constants';
import { getLocalParticipant } from '../../base/participants/functions';
import Watermarks from '../../base/react/components/web/Watermarks';
@@ -58,6 +59,11 @@ interface IProps {
*/
_isChatOpen: boolean;
/**
* Whether or not the display name is visible.
*/
_isDisplayNameVisible: boolean;
/**
* Whether or not the local screen share is on large-video.
*/
@@ -191,6 +197,7 @@ class LargeVideo extends Component<IProps> {
const {
_displayScreenSharingPlaceholder,
_isChatOpen,
_isDisplayNameVisible,
_noAutoPlayVideo,
_showDominantSpeakerBadge,
_whiteboardEnabled
@@ -243,7 +250,12 @@ class LargeVideo extends Component<IProps> {
</div>
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|| <Captions /> }
{_showDominantSpeakerBadge && <StageParticipantNameLabel />}
{
_isDisplayNameVisible
&& (
_showDominantSpeakerBadge && <StageParticipantNameLabel />
)
}
</div>
);
}
@@ -368,6 +380,7 @@ function _mapStateToProps(state: IReduxState) {
_displayScreenSharingPlaceholder: Boolean(isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isOnSpot),
_hideSelfView: getHideSelfView(state),
_isChatOpen: isChatOpen,
_isDisplayNameVisible: isDisplayNameVisible(state),
_isScreenSharing: Boolean(isLocalScreenshareOnLargeVideo),
_largeVideoParticipantId: largeVideoParticipant?.id ?? '',
_localParticipantId: localParticipantId ?? '',

View File

@@ -68,12 +68,16 @@ function _toDateString(itemDate: number, t: Function) {
const dateInMs = date.getTime();
const now = new Date();
const todayInMs = new Date().setHours(0, 0, 0, 0);
const yesterdayInMs = todayInMs - 86400000; // 1 day = 86400000ms
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const yesterdayInMs = yesterday.getTime();
if (dateInMs >= todayInMs) {
return m.fromNow();
} else if (dateInMs >= yesterdayInMs) {
return t('dateUtils.yesterday');
return `${t('dateUtils.yesterday')}, ${m.format('h:mm A')}`;
} else if (date.getFullYear() !== now.getFullYear()) {
// We only want to include the year in the date if its not the current
// year.

View File

@@ -1 +1,2 @@
export { default as StartRecordingDialog } from './native/StartRecordingDialog';
export { default as RecordingConsentDialog } from './native/RecordingConsentDialog';

View File

@@ -1 +1,2 @@
export { default as StartRecordingDialog } from './web/StartRecordingDialog';
export { default as RecordingConsentDialog } from './web/RecordingConsentDialog';

View File

@@ -0,0 +1,28 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDialog';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
/**
* Component that renders the dialog for explicit consent for recordings.
*
* @returns {JSX.Element}
*/
export default function RecordingConsentDialog() {
const dispatch = useDispatch();
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
}, []);
return (
<ConfirmDialog
confirmLabel = { 'dialog.Understand' }
descriptionKey = { 'dialog.recordingInProgressDescription' }
isCancelHidden = { true }
onSubmit = { consent }
title = { 'dialog.recordingInProgressTitle' } />
);
}

View File

@@ -0,0 +1,34 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { setAudioUnmutePermissions, setVideoUnmutePermissions } from '../../../../base/media/actions';
import Dialog from '../../../../base/ui/components/web/Dialog';
/**
* Component that renders the dialog for explicit consent for recordings.
*
* @returns {JSX.Element}
*/
export default function RecordingConsentDialog() {
const { t } = useTranslation();
const dispatch = useDispatch();
const consent = useCallback(() => {
dispatch(setAudioUnmutePermissions(false, true));
dispatch(setVideoUnmutePermissions(false, true));
}, []);
return (
<Dialog
cancel = {{ hidden: true }}
disableBackdropClose = { true }
ok = {{ translationKey: 'dialog.Understand' }}
onSubmit = { consent }
titleKey = 'dialog.recordingInProgressTitle'>
<div>
{t('dialog.recordingInProgressDescription')}
</div>
</Dialog>
);
}

View File

@@ -437,3 +437,26 @@ export function isLiveStreamingButtonVisible({
}) {
return !isInBreakoutRoom && liveStreamingEnabled && liveStreamingAllowed;
}
/**
* Whether the RecordingConsentDialog should be displayed.
*
* @param {any} recorderSession - The recorder session.
* @param {IReduxState} state - The Redux state.
* @returns {boolean}
*/
export function shouldRequireRecordingConsent(recorderSession: any, state: IReduxState) {
const { requireRecordingConsent } = state['features/dynamic-branding'] || {};
const { requireConsent } = state['features/base/config'].recordings || {};
if (!requireConsent && !requireRecordingConsent) {
return false;
}
if (!recorderSession._initiator
|| recorderSession._statusFromJicofo === JitsiRecordingConstants.status.OFF) {
return false;
}
return recorderSession._initiator !== getLocalParticipant(state)?.id;
}

View File

@@ -1,13 +1,22 @@
import { batch } from 'react-redux';
import { createRecordingEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { openDialog } from '../base/dialog/actions';
import JitsiMeetJS, {
JitsiConferenceEvents,
JitsiRecordingConstants
} from '../base/lib-jitsi-meet';
import {
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../base/media/actions';
import { MEDIA_TYPE } from '../base/media/constants';
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { updateLocalRecordingStatus } from '../base/participants/actions';
@@ -37,6 +46,7 @@ import {
showStoppedRecordingNotification,
updateRecordingSessionData
} from './actions';
import { RecordingConsentDialog } from './components/Recording';
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
import {
LIVE_STREAMING_OFF_SOUND_ID,
@@ -49,6 +59,7 @@ import {
getResourceId,
getSessionById,
registerRecordingAudioFiles,
shouldRequireRecordingConsent,
unregisterRecordingAudioFiles
} from './functions';
import logger from './logger';
@@ -101,7 +112,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
(recorderSession: any) => {
if (recorderSession) {
recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
if (recorderSession.getError()) {
_showRecordingErrorNotification(recorderSession, dispatch, getState);
} else {
_showExplicitConsentDialog(recorderSession, dispatch, getState);
}
}
return;
@@ -390,3 +405,25 @@ function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatc
APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
}
}
/**
* Mutes audio and video and displays the RecordingConsentDialog when the conditions are met.
*
* @param {any} recorderSession - The recording session.
* @param {Function} dispatch - The Redux dispatch function.
* @param {Function} getState - The Redux getState function.
* @returns {void}
*/
function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
if (!shouldRequireRecordingConsent(recorderSession, getState())) {
return;
}
batch(() => {
dispatch(setAudioUnmutePermissions(true, true));
dispatch(setVideoUnmutePermissions(true, true));
dispatch(setAudioMuted(true));
dispatch(setVideoMuted(true));
dispatch(openDialog(RecordingConsentDialog));
});
}

View File

@@ -200,8 +200,14 @@ end
-- Managing breakout rooms
function create_breakout_room(room_jid, subject)
local main_room, main_room_jid = get_main_room(room_jid);
function create_breakout_room(orig_room, subject)
local main_room, main_room_jid = get_main_room(orig_room.jid);
if orig_room ~= main_room then
module:log('warn', 'Invalid create breakout room request for %s', orig_room.jid);
return;
end
local breakout_room_jid = uuid_gen() .. '@' .. breakout_rooms_muc_component_config;
if not main_room._data.breakout_rooms then
@@ -219,13 +225,18 @@ function create_breakout_room(room_jid, subject)
broadcast_breakout_rooms(main_room_jid);
end
function destroy_breakout_room(room_jid, message)
function destroy_breakout_room(orig_room, room_jid, message)
local main_room, main_room_jid = get_main_room(room_jid);
if room_jid == main_room_jid then
return;
end
if orig_room ~= main_room then
module:log('warn', 'Invalid destroy breakout room request for %s', orig_room.jid);
return;
end
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
if breakout_room then
@@ -244,13 +255,18 @@ function destroy_breakout_room(room_jid, message)
end
function rename_breakout_room(room_jid, name)
function rename_breakout_room(orig_room, room_jid, name)
local main_room, main_room_jid = get_main_room(room_jid);
if room_jid == main_room_jid then
return;
end
if orig_room ~= main_room then
module:log('warn', 'Invalid rename breakout room request for %s', orig_room.jid);
return;
end
if main_room then
if main_room._data.breakout_rooms then
main_room._data.breakout_rooms[room_jid] = name;
@@ -322,18 +338,25 @@ function on_message(event)
end
if message.attr.type == JSON_TYPE_ADD_BREAKOUT_ROOM then
create_breakout_room(room.jid, message.attr.subject);
create_breakout_room(room, message.attr.subject);
return true;
elseif message.attr.type == JSON_TYPE_REMOVE_BREAKOUT_ROOM then
destroy_breakout_room(message.attr.breakoutRoomJid);
destroy_breakout_room(room, message.attr.breakoutRoomJid);
return true;
elseif message.attr.type == JSON_TYPE_RENAME_BREAKOUT_ROOM then
rename_breakout_room(message.attr.breakoutRoomJid, message.attr.subject);
rename_breakout_room(room, message.attr.breakoutRoomJid, message.attr.subject);
return true;
elseif message.attr.type == JSON_TYPE_MOVE_TO_ROOM_REQUEST then
local participant_jid = message.attr.participantJid;
local target_room_jid = message.attr.roomJid;
if not room._data.breakout_rooms or not (
room._data.breakout_rooms[target_room_jid] or target_room_jid == internal_room_jid_match_rewrite(room.jid))
then
module:log('warn', 'Invalid breakout room %s for %s', target_room_jid, room.jid);
return false
end
local json_msg, error = json.encode({
type = BREAKOUT_ROOMS_IDENTITY_TYPE,
event = JSON_TYPE_MOVE_TO_ROOM_REQUEST,
@@ -342,6 +365,7 @@ function on_message(event)
if not json_msg then
module:log('error', 'skip sending request room:%s error:%s', room.jid, error);
return false
end
send_json_msg(participant_jid, json_msg)
@@ -491,7 +515,7 @@ function on_main_room_destroyed(event)
end
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
destroy_breakout_room(breakout_room_jid, event.reason)
destroy_breakout_room(main_room, breakout_room_jid, event.reason)
end
end

View File

@@ -2,6 +2,9 @@
# If there is a tenant in the URL it must end with a slash (e.g. "https://alpha.jitsi.net/sometenant/")
#BASE_URL=
# Room name suffix to use when creating new room names
# ROOM_NAME_SUFFIX=
# To be able to match a domain to a specific address
# The format is "MAP example.com 1.2.3.4"
#RESOLVER_RULES=
@@ -15,8 +18,8 @@
# The path to the browser video capture file
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
# The path to the helper iframe page that will be used for the iframeAPI tests
#IFRAME_PAGE_BASE=
# The tenant used when executing the iframeAPI tests, will override any tenant from BASE_URL if any
#IFRAME_TENANT=
# The grid host url (https://mygrid.com/wd/hub)
#GRID_HOST_URL=
@@ -26,6 +29,12 @@
# The kid to use in the token
#JWT_KID=
# An access token to use to create meetings (used for the first participant)
#JWT_ACCESS_TOKEN=
# The count of workers that execute the tests in parallel
# MAX_INSTANCES=1
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
#WEBHOOKS_PROXY_URL=
# A shared secret to authenticate the webhook proxy connection

View File

@@ -60,6 +60,30 @@ export class Participant {
analytics: {
disabled: true
},
// if there is a video file to play, use deployment config,
// otherwise use lower resolution to avoid high CPU usage
constraints: process.env.VIDEO_CAPTURE_FILE ? undefined : {
video: {
height: {
ideal: 360,
max: 360,
min: 180
},
// @ts-ignore
width: {
ideal: 640,
max: 640,
min: 320
},
frameRate: {
max: 30
}
}
},
resolution: process.env.VIDEO_CAPTURE_FILE ? undefined : 360,
requireDisplayName: false,
testing: {
testMode: true
@@ -195,7 +219,9 @@ export class Participant {
// @ts-ignore
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${ctx.roomName}"`;
if (baseUrl.pathname.length > 1) {
if (process.env.IFRAME_TENANT) {
url = `${url}&tenant="${process.env.IFRAME_TENANT}"`;
} else if (baseUrl.pathname.length > 1) {
// remove leading slash
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
}
@@ -206,8 +232,15 @@ export class Participant {
await this.driver.setTimeout({ 'pageLoad': 30000 });
let urlToLoad = url.startsWith('/') ? url.substring(1) : url;
if (options.forceGenerateToken && !ctx.iframeAPI && ctx.isJaasAvailable() && process.env.IFRAME_TENANT) {
// This to enables tests like invite, which can force using the jaas auth instead of the provided token
urlToLoad = `/${process.env.IFRAME_TENANT}/${urlToLoad}`;
}
// drop the leading '/' so we can use the tenant if any
await this.driver.url(url.startsWith('/') ? url.substring(1) : url);
await this.driver.url(urlToLoad);
await this.waitForPageToLoad();
@@ -221,17 +254,16 @@ export class Participant {
await this.waitToJoinMUC();
}
await this.postLoadProcess(options.skipInMeetingChecks);
await this.postLoadProcess();
}
/**
* Loads stuff after the page loads.
*
* @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
* @returns {Promise<void>}
* @private
*/
private async postLoadProcess(skipInMeetingChecks = false): Promise<void> {
private async postLoadProcess(): Promise<void> {
const driver = this.driver;
const parallel = [];
@@ -261,15 +293,6 @@ export class Participant {
}
}, this._name, driver.sessionId, LOG_PREFIX));
if (skipInMeetingChecks) {
await Promise.allSettled(parallel);
return;
}
parallel.push(this.waitForIceConnected());
parallel.push(this.waitForSendReceiveData());
await Promise.all(parallel);
}
@@ -594,7 +617,13 @@ export class Participant {
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
// change of url
await this.driver.waitUntil(
async () => current !== await this.driver.getUrl(),
async () => {
const u = await this.driver.getUrl();
console.log('url:', current, u);
return current !== u;
},
{
timeout: 5000,
timeoutMsg: `${this.name} did not leave the muc in 5s`

View File

@@ -9,13 +9,13 @@ export const LOG_PREFIX = '[MeetTest] ';
* Initialize logger for a driver.
*
* @param {WebdriverIO.Browser} driver - The driver.
* @param {string} name - The name of the participant.
* @param {string} fileName - The name of the file.
* @param {string} folder - The folder to save the file.
* @returns {void}
*/
export function initLogger(driver: WebdriverIO.Browser, name: string, folder: string) {
export function initLogger(driver: WebdriverIO.Browser, fileName: string, folder: string) {
// @ts-ignore
driver.logFile = `${folder}/${name}.log`;
driver.logFile = `${folder}/${fileName}.log`;
driver.sessionSubscribe({ events: [ 'log.entryAdded' ] });
driver.on('log.entryAdded', (entry: any) => {

View File

@@ -45,11 +45,19 @@ export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptio
})
]);
const { skipInMeetingChecks } = options;
if (options.skipInMeetingChecks) {
return Promise.resolve();
}
await Promise.all([
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(2),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(2)
ctx.p1.waitForIceConnected(),
ctx.p2.waitForIceConnected(),
ctx.p3.waitForIceConnected()
]);
await Promise.all([
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
]);
}
@@ -128,12 +136,21 @@ export async function ensureFourParticipants(ctx: IContext, options: IJoinOption
})
]);
const { skipInMeetingChecks } = options;
if (options.skipInMeetingChecks) {
return Promise.resolve();
}
await Promise.all([
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(3),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3),
skipInMeetingChecks ? Promise.resolve() : ctx.p3.waitForRemoteStreams(3)
ctx.p1.waitForIceConnected(),
ctx.p2.waitForIceConnected(),
ctx.p3.waitForIceConnected(),
ctx.p4.waitForIceConnected()
]);
await Promise.all([
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
ctx.p4.waitForSendReceiveData().then(() => ctx.p4.waitForRemoteStreams(1)),
]);
}
@@ -148,9 +165,15 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
const p1DisplayName = P1_DISPLAY_NAME;
let token;
// if it is jaas create the first one to be moderator and second not moderator
if (ctx.jwtPrivateKeyPath && !options?.skipFirstModerator) {
token = getModeratorToken(p1DisplayName);
if (!options?.skipFirstModerator) {
// we prioritize the access token when iframe is not used and private key is set,
// otherwise if private key is not specified we use the access token if set
if (process.env.JWT_ACCESS_TOKEN
&& (ctx.jwtPrivateKeyPath && !ctx.iframeAPI && !options?.forceGenerateToken)) {
token = process.env.JWT_ACCESS_TOKEN;
} else if (ctx.jwtPrivateKeyPath) {
token = getModeratorToken(p1DisplayName);
}
}
// make sure the first participant is moderator, if supported by deployment
@@ -158,8 +181,7 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
ctx.p1 = p;
}, {
displayName: p1DisplayName,
...options,
skipInMeetingChecks: true
...options
}, token);
}
@@ -172,8 +194,6 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
await joinTheModeratorAsP1(ctx, options);
const { skipInMeetingChecks } = options;
await _joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
@@ -181,9 +201,17 @@ export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions
...options
});
if (options.skipInMeetingChecks) {
return Promise.resolve();
}
await Promise.all([
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
ctx.p1.waitForIceConnected(),
ctx.p2.waitForIceConnected()
]);
await Promise.all([
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1))
]);
}
@@ -253,7 +281,8 @@ export async function muteAudioAndCheck(testee: Participant, observer: Participa
* @param observer
*/
export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
await testee.getNotifications().closeAskToUnmuteNotification();
await testee.getNotifications().closeAskToUnmuteNotification(true);
await testee.getNotifications().closeAVModerationMutedNotification(true);
await testee.getToolbar().clickAudioUnmuteButton();
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);

View File

@@ -7,8 +7,10 @@ export type IContext = {
conferenceJid: string;
dialInPin: string;
iframeAPI: boolean;
isJaasAvailable: () => boolean;
jwtKid: string;
jwtPrivateKeyPath: string;
keepAlive: Array<any>;
p1: Participant;
p2: Participant;
p3: Participant;
@@ -32,6 +34,12 @@ export type IJoinOptions = {
*/
displayName?: string;
/**
* When joining the first participant and jwt singing material is available and a provided token
* is available, prefer generating a new token for the first participant.
*/
forceGenerateToken?: boolean;
/**
* Whether to skip setting display name.
*/

View File

@@ -46,10 +46,9 @@ export default class LargeVideo extends BasePageObject {
* Returns the source of the large video currently shown.
*/
getId() {
return this.participant.execute('return document.getElementById("largeVideo").srcObject.id');
return this.participant.execute(() => document.getElementById('largeVideo')?.srcObject?.id);
}
/**
* Checks if the large video is playing or not.
*

View File

@@ -25,6 +25,6 @@ export default class LobbyScreen extends PreMeetingScreen {
* Waits for lobby screen to load.
*/
waitForLoading(): Promise<void> {
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 4000 });
return this.participant.driver.$('.lobby-screen').waitForDisplayed({ timeout: 6000 });
}
}

View File

@@ -1,5 +1,6 @@
import BasePageObject from './BasePageObject';
const AV_MODERATION_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectTitle';
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
@@ -47,8 +48,15 @@ export default class Notifications extends BasePageObject {
/**
* Closes the ask to unmute notification.
*/
async closeAskToUnmuteNotification() {
return this.closeLobbyNotification(ASK_TO_UNMUTE_NOTIFICATION_ID);
async closeAVModerationMutedNotification(skipNonExisting = false) {
return this.closeNotification(AV_MODERATION_MUTED_NOTIFICATION_ID, skipNonExisting);
}
/**
* Closes the ask to unmute notification.
*/
async closeAskToUnmuteNotification(skipNonExisting = false) {
return this.closeNotification(ASK_TO_UNMUTE_NOTIFICATION_ID, skipNonExisting);
}
/**
@@ -86,6 +94,28 @@ export default class Notifications extends BasePageObject {
return this.getNotificationText(LOBBY_ENABLED_TEST_ID);
}
/**
* Closes a specific lobby notification.
* @param testId
* @param skipNonExisting
* @private
*/
private async closeNotification(testId: string, skipNonExisting = false) {
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);
if (skipNonExisting && !await notification.isExisting()) {
return Promise.resolve();
}
await notification.waitForExist();
await notification.waitForStable();
const closeButton = notification.$('#close-notification');
await closeButton.moveTo();
await closeButton.click();
}
/**
* Closes a specific lobby notification.
* @param testId

View File

@@ -112,7 +112,7 @@ export default class ParticipantsPane extends BasePageObject {
await this.openParticipantContextMenu(participantToUnmute);
const unmuteButton = this.participant.driver
.$(`button[data-testid="unmute-video-${participantId}"]`);
.$(`[data-testid="unmute-video-${participantId}"]`);
await unmuteButton.waitForExist();
await unmuteButton.click();

View File

@@ -17,6 +17,7 @@ export default class PasswordDialog extends BaseDialog {
timeoutMsg: 'Password dialog not found'
});
await input.waitForDisplayed();
await input.waitForStable();
}
/**

View File

@@ -44,7 +44,7 @@ export default abstract class PreMeetingScreen extends BasePageObject {
return this.participant.driver.waitUntil(
() => this.isLobbyRoomJoined(),
{
timeout: 3_000, // 3 seconds
timeout: 6_000, // 6 seconds
timeoutMsg: `Timeout waiting to join lobby for ${this.participant.name}`
}
);

View File

@@ -1,5 +1,3 @@
import { Key } from 'webdriverio';
import BaseDialog from './BaseDialog';
const ADD_PASSWORD_LINK = 'add-password';
@@ -118,21 +116,6 @@ export default class SecurityDialog extends BaseDialog {
await this.participant.driver.keys(password);
await this.participant.driver.$('button=Add').click();
let validationMessage;
// There are two cases here, validation is enabled and the field passwordEntry maybe there
// with validation failed, or maybe successfully hidden after setting the password
// So let's give it some time to act on any of the above
if (!await passwordEntry.isExisting()) {
// validation had failed on password field as it is still on the page
validationMessage = passwordEntry.getAttribute('validationMessage');
}
if (validationMessage) {
await this.participant.driver.keys([ Key.Escape ]);
expect(validationMessage).toBe('');
}
}
/**

View File

@@ -1,7 +1,7 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Audio only', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx, { skipFirstModerator: true }));
it('joining the meeting', () => ensureTwoParticipants(ctx));
/**
* Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted.

View File

@@ -37,6 +37,13 @@ describe('Participants presence', () => {
const { p1, p2, webhooksProxy } = ctx;
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
return;
}
// let's populate endpoint ids
await Promise.all([
p1.getEndpointId(),

View File

@@ -1,4 +1,5 @@
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
import type SecurityDialog from '../../pageobjects/SecurityDialog';
/**
* 1. Lock the room (make sure the image changes to locked)
@@ -41,7 +42,7 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(true);
await waitForRoomLockState(p2SecurityDialog, true);
});
it('unlock room', async () => {
@@ -63,7 +64,7 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p2SecurityDialog, false);
await p2SecurityDialog.clickCloseButton();
});
@@ -79,11 +80,11 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(true);
await waitForRoomLockState(p2SecurityDialog, true);
await participant1UnlockRoom();
expect(await p2SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p2SecurityDialog, false);
});
it('unlock after participant enter wrong password', async () => {
// P1 locks the room. Participant tries to enter using wrong password.
@@ -117,7 +118,7 @@ describe('Lock Room', () => {
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
expect(await p2SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p2SecurityDialog, false);
});
});
@@ -133,7 +134,7 @@ async function participant1LockRoom() {
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLocked()).toBe(false);
await waitForRoomLockState(p1SecurityDialog, false);
await p1SecurityDialog.addPassword(ctx.roomKey);
@@ -142,7 +143,7 @@ async function participant1LockRoom() {
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLocked()).toBe(true);
await waitForRoomLockState(p1SecurityDialog, true);
await p1SecurityDialog.clickCloseButton();
}
@@ -159,13 +160,22 @@ async function participant1UnlockRoom() {
await p1SecurityDialog.removePassword();
await p1.driver.waitUntil(
async () => !await p1SecurityDialog.isLocked(),
{
timeout: 3_000, // 3 seconds
timeoutMsg: `Timeout waiting for the room to unlock for ${p1.name}.`
}
);
await waitForRoomLockState(p1SecurityDialog, false);
await p1SecurityDialog.clickCloseButton();
}
/**
* Waits for the room to be locked or unlocked.
* @param securityDialog
* @param locked
*/
function waitForRoomLockState(securityDialog: SecurityDialog, locked: boolean) {
return securityDialog.participant.driver.waitUntil(
async () => await securityDialog.isLocked() === locked,
{
timeout: 3_000, // 3 seconds
timeoutMsg: `Timeout waiting for the room to unlock for ${securityDialog.participant.name}.`
}
);
}

View File

@@ -70,11 +70,18 @@ describe('AVModeration', () => {
// participant3 was unmuted by unmuteByModerator
await unmuteAudioAndCheck(p2, p1);
await unmuteVideoAndCheck(p2, p1);
await unmuteAudioAndCheck(p1, p2);
await unmuteVideoAndCheck(p1, p2);
// make sure p1 is not muted after turning on and then off the AV moderation
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2, true);
});
it('hangup and change moderator', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
return;
}
await Promise.all([ ctx.p2.hangup(), ctx.p3.hangup() ]);
await ensureThreeParticipants(ctx);
@@ -244,7 +251,6 @@ async function unmuteByModerator(
await moderatorParticipantsPane.allowVideo(participant);
await moderatorParticipantsPane.askToUnmute(participant, false);
await participant.getNotifications().waitForAskToUnmuteNotification();
await participant.getNotifications().closeAskToUnmuteNotification();
await unmuteAudioAndCheck(participant, moderator);
await unmuteVideoAndCheck(participant, moderator);

View File

@@ -10,10 +10,7 @@ const HASH = '38f014e4b7dde0f64f8157d26a8c812e';
describe('Avatar', () => {
it('setup the meeting', () =>
ensureTwoParticipants(ctx, {
skipDisplayName: true,
// no default avatar if we have used to join a token with an avatar and no option to set it
skipFirstModerator: true
skipDisplayName: true
})
);
@@ -122,9 +119,21 @@ describe('Avatar', () => {
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
// Start the third participant
await ensureThreeParticipants(ctx);
await ensureThreeParticipants(ctx, {
skipInMeetingChecks: true
});
const { p3 } = ctx;
// When the first participant is FF because of their audio mic feed it will never become dominant speaker
// and no audio track will be received by the third participant and video is muted,
// that's why we need to do a different check that expects any track just from p2
if (p1.driver.isFirefox) {
await Promise.all([ p2.waitForRemoteStreams(1), p3.waitForRemoteStreams(1) ]);
} else {
await Promise.all([ p2.waitForRemoteStreams(2), p3.waitForRemoteStreams(2) ]);
}
// Pin local video and verify avatars are displayed
await p3.getFilmstrip().pinParticipant(p3);
@@ -179,6 +188,12 @@ describe('Avatar', () => {
it('email persistence', async () => {
let { p1 } = ctx;
if (p1.driver.isFirefox) {
// strangely this test when FF is involved, missing source mapping from jvb
// and fails with an error of: expected number of remote streams:1 in 15s for participant1
return;
}
await p1.getToolbar().clickProfileButton();
expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
@@ -186,8 +201,7 @@ describe('Avatar', () => {
await p1.hangup();
await ensureTwoParticipants(ctx, {
skipDisplayName: true,
skipFirstModerator: true
skipDisplayName: true
});
p1 = ctx.p1;

View File

@@ -34,7 +34,7 @@ describe('BreakoutRooms', () => {
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'No breakout room added for p1'
});
@@ -42,7 +42,7 @@ describe('BreakoutRooms', () => {
// second participant should also see one breakout room
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'No breakout room seen by p2'
});
});
@@ -54,7 +54,7 @@ describe('BreakoutRooms', () => {
// there should be one breakout room
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 1000,
timeout: 3000,
timeoutMsg: 'No breakout room seen by p1'
});
@@ -80,7 +80,7 @@ describe('BreakoutRooms', () => {
return list[0].name === MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 did not join breakout room'
});
@@ -95,7 +95,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
});
});
@@ -122,7 +122,7 @@ describe('BreakoutRooms', () => {
return list[0].name !== MAIN_ROOM_NAME;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 did not leave breakout room'
});
@@ -137,7 +137,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P2 is seeing p1 in the breakout room'
});
});
@@ -152,14 +152,14 @@ describe('BreakoutRooms', () => {
// there should be no breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'Breakout room was not removed for p1'
});
// the second participant should also see no breakout rooms
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'Breakout room was not removed for p2'
});
});
@@ -176,7 +176,7 @@ describe('BreakoutRooms', () => {
// there should be two breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'Breakout room was not created by p1'
});
@@ -198,7 +198,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
});
@@ -220,7 +220,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1
&& (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P2 is not seeing p1 in the main room'
});
});
@@ -244,7 +244,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P1 is not seeing two breakout rooms'
});
@@ -266,7 +266,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 0 || list[1].participantCount === 0;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 is not seeing an empty breakout room'
});
@@ -305,7 +305,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount + list[1].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
});
@@ -335,7 +335,7 @@ describe('BreakoutRooms', () => {
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'No breakout room added for p1'
});
@@ -353,7 +353,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
});
@@ -373,7 +373,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
@@ -419,7 +419,7 @@ describe('BreakoutRooms', () => {
return list[0].name === myNewRoomName;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'The breakout room was not renamed for p1'
});
@@ -441,7 +441,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 0;
}, {
timeout: 2000,
timeout: 3000,
timeoutMsg: 'The breakout room not found or not empty for p1'
});
@@ -471,7 +471,7 @@ describe('BreakoutRooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 2000,
timeout: 5000,
timeoutMsg: 'The breakout room was not rename for p1'
});

View File

@@ -179,14 +179,13 @@ describe('Lobby', () => {
await enableLobby();
await enterLobby(p1);
// WebParticipant participant1 = getParticipant1();
const p1SecurityDialog = p1.getSecurityDialog();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
await p1SecurityDialog.toggleLobby();
await p1SecurityDialog.waitForLobbyEnabled();
await p1SecurityDialog.waitForLobbyEnabled(true);
const { p3 } = ctx;
@@ -196,6 +195,10 @@ describe('Lobby', () => {
});
it('change of moderators in lobby', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
return;
}
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
@@ -223,11 +226,11 @@ describe('Lobby', () => {
// here the important check is whether the moderator sees the knocking participant
await enterLobby(p2, false);
await hangupAllParticipants();
});
it('shared password', async () => {
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
const { p1 } = ctx;
@@ -241,7 +244,7 @@ describe('Lobby', () => {
expect(await p1SecurityDialog.isLocked()).toBe(false);
const roomPasscode = String(Math.random() * 1_000);
const roomPasscode = String(Math.trunc(Math.random() * 1_000_000));
await p1SecurityDialog.addPassword(roomPasscode);
@@ -284,6 +287,10 @@ describe('Lobby', () => {
});
it('moderator leaves while lobby enabled', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
return;
}
const { p1, p2, p3 } = ctx;
await p3.hangup();
@@ -303,7 +310,7 @@ describe('Lobby', () => {
});
it('reject and approve in pre-join', async () => {
await ctx.p2.hangup();
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
await enableLobby();

View File

@@ -188,17 +188,15 @@ describe('StartMuted', () => {
}
};
await Promise.all([
ensureOneParticipant(ctx, options),
joinSecondParticipant(ctx, {
configOverwrite: {
p2p: {
enabled: true
}
},
skipInMeetingChecks: true
})
]);
await ensureOneParticipant(ctx, options);
await joinSecondParticipant(ctx, {
configOverwrite: {
p2p: {
enabled: true
}
},
skipInMeetingChecks: true
});
const { p1, p2 } = ctx;

View File

@@ -2,7 +2,7 @@ import { ensureOneParticipant } from '../../helpers/participants';
import { isDialInEnabled } from '../helpers/DialIn';
describe('Invite', () => {
it('join participant', () => ensureOneParticipant(ctx));
it('join participant', () => ensureOneParticipant(ctx, { forceGenerateToken: true }));
it('url displayed', async () => {
const { p1 } = ctx;

View File

@@ -17,8 +17,6 @@ const allure = require('allure-commandline');
// we need it to be able to reuse jitsi-meet code in tests
require.extensions['.web.ts'] = require.extensions['.ts'];
const usingGrid = Boolean(new URL(import.meta.url).searchParams.get('grid'));
const chromeArgs = [
'--allow-insecure-localhost',
'--use-fake-ui-for-media-stream',
@@ -35,8 +33,7 @@ const chromeArgs = [
// Avoids - "You are checking for animations on an inactive tab, animations do not run for inactive tabs"
// when executing waitForStable()
'--disable-renderer-backgrounding',
`--use-file-for-fake-audio-capture=${
usingGrid ? process.env.REMOTE_RESOURCE_PATH : 'tests/resources'}/fakeAudioStream.wav`
'--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
];
if (process.env.RESOLVER_RULES) {
@@ -47,7 +44,7 @@ if (process.env.ALLOW_INSECURE_CERTS === 'true') {
}
if (process.env.HEADLESS === 'true') {
chromeArgs.push('--headless');
chromeArgs.push('--window-size=1280,720');
chromeArgs.push('--window-size=1280,1024');
}
if (process.env.VIDEO_CAPTURE_FILE) {
chromeArgs.push(`--use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
@@ -66,7 +63,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
specs: [
'specs/**'
],
maxInstances: 1, // if changing check onWorkerStart logic
maxInstances: parseInt(process.env.MAX_INSTANCES || '1', 10), // if changing check onWorkerStart logic
baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture/',
tsConfigPath: './tsconfig.json',
@@ -84,7 +81,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
framework: 'mocha',
mochaOpts: {
timeout: 60_000
timeout: 180_000
},
capabilities: {
@@ -169,14 +166,36 @@ export const config: WebdriverIO.MultiremoteConfig = {
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* We have overriden this function in beforeSession to be able to pass cid as first param.
*
* @returns {Promise<void>}
*/
async before() {
async before(cid, _, specs) {
if (specs.length !== 1) {
console.warn('We expect to run a single suite, but got more than one');
}
const testName = path.basename(specs[0]).replace('.spec.ts', '');
console.log(`Running test: ${testName} via worker: ${cid}`);
const globalAny: any = global;
globalAny.ctx = {
times: {}
} as IContext;
globalAny.ctx.keepAlive = [];
await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
const bInstance = multiremotebrowser.getInstance(instance);
initLogger(bInstance, instance, TEST_RESULTS_DIR);
// @ts-ignore
initLogger(bInstance, `${instance}-${cid}-${testName}`, TEST_RESULTS_DIR);
// setup keepalive
globalAny.ctx.keepAlive.push(setInterval(async () => {
await bInstance.execute(() => console.log(`${new Date().toISOString()} keep-alive`));
}, 20_000));
if (bInstance.isFirefox) {
return;
@@ -188,15 +207,14 @@ export const config: WebdriverIO.MultiremoteConfig = {
bInstance.iframePageBase = `file://${path.dirname(rpath)}`;
}));
const globalAny: any = global;
const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
globalAny.ctx.roomName = `jitsimeettorture-${crypto.randomUUID()}`;
if (process.env.ROOM_NAME_SUFFIX) {
globalAny.ctx.roomName += `_${process.env.ROOM_NAME_SUFFIX.trim()}`;
}
globalAny.ctx = {
times: {}
} as IContext;
globalAny.ctx.roomName = roomName;
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
globalAny.ctx.jwtKid = process.env.JWT_KID;
globalAny.ctx.isJaasAvailable = () => globalAny.ctx.jwtKid?.startsWith('vpaas-magic-cookie-');
},
after() {
@@ -205,6 +223,26 @@ export const config: WebdriverIO.MultiremoteConfig = {
if (ctx?.webhooksProxy) {
ctx.webhooksProxy.disconnect();
}
ctx.keepAlive?.forEach(clearInterval);
},
beforeSession(c, capabilities, specs, cid) {
const originalBefore = c.before;
if (!originalBefore || !Array.isArray(originalBefore) || originalBefore.length !== 1) {
console.warn('No before hook found or more than one found, skipping');
return;
}
if (originalBefore) {
c.before = [ async function(...args) {
// Call original with cid as first param, followed by original args
// @ts-ignore
return await originalBefore[0].call(c, cid, ...args);
} ];
}
},
/**
@@ -298,6 +336,14 @@ export const config: WebdriverIO.MultiremoteConfig = {
'image/png');
}));
// @ts-ignore
allProcessing.push(bInstance.execute(() => typeof APP !== 'undefined' && APP.connection?.getLogs())
.then(logs =>
logs && AllureReporter.addAttachment(
`debug-logs-${instance}`,
JSON.stringify(logs, null, ' '),
'text/plain'))
.catch(e => console.error('Failed grabbing debug logs', e)));
AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
@@ -306,7 +352,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
}));
});
await Promise.all(allProcessing);
await Promise.allSettled(allProcessing);
}
},

View File

@@ -20,11 +20,18 @@ if (process.env.HEADLESS === 'true') {
}
const ffExcludes = [
'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
'specs/2way/iFrameApiParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
// FF does not support setting a file as mic input, no dominant speaker events
'specs/3way/activeSpeaker.spec.ts',
'specs/4way/desktopSharing.spec.ts'
'specs/3way/startMuted.spec.ts', // bad audio levels
'specs/4way/desktopSharing.spec.ts',
'specs/4way/lastN.spec.ts',
// when unmuting a participant, we see the presence in debug logs imidiately,
// but for 15 seconds it is not received/processed by the client
// (also menu disappears after clicking one of the moderation option, does not happen manually)
'specs/3way/audioVideoModeration.spec.ts'
];
const mergedConfig = merge(defaultConfig, {

View File

@@ -1,18 +1,40 @@
// wdio.grid.conf.ts
// extends the main configuration file to add the selenium grid address
import { merge } from 'lodash-es';
import { URL } from 'url';
// @ts-ignore
import { config as defaultConfig } from './wdio.conf.ts?grid=true';
import { config as defaultConfig } from './wdio.conf.ts';
const gridUrl = new URL(process.env.GRID_HOST_URL as string);
const protocol = gridUrl.protocol.replace(':', '');
export const config = merge(defaultConfig, {
const mergedConfig = {
...defaultConfig,
protocol,
hostname: gridUrl.hostname,
port: gridUrl.port ? parseInt(gridUrl.port, 10) // Convert port to number
: protocol === 'http' ? 80 : 443,
path: gridUrl.pathname
}, { clone: false });
};
mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.participant2.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant2.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.participant3.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant3.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.participant4.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant4.capabilities['goog:chromeOptions'].args);
export const config = mergedConfig;
/**
* Updates the array of arguments for the Chrome browser to use a remote resource for fake audio capture.
* @param arr
*/
function updateRemoteResource(arr: string[]): string[] {
// eslint-disable-next-line no-confusing-arrow
return arr.map((item: string) => item.startsWith('--use-file-for-fake-audio-capture=')
? `--use-file-for-fake-audio-capture=${process.env.REMOTE_RESOURCE_PATH}/fakeAudioStream.wav` : item
);
}

View File

@@ -0,0 +1,18 @@
// wdio.grid.conf.ts
// extends the main configuration file to add the selenium grid address
import { URL } from 'url';
// @ts-ignore
import { config as defaultConfig } from './wdio.firefox.conf.ts';
const gridUrl = new URL(process.env.GRID_HOST_URL as string);
const protocol = gridUrl.protocol.replace(':', '');
export const config = {
...defaultConfig,
protocol,
hostname: gridUrl.hostname,
port: gridUrl.port ? parseInt(gridUrl.port, 10) // Convert port to number
: protocol === 'http' ? 80 : 443,
path: gridUrl.pathname
};