Compare commits

...

55 Commits

Author SHA1 Message Date
Дамян Минков
e227dcc792 fix(tests): Reenable a FF disabled case for avatar tests. 2025-02-24 16:19:37 -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
damencho
e5fa25892e fix(logging): Keeps the log storage ready when there is conference error.
LogCollector stops saving logs the moment we leave the room, although we take care to stop statistics from ljm and throw events so we can flush the logs.
Flush on conference failed.
2025-02-21 12:35:50 -06:00
Hristo Terezov
ae5fe24556 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1914.0.0+c040dee9...v1915.0.0+6e9b9c01
2025-02-21 08:55:22 -06:00
Rahul Vishwakarma
b9ef0aa27a lang: Update hindi translation 2025-02-20 16:03:51 -06:00
Christoph Settgast
f30625acf0 lang: update German translation (#15650) 2025-02-20 21:49:52 +01:00
damencho
66d70305a0 fix(docs): Updates the extra large conf docs. 2025-02-20 13:37:46 -06:00
damencho
9108b7ebec fix(tests): Adopts tests to the AV moderation UI changes. 2025-02-19 21:39:43 -06:00
damencho
9454049220 fix(av-moderation): When we are allowed to unmute make the notification sticky.
If the notification disappears, we don't have any other indication about this.
We were not showing any notification if only video is allowed.
Adds option to unmute audio or video, depend on what was allowed.
2025-02-19 21:39:43 -06:00
damencho
2ce2e01803 fix(participants): Offer audio,video choice to allow a participant.
We were showing only one option in the notification that was allowing both at the same time.
We add not 3 option, allow audio, allow video or both.
2025-02-19 21:39:43 -06:00
damencho
ab25d6c5ab fix(participants-pan): Move the audio allow to be default.
When both audio and video is to be allowed, make the audio the first one to show nad video to stay in the 3-dots menu.
2025-02-19 21:39:43 -06:00
damencho
1b0dc0cfb0 fix(video-menu): When muting all skip local.
When muting multiple participants always skip the local one for audio and for video.
2025-02-19 21:39:43 -06:00
damencho
33e484a847 fix(fmuc): Updates auto-promote case checks. 2025-02-19 18:18:52 -06:00
Jaya Allamsetty
67bebc0491 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1912.0.0+522577a4...v1914.0.0+c040dee9
2025-02-19 17:09:28 -05:00
Mihaela Dumitru
9186a74ae3 fix(recordings) increase duration for recording prompt notification (#15632) 2025-02-19 19:44:24 +02:00
sargamgayatri0803@gmail.com
67d9a9819e fix:Auto-Scroll Issue in Poll Screen After Adding an Option 2025-02-19 13:17:54 +02:00
damencho
16b88a29db fix(avmoderation): Fix actor jid. 2025-02-19 04:37:38 -06:00
Дамян Минков
9783793514 fix(iframeAPI): Fix setSubtitles command language param.
setRequestingSubtitles requires the last parameter in certain format.
2025-02-19 08:04:43 +01:00
Axel Prola
93de398a09 feat : Add config to disable camera tint foreground (#15619)
Co-authored-by: Axel Prola <axel.prola@equasens.com>
2025-02-18 13:16:47 -06:00
Kevin Vikström
23e97a4284 lang: added language norwegian bokmal (#15594)
* added language norwegian bokmal

* added norwegian bokmål to languages.json
2025-02-18 07:13:07 -06:00
sargamgayatri0803@gmail.com
9bb906551e fix:(profile): ensure apply button remains visible when keyboard appears 2025-02-18 14:35:09 +02:00
Saúl Ibarra Corretgé
b1ad82cef9 fix(build) add .bundle to ignore files 2025-02-17 17:24:54 +01:00
Saúl Ibarra Corretgé
09c9f2930c fix(ios,build) add missing dependencies for fastlane 2025-02-17 17:24:54 +01:00
Saúl Ibarra Corretgé
74efbd7a61 feat(ios) introduce gemfile to make builds more reproducible
With it we can control what Ruby version, cocoapods version and fastlane
version is being used.
2025-02-17 16:17:34 +01:00
Saúl Ibarra Corretgé
1b1e7d9bce fix(ios,ci) use Xcode 16.2 for making iOS builds 2025-02-17 16:17:34 +01:00
damencho
dc98fc4839 feat(tests): Adds video layout test. 2025-02-14 12:00:49 -06:00
damencho
a815f97c7e feat(tests): Adds udp test. 2025-02-14 12:00:49 -06:00
damencho
8261cf2811 feat(tests): Adds tile view test. 2025-02-14 12:00:49 -06:00
damencho
f2238935b5 feat(tests): Adds switch video test. 2025-02-14 12:00:49 -06:00
damencho
5f12f76ada feat(tests): Adds subject test. 2025-02-14 12:00:49 -06:00
damencho
5a9464697f feat(tests): Adds stop video test. 2025-02-14 12:00:49 -06:00
damencho
f44601a82b feat(tests): Adds singlePort test. 2025-02-14 12:00:49 -06:00
damencho
3d3de4a884 feat(tests): Adds preJoin test. 2025-02-14 12:00:49 -06:00
damencho
c02ad56b6d feat(tests): Adds oneOnOne test. 2025-02-14 12:00:49 -06:00
damencho
ea7c5ccd58 fix(tests): Uses utility methods for mute/unmute. 2025-02-14 12:00:49 -06:00
Hristo Terezov
7ec3eae72b feat(test): Implement hangupAllParticipants 2025-02-14 11:07:00 -06:00
Hristo Terezov
edf7d18308 feat(tests): Print error on execute failure. 2025-02-14 11:07:00 -06:00
Hristo Terezov
6bf4a4e91d fix(tests): ensureTwoParticipants.
Now we are waiting for the second participant to join before starting waitForRemoteStreams.
2025-02-14 11:07:00 -06:00
71 changed files with 3344 additions and 538 deletions

View File

@@ -88,17 +88,17 @@ jobs:
run: |
uname -a
xcode-select -p
sudo xcode-select -s /Applications/Xcode_16.0.app/Contents/Developer
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
xcodebuild -version
- name: setup-cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
uses: ruby/setup-ruby@v1
with:
podfile-path: ios/Podfile.lock
ruby-version: '3.4'
bundler-cache: true
- run: npx react-native info
- name: Install Pods
run: |
pod --version
cd ios
pod install --repo-update --deployment
working-directory: ./ios
run: bundle exec pod install --repo-update --deployment
- run: npx react-native bundle --entry-file react/index.native.js --platform ios --bundle-output /tmp/ios.bundle --reset-cache
android-sdk-build:
name: Build mobile SDK (Android)
@@ -137,17 +137,17 @@ jobs:
run: |
uname -a
xcode-select -p
sudo xcode-select -s /Applications/Xcode_16.0.app/Contents/Developer
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
xcodebuild -version
- name: setup-cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
uses: ruby/setup-ruby@v1
with:
podfile-path: ios/Podfile.lock
ruby-version: '3.4'
bundler-cache: true
- run: npx react-native info
- name: Install Pods
run: |
pod --version
cd ios
pod install --repo-update --deployment
working-directory: ./ios
run: bundle exec pod install --repo-update --deployment
- run: |
xcodebuild clean \
-workspace ios/jitsi-meet.xcworkspace \

1
.gitignore vendored
View File

@@ -62,6 +62,7 @@ buck-out/
# fastlane
#
.bundle/
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/test_output

16
Gemfile Normal file
View File

@@ -0,0 +1,16 @@
source "https://rubygems.org"
ruby ">= 3.4.0"
gem "cocoapods", "~> 1.16"
# (Optional) Fastlane for automation
gem "fastlane"
gem "abbrev"
gem "logger"
gem "mutex_m"
gem "csv"
gem "bigdecimal"
# (Optional) Bundler itself to ensure consistency
gem "bundler"

331
Gemfile.lock Normal file
View File

@@ -0,0 +1,331 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
abbrev (0.1.2)
activesupport (7.2.2.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.1)
aws-partitions (1.1050.0)
aws-sdk-core (3.218.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.98.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.181.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
claide (1.1.0)
cocoapods (1.16.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.16.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.27.0, < 2.0)
cocoapods-core (1.16.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
csv (3.3.2)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
drb (2.2.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.226.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.1)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86-linux-gnu)
ffi (1.17.1-x86-linux-musl)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.10.1)
jwt (2.10.1)
base64
logger (1.6.6)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.4)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (4.0.7)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.1)
rouge (3.28.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
securerandom (0.4.1)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
ruby
x86-linux-gnu
x86-linux-musl
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
abbrev
bigdecimal
bundler
cocoapods (~> 1.16)
csv
fastlane
logger
mutex_m
RUBY VERSION
ruby 3.4.2p28
BUNDLED WITH
2.6.3

View File

@@ -600,6 +600,7 @@ var config = {
// short: 2500,
// medium: 5000,
// long: 10000,
// extraLong: 60000,
// },
// // Options for the recording limit notification.
@@ -1857,6 +1858,9 @@ var config = {
// Hide login button on auth dialog, you may want to enable this if you are using JWT tokens to authenticate users
// hideLoginButton: true,
// If true remove the tint foreground on focused user camera in filmstrip
// disableCameraTintForeground: false,
};
// Set the default values for JaaS customers

View File

@@ -2096,7 +2096,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Amplitude: 184def4f87aa26f94a93a7faa334e06b1cae704d
amplitude-react-native: 9e5e0f4609366a2f714ab05dd7924f3c3073c075
amplitude-react-native: 6b7a1d30627233fe6f03741109831561d0a5f69c
AnalyticsConnector: a53214d38ae22734c6266106c0492b37832633a9
AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
@@ -2112,7 +2112,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
giphy-react-native-sdk: 9bda0d166ebfb8e253c1733412a4dae0cd58b468
giphy-react-native-sdk: b39b5fb4efdbb0b8afe6d2c98dcf3cfaff69481d
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
@@ -2127,85 +2127,85 @@ SPEC CHECKSUMS:
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
RCT-Folly: 34124ae2e667a0e5f0ea378db071d27548124321
RCTDeprecation: 3abf1129f54dbf292986302277f62ad01f7af1b2
RCTRequired: 67f606e1be40dbb827898c23d9eaee3562b087d1
RCTTypeSafety: 30826859480f0ee4a3dd4fe64854e40d6f29c558
React: 5128f4953efe912deea7fff94571618d3bd893f3
React-callinvoker: d59de4fb01e0dcd5e8141cc7de07619153f4a3f9
React-Core: 50bb3220a76f6f0e8cd5a64cff55eb31d651c6b9
React-CoreModules: ad66e10a371836745b841bc4ac6bebf513d525c8
React-cxxreact: e712310279bf9ef2b521ffe087c0ca0b3717f6fe
React-Core: 430a266f4ab728cb626df2f25a63c8b5b473eb6d
React-CoreModules: fb640900fdaaedd843dd5af0926c70ca991abf2b
React-cxxreact: 590dea656b5fdfa459b87c72b0d978c8122f0eb2
React-debug: 3770d713498696e4f438ee992eda8643d0515242
React-defaultsnativemodule: 43378038326254c38ef5c714190399cf9ed3243d
React-domnativemodule: 949c447d9414d2de4f9f69f22b75dc814655343d
React-Fabric: 297f3dfa0e20adf60ffec3d96c4dd139f73351fa
React-FabricComponents: 4a2f0f6d07834640d61b51c74c6b0950ebecc525
React-FabricImage: 5cfb8767287cd16af84e5b1edfc1df604ef9c2e8
React-defaultsnativemodule: 81178ca62bdae1b45ac569d58446d724bc3a17ea
React-domnativemodule: cdf33163c8f01705fbc22913d1bb373261fd8947
React-Fabric: 7ecf119c0ad168303cb9f51b1ef204bfb4083c95
React-FabricComponents: f72127c84bea5cdafa88c7d4c2028301894ef44e
React-FabricImage: a85f5e7978b495bf44be01c68ee5dac87d76ac70
React-featureflags: 79165585b574fd24cc9b6d6a46b1222d6b74744d
React-featureflagsnativemodule: bedb88cfae3a79ff1d6232bbccd0615d11572990
React-graphics: 9a97850e83b5ef375c6af4c3e6394c77ebe5b6fc
React-hermes: e20f5ebc7b9b956dc2ca9ecd8f104f60ca5c2bc5
React-idlecallbacksnativemodule: 394f1f4c593ea21c5a1670eef32daf12f5f58152
React-ImageManager: 476db4e54d94af681e29245757f7821aaeaaa9e2
React-jserrorhandler: 1fa6cdf46dcc9c434d731ab157f24c90716b2e2e
React-jsi: 40859d9be28ce04548639d6e9c4ccdca502423b8
React-jsiexecutor: 7d356ccaa05ebfe78924deb48d4f31e98e719868
React-jsinspector: 5b6b4d9dee379201e9a7ff3ede1e16919ff08dd1
React-jsitracing: 7e0beaf3265dbef266f8fccf1d53f74fff18f20d
React-logger: 614787b0dc10e8ddbdfb623c65eb9380befc3850
React-Mapbuffer: 5785862c3ba3c1222162b6a14b05478a6908e4ac
React-microtasksnativemodule: 2fa1c780087238662ed009d44fe71d19c085fa8b
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-keep-awake: afad8a51dfef9fe9655a6344771be32c8596d774
react-native-netinfo: 3aa5637c18834966e0c932de8ae1ae56fea20a97
react-native-orientation-locker: 4409c5b12b65f942e75449872b4f078b6f27af81
react-native-pager-view: c476f76d54f946df5147645e902d3d7173688187
react-native-performance: 47ac22ebf2aa24f324a96a5825581f6ce18c09e8
react-native-safe-area-context: 142fade490cbebbe428640b8cbdb09daf17e8191
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: 472b7c366eaaaa0207e546d9a50410df89790bcf
react-native-webrtc: 48295e7228279470c4f5acb38570e170723bd3b2
react-native-webview: 60a96123ba2995dd4e98b0103a6f906a62a88f38
React-featureflagsnativemodule: 1d5afb5057428cfcfc9eb4eef9e3896a97305d0f
React-graphics: c058a39d404e8ec52eea8d1fcfb005207bb373c3
React-hermes: f55230b73b1ee405e03fc9ef1386cdf5941c44d4
React-idlecallbacksnativemodule: a77baafcc0dfe6f7c9a1f3d6e800a5dafd849a7d
React-ImageManager: 9a7e0845a7cf3ae47a2ecbc4dce1c3a931cb6da5
React-jserrorhandler: 8eec50b49fd25f3336ac5ad35518da10b3685e96
React-jsi: b35179e1f82bb77a9e6c6e5dc62253a5a3fbcab5
React-jsiexecutor: 1b88ca1d0bc3ce9c69a69c833119a8b19b9a03fd
React-jsinspector: 3ca166cde69b0e3b6fc79d354716dd69ff9aab90
React-jsitracing: ec1a0220b9a337f9f05e117c9de26e010a357802
React-logger: 15a50e9e2fabe5d684cdd818ff1cbd2285bab1f0
React-Mapbuffer: 5dffd4714a7261a2fd1d94985eec118d0728b787
React-microtasksnativemodule: 9856dcf1c3d981843197b1b75253a079d961795d
react-native-background-timer: 4638ae3bee00320753647900b21260b10587b6f7
react-native-get-random-values: 419569b6ed3d15bfb9b6781b2f2e058f8e8d2698
react-native-keep-awake: 21ff40767cde4bd81021ffee12480aee4b5b91bb
react-native-netinfo: 5364263f903da576bdef9c84a76fe243ab06812c
react-native-orientation-locker: ee8bb2177365ca74f51dc1e11218fe544634d523
react-native-pager-view: 8bd7d72d1c260ef565952ac617ab6e492c457894
react-native-performance: a857604bcea75256e0a8a5028153cf1325009ab8
react-native-safe-area-context: 8b8404e70b0cbf2a56428a17017c14c1dcc16448
react-native-slider: e472a35a07451af85d2aad887d550dd3213c6190
react-native-splash-screen: 95994222cc95c236bd3cdc59fe45ed5f27969594
react-native-video: 2259c0828a2c78ce2e32e761ab1a38f765aa0dda
react-native-webrtc: 2261a482150195092246fe70b3aff976f2e11ec5
react-native-webview: 63733b0dd3d4e1ac1e4541038c671863d518032b
React-nativeconfig: 237c862aab56a7426301fcbbb8dd6988745231e0
React-NativeModulesApple: 205d3251b2e7fc625b7e6232153d4393bc2d30fe
React-NativeModulesApple: 5b3c2bcfa3a53b22da441b35189e902ede27513d
React-perflogger: 46ce3b295add69087b7c5ff325b55a6c7af204fc
React-performancetimeline: 25b097ecf52b95ee73d7958f36ff2e3797fdb636
React-performancetimeline: 73640f6e2d96f10fbadc9a1b3708c08d8dc0831f
React-RCTActionSheet: 2f91a7dec094618e77b57b4f08093aeca18fd229
React-RCTAnimation: 7be5ec16fffc993f83bbee2a5ce33d95842259b9
React-RCTAppDelegate: 317a42e3661fa73d7706ad4a144b32a8a3bfeb11
React-RCTBlob: fa9d79e8df2c82994d740eab8fd8691c9b26f466
React-RCTFabric: 18417ee9428451497c1a7460049a1d776485d2a0
React-RCTImage: a72775078cab71099f0fcb75640dd53799fb5ff5
React-RCTLinking: c28ad5fe32bc13f3022d5b69cb9c464ddfead8d9
React-RCTNetwork: 630d4475ea350381cb977d9bc815fb1758408d89
React-RCTSettings: 94d1d3e6a8bf9a3935e24ed5bfbfab74b127f929
React-RCTText: 2d4d09e1e94b182e3e1ab48ae058618aa890d049
React-RCTVibration: 3449d694446a1f7e96afaee4cec774c1b9c71be6
React-RCTAnimation: bb3585cc2cf6983b168837a1ffef646e7f9934fb
React-RCTAppDelegate: dc7975e6b711d4a9fb61c76b2ac742188c0df6a9
React-RCTBlob: 694c9aa3ea49f14c743c46877052ea9077c6e586
React-RCTFabric: 7c9e49267733fe32217aacadb95e4fbbaefbdfb2
React-RCTImage: b7ad963f79421bcb3f5870532ef56ae0088e5ed7
React-RCTLinking: 4db9af8242140717e2409db8c1e1c3e5f8ee7fc3
React-RCTNetwork: f29615722f45424a4e4b3a92e9a73f65150d484b
React-RCTSettings: 1d43b8d0ec9bf9e15e6e175a8ff11b28e6c37fa6
React-RCTText: f40e2bff92400c34d88b70d939562d3f2b1f5e7e
React-RCTVibration: 520d03c013f214df1e3b7190228cf7582912b409
React-rendererconsistency: c4727a998bcd1014f4591a36a5a583f4d4efe8de
React-rendererdebug: 838362649a215df83b240af55ea3332792d45abe
React-rendererdebug: 76981de936b2d0f3527aeb60e92d7272997b5d0a
React-rncore: 80318876e342710c2d940189554925205fdbb818
React-RuntimeApple: fa99b61fa839d0e3923e8f27f4b8079f460d3335
React-RuntimeCore: dbbfb7dd76a18d289bade95edfc6a795eef2ba55
React-RuntimeApple: 7aa8f900c02111575f87e401920fd039b0900206
React-RuntimeCore: 206ef584eb31fc4c0c976ae0444718666ecc8fb6
React-runtimeexecutor: 71511b04f7c2ad44a9e94e2c1a73b271f4abb9e9
React-RuntimeHermes: 777c37c304fd3fcfd63e6c970a7a9700c89811e9
React-runtimescheduler: 248cb0c1304b6b4dd46d2a9d1f3ebcf76ec8eea3
React-utils: 40f04d50cc917b80d5f829e48635a273a7e5a0c1
ReactCodegen: c672218d62eee3026615eeec882d862395e61cd8
ReactCommon: c5f4dfc5a41d58a8013df370cf733de9782c7659
RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3
RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c
RNCClipboard: 2821ac938ef46f736a8de0c8814845dde2dcbdfb
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 02ea8b23e2280fa18e00a06d7e62804d74028579
RNGestureHandler: 939f21fabf5d45a725c0bf175eb819dd25cf2e70
RNGoogleSignin: a6a612cce56a45ab701c5c5c6e36f5390522d100
RNScreens: c7ceced6a8384cb9be5e7a5e88e9e714401fd958
RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
RNSVG: ed492aaf3af9ca01bc945f7a149d76d62e73ec82
RNWatch: fd30ca40a5b5ef58dcbc195638e68219bc455236
React-RuntimeHermes: 32932c16965fbd74b9acf17f213c2b4f6f2ac617
React-runtimescheduler: 3bcaa15e0cf35e22727ba5b379445c5946a9dd09
React-utils: 43588634a9eca9915486154b143a0da615cfd893
ReactCodegen: 43d3ebb0cb9c6ffc92a254d31749fd29d69844a2
ReactCommon: ee80ae3d276a9f1daa059169405b97c600dcba45
RNCalendarEvents: f90f73666b6bcbb3cc8a491ffbb5e48c0db3de37
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
RNCClipboard: 7c3e3b5f71d84ef61690ad377b6c50cf27864ff5
RNDefaultPreference: ee13d69e6693d193cd223d10e15e5b3c012d31ba
RNDeviceInfo: 8af23685571b7867d8dc15fb89e7fb5fa8607e1e
RNGestureHandler: 011b703e87c0008b9e0375dca87f37f34a5133e8
RNGoogleSignin: 30e1aee80140dc0706cd78a4951c411376c88329
RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9
RNSound: 314cc5226453ef4a3314a196c65e8a65e5106a7b
RNSVG: 4611b66c2167ba03429e7428a3a490be6e439391
RNWatch: 28fe1f5e0c6410d45fd20925f4796fce05522e3f
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: 1dd9dabb9df8fe08f12cd522eae04a2da0e252eb

View File

@@ -37,6 +37,7 @@
"ml": "മലയാളം",
"mn": "Монгол",
"mr": "मराठी",
"nb": "Norsk bokmål",
"nl": "Nederlands",
"oc": "Occitan",
"pl": "Polski",

View File

@@ -16,7 +16,7 @@
"failedToAdd": "Fehler beim Hinzufügen von Personen",
"googleEmail": "Google-E-Mail",
"inviteMoreHeader": "Sie sind alleine in der Sitzung",
"inviteMoreMailSubject": "An {{appName}} Meeting teilnehmen",
"inviteMoreMailSubject": "An {{appName}} Konferenz teilnehmen",
"inviteMorePrompt": "Mehr Leute einladen",
"linkCopied": "Link in die Zwischenablage kopiert",
"noResults": "Keine passenden Ergebnisse",
@@ -89,10 +89,10 @@
"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 Meeting teilnehmen",
"joinTooltip": "Am Konferenz teilnehmen",
"nextMeeting": "Nächste Konferenz",
"noEvents": "Es gibt keine bevorstehenden Termine.",
"ongoingMeeting": "Laufendes Meeting",
"ongoingMeeting": "Laufende Konferenz",
"permissionButton": "Einstellungen öffnen",
"permissionMessage": "Die App benötigt Zugriff auf den Kalender, um Termine und Konferenzen anzuzeigen.",
"refresh": "Kalender aktualisieren",
@@ -145,7 +145,7 @@
"installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365"
},
"connectingOverlay": {
"joiningRoom": "Eine Verbindung zu Ihrem Meeting wird hergestellt…"
"joiningRoom": "Eine Verbindung zu Ihrer Konferenz wird hergestellt…"
},
"connection": {
"ATTACHED": "Angehängt",
@@ -215,7 +215,7 @@
"downloadMobileApp": "Aus dem App Store herunterladen",
"ifDoNotHaveApp": "Wenn Sie die App noch nicht haben:",
"ifHaveApp": "Wenn Sie die App bereits haben:",
"joinInApp": "Mit der App am Meeting teilnehmen",
"joinInApp": "Mit der App an der Konferenz teilnehmen",
"joinInAppNew": "Mit der App",
"joinInBrowser": "Im Browser",
"launchMeetingLabel": "Wie möchten Sie an der Konferenz teilnehmen?",
@@ -258,7 +258,7 @@
"dialog": {
"Back": "Zurück",
"Cancel": "Abbrechen",
"IamHost": "Ich leite das Meeting",
"IamHost": "Ich leite die Konferenz",
"Ok": "OK",
"Remove": "Entfernen",
"Share": "Teilen",
@@ -317,7 +317,7 @@
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
"e2eeWillDisableDueToMaxModeDescription": "WARNUNG: Ende-zu-Ende-Verschlüsselung wird automatisch deaktiviert, wenn weitere Anwesende an der Konferenz teilnehmen.",
"embedMeeting": "Besprechung einbetten",
"embedMeeting": "Konferenz einbetten",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"error": "Fehler",
"errorRoomCreationRestriction": "Sie haben versucht, zu schnell beizutreten, bitte versuchen Sie es gleich noch einmal.",
@@ -334,7 +334,8 @@
"kickParticipantButton": "Entfernen",
"kickParticipantDialog": "Wollen Sie diese Person wirklich entfernen?",
"kickParticipantTitle": "Person entfernen?",
"kickTitle": "Autsch! {{participantDisplayName}} hat Sie aus dem Meeting geworfen",
"kickSystemTitle": "Autsch! Sie wurden aus der Konferenz geworfen",
"kickTitle": "Autsch! {{participantDisplayName}} hat Sie aus der Konferenz geworfen",
"linkMeeting": "Konferenz verlinken",
"linkMeetingTitle": "Konferenz mit Salesforce verlinken",
"liveStreaming": "Livestreaming",
@@ -381,7 +382,7 @@
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
"noDropboxToken": "Kein gültiges Dropbox-Token",
"password": "Passwort",
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
"passwordLabel": "Diese Konferenz wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um der Konferenz beizutreten.",
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) nicht unterstützt",
"passwordRequired": "$t(lockRoomPasswordUppercase) erforderlich",
@@ -555,13 +556,13 @@
"invitePhone": "Wenn Sie stattdessen per Telefon beitreten möchten, wählen sie: {{number}},,{{conferenceID}}#\n",
"invitePhoneAlternatives": "Suchen Sie nach einer anderen Einwahlnummer ?\nEinwahlnummern der Konferenz anzeigen: {{url}}\n\n\nWenn Sie sich auch über ein Raumtelefon einwählen, nehmen Sie teil, ohne sich mit dem Ton zu verbinden: {{silentUrl}}",
"inviteSipEndpoint": "Um mit SIP teilzunehmen, folgende Adresse nutzen: {{sipUri}}",
"inviteTextiOSInviteUrl": "Am Meeting teilnehmen: {{inviteUrl}}.",
"inviteTextiOSInviteUrl": "An Konferenz teilnehmen: {{inviteUrl}}.",
"inviteTextiOSJoinSilent": "Wenn Sie über ein Konferenztelefon teilnehmen, können Sie diesen Link nutzen um ohne Ton an der Konferenz teilzunehmen: {{silentUrl}}.",
"inviteTextiOSPersonal": "{{name}} lädt Sie zu einem Meeting ein.",
"inviteTextiOSPersonal": "{{name}} lädt Sie zu einer Konferenz ein.",
"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 einem Meeting eingeladen.",
"inviteURLFirstPartPersonal": "{{name}} lädt Sie zu einem Meeting ein.\n",
"inviteURLSecondPart": "\nAm Meeting teilnehmen:\n{{url}}\n",
"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",
"label": "Einwahlinformationen",
"liveStreamURL": "Livestream:",
"moreNumbers": "Weitere Telefonnummern",
@@ -575,7 +576,7 @@
"sip": "SIP-Adresse",
"sipAudioOnly": "SIP-Adresse (nur Ton)",
"title": "Teilen",
"tooltip": "Freigabe-Link und Einwahlinformationen für dieses Meeting",
"tooltip": "Freigabe-Link und Einwahlinformationen für diese Konferenz",
"upgradeOptions": "Bitte prüfen Sie Ihre Upgrade-Optionen auf",
"whiteboardError": "Whiteboard konnte nicht geladen werden. Bitte versuchen Sie es später erneut."
},
@@ -627,7 +628,7 @@
"errorAPI": "Beim Abrufen der YouTube-Livestreams ist ein Fehler aufgetreten. Bitte versuchen Sie, sich erneut anzumelden.",
"errorLiveStreamNotEnabled": "Livestreaming ist für {{email}} nicht aktiviert. Aktivieren Sie das Livestreaming oder melden Sie sich bei einem Konto mit aktiviertem Livestreaming an.",
"expandedOff": "Livestream wurde angehalten",
"expandedOn": "Das Meeting wird momentan an YouTube gestreamt.",
"expandedOn": "Die Konferenz wird momentan an YouTube gestreamt.",
"expandedPending": "Livestream wird gestartet …",
"failedToStart": "Livestream konnte nicht gestartet werden",
"getStreamKeyManually": "Wir waren nicht in der Lage, Livestreams abzurufen. Versuchen Sie, Ihren Livestream-Schlüssel von YouTube zu erhalten.",
@@ -731,14 +732,17 @@
"me": "ich",
"notify": {
"OldElectronAPPTitle": "Sicherheitslücke!",
"allowAction": "Erlauben",
"allowAudio": "Mikrofon einschalten",
"allowBoth": "Beides",
"allowVideo": "Kamera einschalten",
"allowedUnmute": "Sie können die Stummschaltung aufheben, Ihre Kamera einschalten oder Ihren Bildschirm teilen.",
"audioUnmuteBlockedDescription": "Díe Stummschaltung kann aus Überlastungsschutzgründen temporär nicht aufgehoben werden.",
"audioUnmuteBlockedTitle": "Stummschaltung kann nicht aufgehoben werden!",
"chatMessages": "Chatnachrichten",
"connectedOneMember": "{{name}} nimmt am Meeting teil",
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen am Meeting teil",
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
"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.",
"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.",
@@ -753,6 +757,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.",
"invitedOneMember": "{{name}} wurde eingeladen",
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
@@ -783,7 +790,7 @@
"moderationToggleDescription": "von {{participantDisplayName}}",
"moderator": "Moderationsrechte vergeben!",
"muted": "Der Konferenz wurde stumm beigetreten.",
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche von der Konferenz fernzuhalten.",
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
"mutedTitle": "Stummschaltung aktiv!",
"newDeviceAction": "Verwenden",
@@ -811,7 +818,7 @@
"screenSharingAudioOnlyTitle": "Modus \"Beste Leistung\"",
"selfViewTitle": "Sie können die eigene Ansicht immer in den Einstellungen reaktivieren",
"somebody": "Jemand",
"startSilentDescription": "Treten Sie dem Meeting noch einmal bei, um Ihr Audio zu aktivieren",
"startSilentDescription": "Treten Sie der Konferenz noch einmal bei, um Ihr Audio zu aktivieren",
"startSilentTitle": "Sie sind ohne Audioausgabe beigetreten!",
"suboptimalBrowserWarning": "Tut uns leid, aber die Konferenz wird mit {{appName}} kein großartiges Erlebnis. Wir versuchen immer die Situation zu verbessern, bis dahin empfehlen wir aber die Verwendung einer der <a href=\"{{recommendedBrowserPageLink}}\" target=\"_blank\">vollständig unterstützen Browser</a>.",
"suboptimalExperienceTitle": "Browserwarnung",
@@ -819,6 +826,7 @@
"suggestRecordingDescription": "Möchten Sie eine Aufzeichnung starten?",
"suggestRecordingTitle": "Konferenz aufzeichnen",
"unmute": "Stummschaltung aufheben",
"unmuteVideo": "Kamera einschalten",
"videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.",
"videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!",
"videoUnmuteBlockedDescription": "Die Kamera und Bildschirmfreigabe kann aus Überlastungsschutzgründen temporär nicht eingeschaltet werden.",
@@ -1022,7 +1030,7 @@
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"errorFetchingLink": "Der Link zur Aufzeichnung konnte nicht geladen werden.",
"expandedOff": "Aufzeichnung wurde gestoppt",
"expandedOn": "Das Meeting wird momentan aufgezeichnet.",
"expandedOn": "Die Konferenz wird momentan aufgezeichnet.",
"expandedPending": "Aufzeichnung wird gestartet…",
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
"fileSharingdescription": "Aufzeichnung mit den Personen der Konferenz teilen",
@@ -1030,7 +1038,7 @@
"highlightMoment": "Moment als Highlight festhalten",
"highlightMomentDisabled": "Sie können Momente als Highlights festhalten, sobald die Aufnahme startet",
"highlightMomentSuccess": "Highlight festgehalten",
"highlightMomentSucessDescription": "Ihr festgehaltener Moment wird zur Zusammenfassung des Meeting hinzugefügt.",
"highlightMomentSucessDescription": "Ihr festgehaltener Moment wird zur Zusammenfassung der Konferenz hinzugefügt.",
"inProgress": "Aufzeichnung gestartet",
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
@@ -1050,7 +1058,8 @@
"on": "Aufnahme",
"onBy": "{{name}} startete die Aufnahme",
"onlyRecordSelf": "Nur eigenes Kamerabild und Ton aufzeichnen",
"pending": "Aufzeichnung des Meetings wird vorbereitet…",
"pending": "Aufzeichnung der Konferenz wird vorbereitet…",
"policyError": "Sie haben die Aufzeichnung zu früh gestartet. Bitte versuchen Sie es später noch einmal.",
"recordAudioAndVideo": "Kamera und Ton aufzeichnen",
"recordTranscription": "Transkription aufzeichnen",
"saveLocalRecording": "Aufzeichnung lokal abspeichern",
@@ -1165,8 +1174,8 @@
"version": "Version"
},
"share": {
"dialInfoText": "\n\n=====\n\nWollen Sie sich nur auf Ihrem Telefon einwählen?\n\n{{defaultDialInNumber}}Klicken Sie auf diesen Link, um die eingewählten Telefonnummern für dieses Meeting zu sehen\n{{dialInfoPageUrl}}",
"mainText": "Klicken Sie auf den folgenden Link, um dem Meeting beizutreten:\n{{roomUrl}}"
"dialInfoText": "\n\n=====\n\nWollen Sie sich nur auf Ihrem Telefon einwählen?\n\n{{defaultDialInNumber}}Klicken Sie auf diesen Link, um die eingewählten Telefonnummern für diese Konferenz zu sehen\n{{dialInfoPageUrl}}",
"mainText": "Klicken Sie auf den folgenden Link, um der Konferenz beizutreten:\n{{roomUrl}}"
},
"speaker": "Sprecher/-in",
"speakerStats": {
@@ -1397,7 +1406,7 @@
"ccButtonTooltip": "Untertitel ein-/ausschalten",
"expandedLabel": "Transkribieren ist derzeit eingeschaltet",
"failed": "Transkribieren fehlgeschlagen",
"labelToolTip": "Das Meeting wird transkribiert",
"labelToolTip": "Die Konferenz wird transkribiert",
"sourceLanguageDesc": "Aktuell ist die Sprache der Konferenz auf <b>{{sourceLanguage}}</b> eingestellt. <br/> Sie könne dies hier ",
"sourceLanguageHere": "ändern",
"start": "Anzeige der Untertitel starten",
@@ -1528,15 +1537,15 @@
},
"calendar": "Kalender",
"connectCalendarButton": "Kalender verbinden",
"connectCalendarText": "Verbinden Sie Ihren Kalender, um all Ihre Meetings in {{app}} anzuzeigen. Fügen Sie zudem {{provider}}-Meetings in Ihren Kalender ein und starten Sie sie mit nur einem Klick.",
"enterRoomTitle": "Neues Meeting starten",
"connectCalendarText": "Verbinden Sie Ihren Kalender, um all Ihre Konferenzen in {{app}} anzuzeigen. Fügen Sie zudem {{provider}}-Konferenzen in Ihren Kalender ein und starten Sie sie mit nur einem Klick.",
"enterRoomTitle": "Neue Konferenz starten",
"getHelp": "Hilfe",
"go": "Los",
"goSmall": "Los",
"headerSubtitle": "Sichere und hochqualitative Meetings",
"headerSubtitle": "Sichere und hochqualitative Konferenzen",
"headerTitle": "Jitsi Meet",
"info": "Einwahlinformationen",
"jitsiOnMobile": "Jitsi unterwegs einfach unsere Apps herunterladen und Meetings von überall starten",
"jitsiOnMobile": "Jitsi unterwegs einfach unsere Apps herunterladen und Konferenzen von überall starten",
"join": "ERSTELLEN / BEITRETEN",
"logo": {
"calendar": "Kalender Logo",
@@ -1562,7 +1571,7 @@
"roomnameHint": "Name oder URL der Konferenz, der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Personen übermittelt werden, damit diese der gleichen Konferenz beitreten.",
"sendFeedback": "Feedback senden",
"settings": "Einstellungen",
"startMeeting": "Meeting starten",
"startMeeting": "Konferenz starten",
"terms": "AGB",
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen",
"upcomingMeetings": "Ihre zukünftigen Konferenzen"

View File

@@ -192,7 +192,7 @@
"alreadySharedVideoTitle": "एक समय में केवल एक साझा वीडियो की अनुमति है",
"applicationWindow": "एप्लिकेशन विंडो",
"authenticationRequired": "प्रमाणीकरण आवश्यक है",
"cameraConstraintFailedError": "Your camera does not satisfy some of the required constraints.",
"cameraConstraintFailedError": "आपका कैमरा आवश्यक बाधाओं में से कुछ को पूरा नहीं करता है।",
"cameraNotFoundError": "कैमरा नहीं मिला।",
"cameraNotSendingData": "हम आपके कैमरे का उपयोग करने में असमर्थ हैं। कृपया जांचें कि क्या कोई अन्य एप्लिकेशन इस डिवाइस का उपयोग तो नहीं कर रहा है, सेटिंग मेनू से किसी अन्य डिवाइस का चयन करें या एप्लिकेशन को फिर से लोड करने का प्रयास करें।",
"cameraNotSendingDataTitle": "कैमरा उपयोग करने में असमर्थ",
@@ -222,7 +222,7 @@
"e2eeWarning": "चेतावनी: इस मीटिंग में सभी प्रतिभागियों के पास एंड-टू-एंड एन्क्रिप्शन के लिए समक्षता नहीं है। यदि आप इसे सक्षम करते हैं तो वे आपको देखने और सुनने में सक्षम नहीं होंगे।",
"enterDisplayName": "कृपया यहाँ अपना नाम लिखें",
"error": "त्रुटि",
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
"gracefulShutdown": "हमारी सेवा वर्तमान में रखरखाव के लिए बंद है। कृपया बाद में पुनः प्रयास करें।",
"grantModeratorDialog": "क्या आप वाकई इस प्रतिभागी को एक मध्यस्थ बनाना चाहते हैं?",
"grantModeratorTitle": "मध्यस्थ स्वीकृती दे ",
"incorrectPassword": "गलत उपयोगकर्ता नाम या पासवर्ड",
@@ -230,7 +230,7 @@
"internalError": "उफ़! कुछ गड़बड़ हो गई। निम्नलिखित त्रुटि हुई: {{error}}",
"internalErrorTitle": "आंतरिक त्रुटि",
"kickMessage": "आप अधिक जानकारी के लिए {{participantDisplayName}} से संपर्क कर सकते हैं।",
"kickParticipantButton": "Kick",
"kickParticipantButton": "निकालें",
"kickParticipantDialog": "क्या आप वाकई इस प्रतिभागी को निकलना चाहते हैं?",
"kickParticipantTitle": "इस प्रतिभागी को निकाले?",
"kickTitle": "अरे! {{participantDisplayName}} ने आपको मीटिंग से बाहर कर दिया",
@@ -245,7 +245,7 @@
"logoutTitle": "लॉग आउट ",
"maxUsersLimitReached": "अधिकतम प्रतिभागियों की सीमा पूरी हो चुकी है. कृपया बैठक के मालिक से संपर्क करें या बाद में पुनः प्रयास करें!!",
"maxUsersLimitReachedTitle": "अधिकतम प्रतिभागियों सीमा पार हो गई",
"micConstraintFailedError": "Your microphone does not satisfy some of the required constraints.",
"micConstraintFailedError": "आपका माइक्रोफ़ोन आवश्यक प्रतिबंधों को पूरा नहीं करता।",
"micNotFoundError": "माइक्रोफोन नहीं मिला।",
"micNotSendingData": "अपने माइक को अनम्यूट करने और इसके स्तर को समायोजित करने के लिए अपने कंप्यूटर की सेटिंग पर जाएं",
"micNotSendingDataTitle": "आपका माइक आपकी सिस्टम सेटिंग्स द्वारा मौन है",
@@ -285,18 +285,18 @@
"remoteControlDeniedMessage": "{{user}} ने आपका रिमोट कंट्रोल अनुरोध अस्वीकार कर दिया!",
"remoteControlErrorMessage": "{{user}}से रिमोट कंट्रोल की अनुमति का अनुरोध करते समय एक त्रुटि हुई!",
"remoteControlRequestMessage": "क्या आप {{user}} को दूर से अपने डेस्कटॉप को नियंत्रित करने की अनुमति देंगे?",
"remoteControlShareScreenWarning": "Note that if you press \"Allow\" you will share your screen!",
"remoteControlShareScreenWarning": "ध्यान दें कि यदि आप \"अनुमति दें\" दबाते हैं, तो आप अपनी स्क्रीन साझा करेंगे!",
"remoteControlStopMessage": "रिमोट कंट्रोल सत्र समाप्त हो गया!",
"remoteControlTitle": "रिमोट डेस्कटॉप कंट्रोल",
"removePassword": "निकालें $t(lockRoomPassword)",
"removeSharedVideoMsg": "क्या आप वाकई अपने साझा किए गए वीडियो को निकालना चाहते हैं?",
"removeSharedVideoTitle": "साझा किया गया वीडियो निकालें",
"reservationError": "Reservation system error",
"reservationError": "आरक्षण प्रणाली में त्रुटि",
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
"retry": "पुनः प्रयास करें",
"screenSharingAudio": "Share audio",
"screenSharingAudio": "ऑडियो साझा करें",
"screenSharingFailed": "उफ़! कुछ गड़बड़ हो गई, हम स्क्रीन शेयरिंग शुरू करने में सक्षम नहीं थे!",
"screenSharingFailedTitle": "Screen sharing failed!",
"screenSharingFailedTitle": "स्क्रीन साझा करना विफल हुआ!",
"screenSharingPermissionDeniedError": "उफ़! आपकी स्क्रीन शेयरिंग अनुमतियों में कुछ गड़बड़ हो गई है। कृपया पुनः लोड करें और पुनः प्रयास करें।",
"sendPrivateMessage": "आपने हाल ही में एक निजी संदेश प्राप्त किया है। क्या आप उसका निजी रूप से जवाब देने का इरादा रखते हैं? या आप अपना संदेश समूह को भेजना चाहते हैं?",
"sendPrivateMessageCancel": "समूह को भेजें",
@@ -304,7 +304,7 @@
"sendPrivateMessageTitle": "निजी तौर पर भेजें?",
"serviceUnavailable": "सेवा अनुपलब्ध",
"sessTerminated": "कॉल समाप्त",
"sessionRestarted": "Call restarted because of a connection issue",
"sessionRestarted": "कनेक्शन समस्या के कारण कॉल पुनः प्रारंभ की गई",
"shareVideoLinkError": "कृपया एक सही यूट्यूब लिंक प्रदान करें।.",
"shareVideoTitle": "एक वीडियो साझा करें",
"shareYourScreen": "अपनी स्क्रीन साझा करें",
@@ -313,10 +313,10 @@
"startRecording": "रिकॉर्डिंग प्रारंभ करें",
"startRemoteControlErrorMessage": "रिमोट कंट्रोल सत्र शुरू करने की कोशिश करते समय एक त्रुटि हुई!",
"stopLiveStreaming": "लाइव स्ट्रीम बंद करें",
"stopRecording": "Stop recording",
"stopRecording": "रिकॉर्डिंग बंद करें",
"stopRecordingWarning": "क्या आप वाकई रिकॉर्डिंग को रोकना चाहते हैं?",
"stopStreamingWarning": "क्या आप वाकई लाइव स्ट्रीमिंग को रोकना चाहते हैं?",
"streamKey": "Live stream key",
"streamKey": "लाइव स्ट्रीम कुंजी",
"thankYou": " {{appName}} का उपयोग करने के लिए धन्यवाद!",
"token": "टोकन",
"tokenAuthFailed": "क्षमा करें, आपको इस कॉल में शामिल होने की अनुमति नहीं है।",
@@ -336,7 +336,7 @@
"labelToolTip": "इस कॉल पर ऑडियो और वीडियो संचार एंड-टू-एंड एन्क्रिप्टेड है"
},
"embedMeeting": {
"title": "Embed this meeting"
"title": "इस बैठक को एम्बेड करें"
},
"feedback": {
"average": "औसत",
@@ -381,7 +381,7 @@
"moreNumbers": "अधिक संख्या",
"noNumbers": "कोई डायल-इन नंबर नहीं।",
"noPassword": "कोई नहीं",
"noRoom": "No room was specified to dial-in into.",
"noRoom": "डायल-इन करने के लिए कोई कक्ष निर्दिष्ट नहीं किया गया।",
"numbers": "डायल-इन नंबर",
"password": "$t(lockRoomPasswordUppercase):",
"title": "साझा करें",
@@ -404,11 +404,11 @@
"keyboardShortcuts": {
"focusLocal": "अपने वीडियो पर केंद्रित करें",
"focusRemote": "किसी अन्य व्यक्ति के वीडियो पर केंद्रित करें",
"fullScreen": "View or exit full screen",
"fullScreen": "पूर्ण स्क्रीन देखें या बाहर निकलें",
"keyboardShortcuts": "कीबोर्ड शॉर्टकट्स",
"localRecording": "स्थानीय रिकॉर्डिंग नियंत्रण दिखाएं या छिपाएँ",
"mute": "अपने माइक्रोफ़ोन को म्यूट या अनम्यूट करें",
"pushToTalk": "Push to talk",
"pushToTalk": "बोलने के लिए दबाएं",
"raiseHand": "अपना हाथ उठाएँ या नीचे करें",
"showSpeakerStats": "स्पीकर आंकड़े दिखाएं",
"toggleChat": "चैट खोलें या बंद करें",
@@ -418,39 +418,39 @@
"videoMute": "अपना कैमरा प्रारंभ या बंद करें"
},
"liveStreaming": {
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
"busyTitle": "All streamers are currently busy",
"changeSignIn": "Switch accounts.",
"choose": "Choose a live stream",
"chooseCTA": "Choose a streaming option. You're currently logged in as {{email}}.",
"enterStreamKey": "Enter your YouTube live stream key here.",
"error": "Live Streaming failed. Please try again.",
"errorAPI": "An error occurred while accessing your YouTube broadcasts. Please try logging in again.",
"errorLiveStreamNotEnabled": "Live Streaming is not enabled on {{email}}. Please enable live streaming or log into an account with live streaming enabled.",
"expandedOff": "The live streaming has stopped",
"expandedOn": "The meeting is currently being streamed to YouTube.",
"expandedPending": "The live streaming is being started…",
"failedToStart": "Live Streaming failed to start",
"getStreamKeyManually": "We werent able to fetch any live streams. Try getting your live stream key from YouTube.",
"googlePrivacyPolicy": "Google Privacy Policy",
"invalidStreamKey": "Live stream key may be incorrect.",
"limitNotificationDescriptionNative": "Your streaming will be limited to {{limit}} min. For unlimited streaming try {{app}}.",
"limitNotificationDescriptionWeb": "Due to high demand your streaming will be limited to {{limit}} min. For unlimited streaming try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"off": "Live Streaming stopped",
"offBy": "{{name}} stopped the live streaming",
"on": "Live Streaming started",
"onBy": "{{name}} started the live streaming",
"pending": "Starting Live Stream…",
"serviceName": "Live Streaming service",
"signIn": "Sign in with Google",
"signInCTA": "Sign in or enter your live stream key from YouTube.",
"signOut": "Sign out",
"signedInAs": "You are currently signed in as:",
"start": "Start a live stream",
"streamIdHelp": "What's this?",
"title": "सीधा प्रसारण",
"unavailableTitle": "Live Streaming unavailable",
"youtubeTerms": "YouTube terms of services"
"busy": "हम स्ट्रीमिंग संसाधनों को मुक्त करने पर काम कर रहे हैं। कृपया कुछ मिनटों में पुनः प्रयास करें।",
"busyTitle": "सभी स्ट्रीमर वर्तमान में व्यस्त हैं",
"changeSignIn": "खाता बदलें।",
"choose": "एक लाइव स्ट्रीम चुनें",
"chooseCTA": "स्ट्रीमिंग विकल्प चुनें। आप वर्तमान में {{email}} के रूप में लॉग इन हैं।",
"enterStreamKey": "अपनी YouTube लाइव स्ट्रीम कुंजी यहाँ दर्ज करें।",
"error": "लाइव स्ट्रीमिंग विफल रही। कृपया पुनः प्रयास करें।",
"errorAPI": "आपके YouTube प्रसारण तक पहुँचने में त्रुटि हुई। कृपया पुनः लॉगिन करें।",
"errorLiveStreamNotEnabled": "{{email}} पर लाइव स्ट्रीमिंग सक्षम नहीं है। कृपया लाइव स्ट्रीमिंग सक्षम करें या ऐसे खाते में लॉग इन करें जिसमें लाइव स्ट्रीमिंग सक्षम हो।",
"expandedOff": "लाइव स्ट्रीमिंग बंद हो गई है",
"expandedOn": "बैठक वर्तमान में YouTube पर स्ट्रीम की जा रही है।",
"expandedPending": "लाइव स्ट्रीमिंग शुरू की जा रही है…",
"failedToStart": "लाइव स्ट्रीमिंग शुरू करने में विफल रहा",
"getStreamKeyManually": "हम कोई लाइव स्ट्रीम प्राप्त नहीं कर सके। कृपया YouTube से अपनी लाइव स्ट्रीम कुंजी प्राप्त करने का प्रयास करें।",
"googlePrivacyPolicy": "Google गोपनीयता नीति",
"invalidStreamKey": "लाइव स्ट्रीम कुंजी गलत हो सकती है।",
"limitNotificationDescriptionNative": "आपकी स्ट्रीमिंग {{limit}} मिनट तक सीमित होगी। असीमित स्ट्रीमिंग के लिए {{app}} आज़माएँ।",
"limitNotificationDescriptionWeb": "अधिक मांग के कारण आपकी स्ट्रीमिंग {{limit}} मिनट तक सीमित होगी। असीमित स्ट्रीमिंग के लिए <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a> आज़माएँ।",
"off": "लाइव स्ट्रीमिंग बंद हो गई",
"offBy": "{{name}} ने लाइव स्ट्रीमिंग बंद कर दी",
"on": "लाइव स्ट्रीमिंग शुरू हो गई",
"onBy": "{{name}} ने लाइव स्ट्रीमिंग शुरू की",
"pending": "लाइव स्ट्रीम शुरू हो रही है…",
"serviceName": "लाइव स्ट्रीमिंग सेवा",
"signIn": "Google से साइन इन करें",
"signInCTA": "साइन इन करें या YouTube से अपनी लाइव स्ट्रीम कुंजी दर्ज करें।",
"signOut": "साइन आउट करें",
"signedInAs": "आप वर्तमान में इस रूप में साइन इन हैं:",
"start": "एक लाइव स्ट्रीम शुरू करें",
"streamIdHelp": "यह क्या है?",
"title": "लाइव स्ट्रीमिंग",
"unavailableTitle": "लाइव स्ट्रीमिंग उपलब्ध नहीं है",
"youtubeTerms": "YouTube सेवा की शर्तें"
},
"lobby": {
"allow": "अनुमति दें",
@@ -481,38 +481,38 @@
"notificationLobbyEnabled": "लॉबी को {{originParticipantName}}द्वारा सक्षम किया गया",
"notificationTitle": "लॉबी",
"passwordField": "मीटिंग पासवर्ड दर्ज करें",
"passwordJoinButton": "Join",
"passwordJoinButton": "शामिल हों",
"title": "लॉबी",
"toggleLabel": "लॉबी सक्षम करें"
},
"localRecording": {
"clientState": {
"off": "Off",
"on": "On",
"unknown": "Unknown"
"off": "बंद",
"on": "चालू",
"unknown": "अज्ञात"
},
"dialogTitle": "Local Recording Controls",
"duration": "Duration",
"durationNA": "N/A",
"encoding": "Encoding",
"label": "LOR",
"labelToolTip": "Local recording is engaged",
"localRecording": "Local Recording",
"me": "Me",
"dialogTitle": "स्थानीय रिकॉर्डिंग नियंत्रण",
"duration": "अवधि",
"durationNA": "उपलब्ध नहीं",
"encoding": "एन्कोडिंग",
"label": "स्थानीय रिकॉर्डिंग",
"labelToolTip": "स्थानीय रिकॉर्डिंग सक्रिय है",
"localRecording": "स्थानीय रिकॉर्डिंग",
"me": "मैं",
"messages": {
"engaged": "Local recording engaged.",
"finished": "Recording session {{token}} finished. Please send the recorded file to the moderator.",
"finishedModerator": "Recording session {{token}} finished. The recording of the local track has been saved. Please ask the other participants to submit their recordings.",
"notModerator": "You are not the moderator. You cannot start or stop local recording."
"engaged": "स्थानीय रिकॉर्डिंग सक्रिय हो गई।",
"finished": "रिकॉर्डिंग सत्र {{token}} समाप्त हो गया। कृपया रिकॉर्ड की गई फ़ाइल मॉडरेटर को भेजें।",
"finishedModerator": "रिकॉर्डिंग सत्र {{token}} समाप्त हो गया। स्थानीय ट्रैक की रिकॉर्डिंग सहेज ली गई है। कृपया अन्य प्रतिभागियों से उनकी रिकॉर्डिंग जमा करने के लिए कहें।",
"notModerator": "आप मॉडरेटर नहीं हैं। आप स्थानीय रिकॉर्डिंग प्रारंभ या बंद नहीं कर सकते।"
},
"moderator": "Moderator",
"no": "No",
"participant": "Participant",
"participantStats": "Participant Stats",
"sessionToken": "Session Token",
"start": "Start Recording",
"stop": "Stop Recording",
"yes": "Yes"
"moderator": "मॉडरेटर",
"no": "नहीं",
"participant": "प्रतिभागी",
"participantStats": "प्रतिभागी आँकड़े",
"sessionToken": "सत्र टोकन",
"start": "रिकॉर्डिंग प्रारंभ करें",
"stop": "रिकॉर्डिंग बंद करें",
"yes": "हाँ"
},
"lockRoomPassword": "पासवर्ड",
"lockRoomPasswordUppercase": "पासवर्ड",
@@ -536,8 +536,8 @@
"kickParticipant": "{{kicked}} को {{kicker}} द्वारा किक किया गया",
"me": "मैं",
"moderator": "मॉडरेटर के अधिकार दिए गए!",
"muted": "You have started the conversation muted.",
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
"muted": "आपने वार्तालाप को म्यूट करके शुरू किया है।",
"mutedRemotelyDescription": "जब आप बोलने के लिए तैयार हों, तो आप हमेशा अनम्यूट कर सकते हैं। बैठक में शोर कम रखने के लिए बोलने के बाद म्यूट कर दें।",
"mutedRemotelyTitle": "आपको {{participantDisplayName}} द्वारा म्यूट कर दिया गया है!",
"mutedTitle": "आप मौन हैं!",
"newDeviceAction": "उपयोग करें",
@@ -563,7 +563,7 @@
"reject": "अस्वीकार"
}
},
"passwordDigitsOnly": "Up to {{number}} digits",
"passwordDigitsOnly": "अधिकतम {{number}} अंक",
"passwordSetRemotely": "दूसरे प्रतिभागी द्वारा निर्धारित",
"polls": {
"errors": {
@@ -580,25 +580,25 @@
"callMeAtNumber": "मुझे इस नंबर पर कॉल करें:",
"calling": "कॉलिंग",
"configuringDevices": "डिवाइस कॉन्फ़िगर कर रहा है…",
"connectedWithAudioQ": "Youre connected with audio?",
"connectedWithAudioQ": "क्या आप ऑडियो से जुड़े हैं?",
"connection": {
"good": "Your internet connection looks good!",
"nonOptimal": "Your internet connection is not optimal",
"poor": "आपके पास एक खराब इंटरनेट कनेक्शन है"
"good": "आपका इंटरनेट कनेक्शन अच्छा है!",
"nonOptimal": "आपका इंटरनेट कनेक्शन आदर्श नहीं है",
"poor": "आपक इंटरनेट कनेक्शन खराब है"
},
"connectionDetails": {
"audioClipping": "We expect your audio to be clipped.",
"audioHighQuality": "We expect your audio to have excellent quality.",
"audioLowNoVideo": "We expect your audio quality to be low and no video.",
"goodQuality": "Awesome! Your media quality is going to be great.",
"noMediaConnectivity": "We could not find a way to establish media connectivity for this test. This is typically caused by a firewall or NAT.",
"noVideo": "We expect that your video will be terrible.",
"undetectable": "If you still can not make calls in browser, we recommend that you make sure your speakers, microphone and camera are properly set up, that you have granted your browser rights to use your microphone and camera, and that your browser version is up-to-date. If you still have trouble calling, you should contact the web application developer.",
"veryPoorConnection": "We expect your call quality to be really terrible.",
"videoFreezing": "We expect your video to freeze, turn black, and be pixelated.",
"videoHighQuality": "We expect your video to have good quality.",
"videoLowQuality": "We expect your video to have low quality in terms of frame rate and resolution.",
"videoTearing": "We expect your video to be pixelated or have visual artefacts."
"audioClipping": "हमें उम्मीद है कि आपका ऑडियो कट सकता है।",
"audioHighQuality": "हमें उम्मीद है कि आपका ऑडियो बेहतरीन गुणवत्ता का होगा।",
"audioLowNoVideo": "हमें उम्मीद है कि आपकी ऑडियो गुणवत्ता कम होगी और वीडियो उपलब्ध नहीं होगा।",
"goodQuality": "बहुत बढ़िया! आपकी मीडिया गुणवत्ता शानदार होगी।",
"noMediaConnectivity": "हम इस परीक्षण के लिए मीडिया कनेक्टिविटी स्थापित करने में असमर्थ हैं। यह आमतौर पर फ़ायरवॉल या NAT के कारण होता है।",
"noVideo": "हमें उम्मीद है कि आपका वीडियो बहुत खराब होगा।",
"undetectable": "यदि आप अभी भी ब्राउज़र में कॉल नहीं कर पा रहे हैं, तो हम अनुशंसा करते हैं कि आप सुनिश्चित करें कि आपके स्पीकर, माइक्रोफ़ोन और कैमरा सही तरीके से सेट किए गए हैं, कि आपने अपने ब्राउज़र को माइक्रोफ़ोन और कैमरा उपयोग की अनुमति दी है, और आपका ब्राउज़र संस्करण अपडेट है। यदि समस्या बनी रहती है, तो आपको वेब एप्लिकेशन डेवलपर से संपर्क करना चाहिए।",
"veryPoorConnection": "हमें उम्मीद है कि आपकी कॉल गुणवत्ता बहुत खराब होगी।",
"videoFreezing": "हमें उम्मीद है कि आपका वीडियो फ्रीज़ होगा, काला हो जाएगा और धुंधला दिखेगा।",
"videoHighQuality": "हमें उम्मीद है कि आपका वीडियो अच्छी गुणवत्ता का होगा।",
"videoLowQuality": "हमें उम्मीद है कि आपका वीडियो फ्रेम दर और रिज़ॉल्यूशन के मामले में निम्न गुणवत्ता का होगा।",
"videoTearing": "हमें उम्मीद है कि आपका वीडियो धुंधला होगा या इसमें दृश्य गड़बड़ियां हो सकती हैं।"
},
"copyAndShare": "मीटिंग लिंक कॉपी और साझा करे ",
"dialInMeeting": "मीटिंग में डायल करें",
@@ -637,7 +637,7 @@
"disconnected": "डिस्कनेक्ट किया गया",
"expired": "एक्सपायर्ड",
"ignored": "Ignored",
"initializingCall": "Initializing Call…",
"initializingCall": "कॉल प्रारंभ की जा रही है…",
"invited": "आमंत्रित",
"rejected": "अस्वीकृत",
"ringing": "Ringing…"
@@ -650,38 +650,38 @@
},
"raisedHand": "बोलना चाहेंगे",
"recording": {
"authDropboxText": "Upload to Dropbox",
"availableSpace": "Available space: {{spaceLeft}} MB (approximately {{duration}} minutes of recording)",
"beta": "BETA",
"busy": "We're working on freeing recording resources. Please try again in a few minutes.",
"authDropboxText": "ड्रॉपबॉक्स पर अपलोड करें",
"availableSpace": "उपलब्ध स्थान: {{spaceLeft}} MB (लगभग {{duration}} मिनट की रिकॉर्डिंग)",
"beta": "बीटा",
"busy": "हम रिकॉर्डिंग संसाधनों को मुक्त करने पर काम कर रहे हैं। कृपया कुछ मिनटों में पुनः प्रयास करें।",
"busyTitle": "सभी रिकॉर्डर अभी व्यस्त हैं",
"error": "रिकॉर्डिंग विफल हुई। कृपया पुन: प्रयास करें।",
"error": "रिकॉर्डिंग विफल हुई। कृपया पुन प्रयास करें।",
"expandedOff": "रिकॉर्डिंग बंद हो गई है",
"expandedOn": "The meeting is currently being recorded.",
"expandedOn": "बैठक की रिकॉर्डिंग की जा रही है।",
"expandedPending": "रिकॉर्डिंग शुरू की जा रही है…",
"failedToStart": "रिकॉर्डिंग शुरू करने में विफलता हुई।",
"fileSharingdescription": "Share recording with meeting participants",
"limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"live": "LIVE",
"loggedIn": "Logged in as {{userName}}",
"off": "Recording stopped",
"offBy": "{{name}} stopped the recording",
"on": "Recording started",
"onBy": "{{name}} started the recording",
"pending": "Preparing to record the meeting…",
"rec": "REC",
"serviceDescription": "Your recording will be saved by the recording service",
"serviceDescriptionCloud": "Cloud recording",
"serviceName": "Recording service",
"signIn": "Sign in",
"signOut": "Sign out",
"fileSharingdescription": "रिकॉर्डिंग को बैठक प्रतिभागियों के साथ साझा करें",
"limitNotificationDescriptionNative": "उच्च मांग के कारण आपकी रिकॉर्डिंग {{limit}} मिनट तक सीमित रहेगी। असीमित रिकॉर्डिंग के लिए <3>{{app}}</3> आज़माएँ।",
"limitNotificationDescriptionWeb": "उच्च मांग के कारण आपकी रिकॉर्डिंग {{limit}} मिनट तक सीमित रहेगी। असीमित रिकॉर्डिंग के लिए <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a> आज़माएँ।",
"live": "लाइव",
"loggedIn": "{{userName}} के रूप में लॉग इन किया गया",
"off": "रिकॉर्डिंग बंद हो गई",
"offBy": "{{name}} ने रिकॉर्डिंग बंद की",
"on": "रिकॉर्डिंग शुरू हो गई",
"onBy": "{{name}} ने रिकॉर्डिंग शुरू की",
"pending": "बैठक की रिकॉर्डिंग की तैयारी हो रही है…",
"rec": "रिकॉर्डिंग",
"serviceDescription": "आपकी रिकॉर्डिंग को रिकॉर्डिंग सेवा द्वारा सहेजा जाएगा",
"serviceDescriptionCloud": "क्लाउड रिकॉर्डिंग",
"serviceName": "रिकॉर्डिंग सेवा",
"signIn": "साइन इन करें",
"signOut": "साइन आउट करें",
"title": "रिकॉर्डिंग",
"unavailable": "ओह! {{serviceName}} वर्तमान में अनुपलब्ध है। हम समस्या को हल करने पर काम कर रहे हैं। कृपया बाद में पुनः प्रयास करें।",
"unavailable": "ओह! {{serviceName}} वर्तमान में अनुपलब्ध है। हम इस समस्या को हल करने पर काम कर रहे हैं। कृपया बाद में पुनः प्रयास करें।",
"unavailableTitle": "रिकॉर्डिंग उपलब्ध नहीं है"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"
"pullToRefresh": "रीफ़्रेश करने के लिए नीचे खींचें"
},
"security": {
"about": "आप अपनी मीटिंग में $t(lockRoomPassword) जोड़ सकते हैं। सहभागियों को मीटिंग में शामिल होने से पहले $t(lockRoomPassword) प्रदान करना होगा।",
@@ -691,16 +691,16 @@
},
"settings": {
"calendar": {
"about": "The {{appName}} calendar integration is used to securely access your calendar so it can read upcoming events.",
"disconnect": "Disconnect",
"microsoftSignIn": "Sign in with Microsoft",
"signedIn": "Currently accessing calendar events for {{email}}. Click the Disconnect button below to stop accessing calendar events.",
"title": "Calendar"
"about": "{{appName}} कैलेंडर एकीकरण आपके कैलेंडर तक सुरक्षित रूप से पहुंचने के लिए उपयोग किया जाता है ताकि यह आगामी कार्यक्रम पढ़ सके।",
"disconnect": "डिस्कनेक्ट करें",
"microsoftSignIn": "Microsoft से साइन इन करें",
"signedIn": "वर्तमान में {{email}} के कैलेंडर कार्यक्रमों तक पहुंच रही है। कैलेंडर कार्यक्रमों की पहुंच बंद करने के लिए नीचे दिए गए डिस्कनेक्ट बटन पर क्लिक करें।",
"title": "कैलेंडर"
},
"devices": "डिवाइस",
"followMe": "Everyone follows me",
"followMe": "हर कोई मेरा अनुसरण करेगा",
"language": "भाषा",
"loggedIn": "Logged in as {{name}}",
"loggedIn": "{{name}} के रूप में लॉग इन किया",
"microphones": "माइक्रोफोन",
"moderator": "Moderator",
"more": "More",
@@ -710,8 +710,8 @@
"selectCamera": "कैमरा",
"selectMic": "माइक्रोफोन",
"speakers": "Speakers",
"startAudioMuted": "Everyone starts muted",
"startVideoMuted": "Everyone starts hidden",
"startAudioMuted": "सभी लोग म्यूट से शुरू करेंगे",
"startVideoMuted": "सभी लोग छिपे हुए शुरू करेंगे",
"title": "सेटिंग"
},
"settingsView": {
@@ -720,9 +720,9 @@
"alertOk": "ओके",
"alertTitle": "चेतावनी",
"alertURLText": "दर्ज किया गया सर्वर URL अमान्य है",
"buildInfoSection": "Build Information",
"buildInfoSection": "बिल्ड जानकारी",
"conferenceSection": "सम्मेलन",
"disableCallIntegration": "Disable native call integration",
"disableCallIntegration": "मूल कॉल एकीकरण अक्षम करें",
"disableCrashReporting": "क्रैश रिपोर्टिंग अक्षम करें",
"disableCrashReportingWarning": "क्या आप वाकई क्रैश रिपोर्टिंग को अक्षम करना चाहते हैं? एप्लिकेशन को पुनरारंभ करने के बाद सेटिंग लागू की जाएगी",
"disableP2P": "पीयर-टू-पीयर मोड को अक्षम करें",
@@ -731,16 +731,16 @@
"header": "सेटिंग",
"profileSection": "प्रोफाइल",
"serverURL": "सर्वर URL",
"showAdvanced": "Show advanced settings",
"startWithAudioMuted": "Start with audio muted",
"startWithVideoMuted": "Start with video muted",
"showAdvanced": "उन्नत सेटिंग्स दिखाएं",
"startWithAudioMuted": "ऑडियो म्यूट के साथ शुरू करें",
"startWithVideoMuted": "वीडियो म्यूट के साथ शुरू करें",
"version": "संस्करण"
},
"share": {
"dialInfoText": "\n\n=====\n\nJust want to dial in on your phone?\n\n{{defaultDialInNumber}}Click this link to see the dial in phone numbers for this meeting\n{{dialInfoPageUrl}}",
"mainText": "मीटिंग में शामिल होने के लिए निम्न लिंक पर क्लिक करें:\n{{roomUrl}}"
},
"speaker": "Speaker",
"speaker": "स्पीकर",
"speakerStats": {
"hours": "{{count}}h",
"minutes": "{{count}}m",
@@ -748,8 +748,8 @@
"search": "खोजें",
"searchHint": "प्रतिभागियों को खोजें",
"seconds": "{{count}}s",
"speakerStats": "Speaker Stats",
"speakerTime": "Speaker Time"
"speakerStats": "स्पीकर आंकड़े",
"speakerTime": "स्पीकर समय"
},
"startupoverlay": {
"genericTitle": "मीटिंग को आपके माइक्रोफ़ोन और कैमरे का उपयोग करने की आवश्यकता है।",
@@ -825,10 +825,10 @@
"download": "हमारे एप्लिकेशन डाउनलोड करें",
"e2ee": "एंड-टू-एंड एन्क्रिप्शन",
"embedMeeting": "Embed meeting",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view",
"enterFullScreen": "पूर्ण स्क्रीन में देखें",
"enterTileView": "टाइल दृश्य में प्रवेश करें",
"exitFullScreen": "पूर्ण स्क्रीन से बाहर निकलें",
"exitTileView": "टाइल दृश्य से बाहर निकलें",
"feedback": "प्रतिक्रिया छोड़ें",
"hangup": "छोड़ें",
"help": "Help",
@@ -837,7 +837,7 @@
"lobbyButtonEnable": "लॉबी मोड सक्षम करें",
"login": "लॉग इन",
"logout": "लॉगआउट",
"lowerYourHand": "Lower your hand",
"lowerYourHand": "अपना हाथ नीचे करें",
"moreActions": "More actions",
"moreOptions": "अधिक विकल्प",
"mute": "म्यूट / अनम्यूट",
@@ -866,7 +866,7 @@
"startSubtitles": "Start subtitles",
"stopScreenSharing": "स्क्रीन शेयरिंग बंद करो",
"stopSharedVideo": "YouTube वीडियो बंद करें",
"stopSubtitles": "Stop subtitles",
"stopSubtitles": "उपशीर्षक बंद करें",
"talkWhileMutedPopup": "बोलने की कोशिश कर रहा है? आप मौन हैं",
"tileViewToggle": "टॉगल टाइल दृश्य",
"toggleCamera": "कैमरा टॉगल करें",
@@ -874,13 +874,13 @@
"videomute": "स्टार्ट / स्टॉप कैमरा"
},
"transcribing": {
"ccButtonTooltip": "Start / Stop subtitles",
"ccButtonTooltip": "सबटाइटल शुरू / बंद करें",
"error": "ट्रांसक्रिप्शनिंग विफल रही। कृपया पुन: प्रयास करें",
"expandedLabel": "वर्तमान में ट्रांसक्रिप्शनिंग चालू है",
"failedToStart": "ट्रांसक्रिप्शनिंग प्रारंभ करने में विफल",
"labelToolTip": "The meeting is being transcribed",
"labelToolTip": "बैठक का लिप्यंतरण किया जा रहा है",
"off": "ट्रांसक्रिप्शनिंग बंद कर दिया",
"pending": "Preparing to transcribe the meeting…",
"pending": "बैठक के ट्रांसक्रिप्शन की तैयारी हो रही है…",
"start": "उपशीर्षक दिखाना शुरू करें",
"stop": "उपशीर्षक दिखाना बंद करें",
"tr": "TR"
@@ -899,20 +899,20 @@
"pending": "{{displayName}} को आमंत्रित किया गया है"
},
"videoStatus": {
"audioOnly": "AUD",
"audioOnlyExpanded": "You are in low bandwidth mode. In this mode you will receive only audio and screen sharing.",
"callQuality": "Video Quality",
"hd": "HD",
"hdTooltip": "Viewing high definition video",
"highDefinition": "High definition",
"labelTooiltipNoVideo": "No video",
"labelTooltipAudioOnly": "Low bandwidth mode enabled",
"ld": "LD",
"ldTooltip": "Viewing low definition video",
"lowDefinition": "Low definition",
"sd": "SD",
"sdTooltip": "Viewing standard definition video",
"standardDefinition": "Standard definition"
"audioOnly": "केवल ऑडियो",
"audioOnlyExpanded": "आप कम बैंडविड्थ मोड में हैं। इस मोड में आपको केवल ऑडियो और स्क्रीन शेयरिंग प्राप्त होगी।",
"callQuality": "वीडियो गुणवत्ता",
"hd": "एचडी",
"hdTooltip": "हाई डेफिनिशन वीडियो देख रहे हैं",
"highDefinition": "हाई डेफिनिशन",
"labelTooiltipNoVideo": "कोई वीडियो नहीं",
"labelTooltipAudioOnly": "कम बैंडविड्थ मोड सक्षम",
"ld": "एलडी",
"ldTooltip": "लो डेफिनिशन वीडियो देख रहे हैं",
"lowDefinition": "लो डेफिनिशन",
"sd": "एसडी",
"sdTooltip": "स्टैंडर्ड डेफिनिशन वीडियो देख रहे हैं",
"standardDefinition": "स्टैंडर्ड डेफिनिशन"
},
"videothumbnail": {
"connectionInfo": "कनेक्शन जानकारी",

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-nb.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -733,7 +733,9 @@
"me": "me",
"notify": {
"OldElectronAPPTitle": "Security vulnerability!",
"allowAction": "Allow",
"allowAudio": "Allow Audio",
"allowBoth": "Both",
"allowVideo": "Allow Video",
"allowedUnmute": "You can unmute your microphone, start your camera or share your screen.",
"audioUnmuteBlockedDescription": "Mic unmute operation has been temporarily blocked because of system limits.",
"audioUnmuteBlockedTitle": "Mic unmute blocked!",
@@ -755,7 +757,7 @@
"focusFail": "{{component}} not available - retry in {{ms}} sec",
"gifsMenu": "GIPHY",
"groupTitle": "Notifications",
"hostAskedUnmute": "The moderator would like you to speak",
"hostAskedUnmute": "The moderator would like you to participate.",
"invalidTenant": "Invalid tenant",
"invalidTenantHyphenDescription": "The tenant you are using is invalid (starts or ends with '-').",
"invalidTenantLengthDescription": "The tenant you are using is too long.",
@@ -807,7 +809,7 @@
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"raiseHandAction": "Raise hand",
"raisedHand": "Would like to speak.",
"raisedHand": "Would like to participate.",
"raisedHands": "{{participantName}} and {{raisedHands}} more people",
"reactionSounds": "Disable sounds",
"reactionSoundsForAll": "Disable sounds for all",
@@ -824,7 +826,8 @@
"suggestRecordingAction": "Start",
"suggestRecordingDescription": "Would you like to start a recording?",
"suggestRecordingTitle": "Record this meeting",
"unmute": "Unmute",
"unmute": "Unmute Audio",
"unmuteVideo": "Unmute Video",
"videoMutedRemotelyDescription": "You can always turn it on again.",
"videoMutedRemotelyTitle": "Your video has been turned off by {{participantDisplayName}}",
"videoUnmuteBlockedDescription": "Camera unmute and desktop sharing operation have been temporarily blocked because of system limits.",

View File

@@ -492,7 +492,8 @@ function initCommands() {
APP.store.dispatch(toggleRequestingSubtitles());
},
'set-subtitles': (enabled, displaySubtitles, language) => {
APP.store.dispatch(setRequestingSubtitles(enabled, displaySubtitles, language));
APP.store.dispatch(setRequestingSubtitles(
enabled, displaySubtitles, language ? `translation-languages:${language}` : null));
},
'toggle-tile-view': () => {
sendAnalytics(createApiEvent('tile-view.toggled'));

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/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/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/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"integrity": "sha512-9gWT8koE7bS/32LuYrUKdsFYjJ0mkyQ1ctANG0KlRnEDqIzx4T+C+6F+RltiytSNxsMC+08+h1uC4BSxgqzyng==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/lib-jitsi-meet.tgz",
"integrity": "sha512-erPBz93xzWDIvW9EdvSfiraHFi0TMo1W68zxe7rKvIQWX1DCjmKxWKnxdq5WirSD7MXwoSIxgdX4PB7Wz3aTmg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -37637,8 +37637,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"integrity": "sha512-9gWT8koE7bS/32LuYrUKdsFYjJ0mkyQ1ctANG0KlRnEDqIzx4T+C+6F+RltiytSNxsMC+08+h1uC4BSxgqzyng==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/lib-jitsi-meet.tgz",
"integrity": "sha512-erPBz93xzWDIvW9EdvSfiraHFi0TMo1W68zxe7rKvIQWX1DCjmKxWKnxdq5WirSD7MXwoSIxgdX4PB7Wz3aTmg==",
"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/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1915.0.0+6e9b9c01/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

@@ -4,6 +4,7 @@ import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { getConferenceState } from '../base/conference/functions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { MEDIA_TYPE, MediaType } from '../base/media/constants';
import { isAudioMuted, isVideoMuted } from '../base/media/functions';
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { raiseHand } from '../base/participants/actions';
import {
@@ -208,24 +209,46 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch }, previousConference) => {
(conference, { dispatch, getState }, previousConference) => {
if (conference && !previousConference) {
// local participant is allowed to unmute
conference.on(JitsiConferenceEvents.AV_MODERATION_APPROVED, ({ mediaType }: { mediaType: MediaType; }) => {
dispatch(localParticipantApproved(mediaType));
// Audio & video moderation are both enabled at the same time.
// Avoid displaying 2 different notifications.
if (mediaType === MEDIA_TYPE.AUDIO) {
dispatch(showNotification({
titleKey: 'notify.hostAskedUnmute',
sticky: true,
customActionNameKey: [ 'notify.unmute' ],
customActionHandler: [ () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO)) ],
uid: ASKED_TO_UNMUTE_NOTIFICATION_ID
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
const customActionNameKey = [];
const customActionHandler = [];
if ((mediaType === MEDIA_TYPE.AUDIO || getState()['features/av-moderation'].audioUnmuteApproved)
&& isAudioMuted(getState())) {
customActionNameKey.push('notify.unmute');
customActionHandler.push(() => {
dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
});
}
if ((mediaType === MEDIA_TYPE.VIDEO || getState()['features/av-moderation'].videoUnmuteApproved)
&& isVideoMuted(getState())) {
customActionNameKey.push('notify.unmuteVideo');
customActionHandler.push(() => {
dispatch(muteLocal(false, MEDIA_TYPE.VIDEO));
dispatch(hideNotification(ASKED_TO_UNMUTE_NOTIFICATION_ID));
// lower hand as there will be no audio and change in dominant speaker to clear it
dispatch(raiseHand(false));
});
}
dispatch(showNotification({
titleKey: 'notify.hostAskedUnmute',
sticky: true,
customActionNameKey,
customActionHandler,
uid: ASKED_TO_UNMUTE_NOTIFICATION_ID
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
});
conference.on(JitsiConferenceEvents.AV_MODERATION_REJECTED, ({ mediaType }: { mediaType: MediaType; }) => {

View File

@@ -292,6 +292,7 @@ export interface IConfig {
disableAddingBackgroundImages?: boolean;
disableAudioLevels?: boolean;
disableBeforeUnloadHandlers?: boolean;
disableCameraTintForeground?: boolean;
disableChatSmileys?: boolean;
disableDeepLinking?: boolean;
disableFilmstripAutohiding?: boolean;
@@ -479,6 +480,7 @@ export interface IConfig {
noiseSuppression?: INoiseSuppressionConfig;
noticeMessage?: string;
notificationTimeouts?: {
extraLong?: number;
long?: number;
medium?: number;
short?: number;

View File

@@ -91,6 +91,7 @@ export default [
'disableAddingBackgroundImages',
'disableAudioLevels',
'disableBeforeUnloadHandlers',
'disableCameraTintForeground',
'disableChatSmileys',
'disableDeepLinking',
'disabledNotifications',

View File

@@ -39,9 +39,10 @@ export default class JitsiMeetLogStorage {
* <tt>false</tt> otherwise.
*/
isReady() {
const { conference } = this.getState()['features/base/conference'];
const { conference, error: conferenceError } = this.getState()['features/base/conference'];
const { error: connectionError } = this.getState()['features/base/connection'];
return Boolean(conference);
return Boolean(conference || conferenceError || connectionError);
}
/**

View File

@@ -4,7 +4,7 @@ import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT } from '../app/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../conference/actionTypes';
import { getCurrentConference } from '../conference/functions';
import { SET_CONFIG } from '../config/actionTypes';
import JitsiMeetJS, {
@@ -35,6 +35,15 @@ MiddlewareRegistry.register(store => next => action => {
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case CONFERENCE_FAILED: {
const result = next(action);
const { logCollector } = store.getState()['features/base/logging'];
logCollector?.flush();
return result;
}
case LIB_WILL_INIT:
return _libWillInit(store, next, action);

View File

@@ -3,12 +3,12 @@ import { batch } from 'react-redux';
import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { approveParticipant } from '../../av-moderation/actions';
import { approveParticipant, approveParticipantAudio, approveParticipantVideo } from '../../av-moderation/actions';
import { UPDATE_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { getBreakoutRooms } from '../../breakout-rooms/functions';
import { toggleE2EE } from '../../e2ee/actions';
import { MAX_MODE } from '../../e2ee/constants';
import { showNotification } from '../../notifications/actions';
import { hideNotification, showNotification } from '../../notifications/actions';
import {
LOCAL_RECORDING_NOTIFICATION_ID,
NOTIFICATION_TIMEOUT_TYPE,
@@ -782,20 +782,43 @@ function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiCon
const isModerator = isLocalParticipantModerator(state);
const participant = getParticipantById(state, participantId);
let shouldDisplayAllowAction = false;
let shouldDisplayAllowAudio = false;
let shouldDisplayAllowVideo = false;
if (isModerator) {
shouldDisplayAllowAction = isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
|| isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
shouldDisplayAllowAudio = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
shouldDisplayAllowVideo = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
}
let action;
if (shouldDisplayAllowAction) {
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo) {
action = {
customActionNameKey: [ 'notify.allowAction' ],
customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
customActionNameKey: [] as string[],
customActionHandler: [] as Function[]
};
if (shouldDisplayAllowAudio) {
action.customActionNameKey.push('notify.allowAudio');
action.customActionHandler.push(() => {
dispatch(approveParticipantAudio(participantId));
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
});
}
if (shouldDisplayAllowVideo) {
action.customActionNameKey.push('notify.allowVideo');
action.customActionHandler.push(() => {
dispatch(approveParticipantVideo(participantId));
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
});
}
if (shouldDisplayAllowAudio && shouldDisplayAllowVideo) {
action.customActionNameKey.push('notify.allowBoth');
action.customActionHandler.push(() => {
dispatch(approveParticipant(participantId));
dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
});
}
} else {
action = {
customActionNameKey: [ 'notify.viewParticipants' ],

View File

@@ -1363,6 +1363,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any): Object {
// skip showing tint for owner participants that are screensharing.
&& !screenshareParticipantIds.includes(id);
const disableTintForeground = state['features/base/config'].disableCameraTintForeground ?? false;
return {
_audioTrack,
@@ -1384,7 +1385,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any): Object {
_raisedHand: hasRaisedHand(participant),
_stageFilmstripLayout: isStageFilmstripAvailable(state),
_stageParticipantsVisible: _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW,
_shouldDisplayTintBackground: shouldDisplayTintBackground,
_shouldDisplayTintBackground: !disableTintForeground && shouldDisplayTintBackground,
_thumbnailType: tileType,
_videoObjectPosition: getVideoObjectPosition(state, participant?.id),
_videoTrack,

View File

@@ -1,6 +1,7 @@
import { throttle } from 'lodash-es';
import { IStore } from '../app/types';
import { IConfig } from '../base/config/configType';
import { NOTIFICATIONS_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { getParticipantCount } from '../base/participants/functions';
@@ -28,17 +29,15 @@ import { INotificationProps } from './types';
* @param {Object} notificationTimeouts - Config notification timeouts.
* @returns {number}
*/
function getNotificationTimeout(type?: string, notificationTimeouts?: {
long?: number;
medium?: number;
short?: number;
}) {
function getNotificationTimeout(type?: string, notificationTimeouts?: IConfig['notificationTimeouts']) {
if (type === NOTIFICATION_TIMEOUT_TYPE.SHORT) {
return notificationTimeouts?.short ?? NOTIFICATION_TIMEOUT.SHORT;
} else if (type === NOTIFICATION_TIMEOUT_TYPE.MEDIUM) {
return notificationTimeouts?.medium ?? NOTIFICATION_TIMEOUT.MEDIUM;
} else if (type === NOTIFICATION_TIMEOUT_TYPE.LONG) {
return notificationTimeouts?.long ?? NOTIFICATION_TIMEOUT.LONG;
} else if (type === NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG) {
return notificationTimeouts?.extraLong ?? NOTIFICATION_TIMEOUT.EXTRA_LONG;
}
return NOTIFICATION_TIMEOUT.STICKY;

View File

@@ -5,6 +5,7 @@ export const NOTIFICATION_TIMEOUT = {
SHORT: 2500,
MEDIUM: 5000,
LONG: 10000,
EXTRA_LONG: 60000,
STICKY: false
};
@@ -12,6 +13,7 @@ export const NOTIFICATION_TIMEOUT = {
* Notification timeout type.
*/
export enum NOTIFICATION_TIMEOUT_TYPE {
EXTRA_LONG = 'extra_long',
LONG = 'long',
MEDIUM = 'medium',
SHORT = 'short',

View File

@@ -159,12 +159,12 @@ export function getQuickActionButtonType(
if (!isVideoMuted) {
return QUICK_ACTION_BUTTON.STOP_VIDEO;
}
if (isVideoForceMuted) {
return QUICK_ACTION_BUTTON.ALLOW_VIDEO;
}
if (isSupported()(state) && !isParticipantSilent) {
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
}
if (isVideoForceMuted) {
return QUICK_ACTION_BUTTON.ALLOW_VIDEO;
}
}
return QUICK_ACTION_BUTTON.NONE;

View File

@@ -45,7 +45,9 @@ const PollCreate = (props: AbstractProps) => {
useEffect(() => {
answerInputs.current = answerInputs.current.slice(0, answers.length);
setTimeout(() => {
answerListRef.current?.scrollToEnd({ animated: true });
}, 1000);
}, [ answers ]);
/*

View File

@@ -468,6 +468,6 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
} ],
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
};
}

View File

@@ -114,6 +114,7 @@ const ProfileView = ({ isInWelcomePage }: {
return (
<JitsiScreen
disableForcedKeyboardDismiss = { true }
hasBottomTextInput = { true }
// @ts-ignore
safeAreaInsets = { [ !isInWelcomePage && 'bottom', 'left', 'right' ].filter(Boolean) as Edge[] }

View File

@@ -11,7 +11,7 @@ import { shouldShowModeratedNotification } from '../av-moderation/functions';
import { setAudioMuted, setVideoMuted } from '../base/media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_MUTISM_AUTHORITY } from '../base/media/constants';
import { muteRemoteParticipant } from '../base/participants/actions';
import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions';
import { getRemoteParticipants } from '../base/participants/functions';
import { toggleScreensharing } from '../base/tracks/actions';
import { isModerationNotificationDisplayed } from '../notifications/functions';
@@ -36,7 +36,7 @@ export function muteLocal(enable: boolean, mediaType: MediaType, stopScreenShari
}
// check for A/V Moderation when trying to unmute
if (!enable && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, getState())) {
if (isAudio && !enable && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, getState())) {
if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, getState())) {
dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
}
@@ -88,11 +88,6 @@ export function muteRemote(participantId: string, mediaType: MediaType) {
export function muteAllParticipants(exclude: Array<string>, mediaType: MediaType) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const localId = getLocalParticipant(state)?.id ?? '';
if (!exclude.includes(localId)) {
dispatch(muteLocal(true, mediaType, mediaType !== MEDIA_TYPE.AUDIO));
}
getRemoteParticipants(state).forEach((p, id) => {
if (exclude.includes(id)) {

View File

@@ -84,6 +84,7 @@ s2s_whitelist = {
```
Component "visitors.jitmeet.example.com" "visitors_component"
auto_allow_visitor_promotion = true
admins = { "focus@auth.jitmeet.example.com" }
```
- Make sure you add the correct upstreams to nginx config
```

View File

@@ -48,7 +48,7 @@ function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
body_json.type = 'av_moderation';
body_json.enabled = enable;
body_json.room = internal_room_jid_match_rewrite(room.jid);
body_json.actor = actorJid;
body_json.actor = internal_room_jid_match_rewrite(actorJid);
body_json.mediaType = mediaType;
local body_json_str, error = json.encode(body_json);

View File

@@ -348,8 +348,11 @@ module:hook('muc-broadcast-presence', function (event)
is_moderator = true;
end
elseif session.auth_token and auto_promoted_with_token then
-- non-vpaas and having a token is considered a moderator
is_moderator = true;
if not session.jitsi_meet_tenant_mismatch or session.jitsi_web_query_prefix == '' then
-- non-vpaas and having a token is considered a moderator, and if it is not in '/' tenant
-- the tenant from url and token should match
is_moderator = true;
end
end
end
end

View File

@@ -26,6 +26,9 @@
# The kid to use in the token
#JWT_KID=
# 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

@@ -93,6 +93,24 @@ export class Participant {
this._jwt = jwt;
}
/**
* A wrapper for <tt>this.driver.execute</tt> that would catch errors, print them and throw them again.
*
* @param {string | ((...innerArgs: InnerArguments) => ReturnValue)} script - The script that will be executed.
* @param {any[]} args - The rest of the arguments.
* @returns {ReturnValue} - The result of the script.
*/
async execute<ReturnValue, InnerArguments extends any[]>(
script: string | ((...innerArgs: InnerArguments) => ReturnValue),
...args: InnerArguments): Promise<ReturnValue> {
try {
return await this.driver.execute(script, ...args);
} catch (error) {
console.error('An error occured while trying to execute a script: ', error);
throw error;
}
}
/**
* Returns participant endpoint ID.
*
@@ -100,7 +118,7 @@ export class Participant {
*/
async getEndpointId(): Promise<string> {
if (!this._endpointId) {
this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
this._endpointId = await this.execute(() => { // eslint-disable-line arrow-body-style
return APP?.conference?.getMyUserId();
});
}
@@ -218,7 +236,7 @@ export class Participant {
const parallel = [];
parallel.push(driver.execute((name, sessionId, prefix) => {
parallel.push(this.execute((name, sessionId, prefix) => {
APP?.UI?.dockToolbar(true);
// disable keyframe animations (.fadeIn and .fadeOut classes)
@@ -262,7 +280,7 @@ export class Participant {
*/
async waitForPageToLoad(): Promise<void> {
return this.driver.waitUntil(
() => this.driver.execute(() => document.readyState === 'complete'),
() => this.execute(() => document.readyState === 'complete'),
{
timeout: 30_000, // 30 seconds
timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
@@ -285,14 +303,14 @@ export class Participant {
* Checks if the participant is in the meeting.
*/
isInMuc() {
return this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
return this.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
}
/**
* Checks if the participant is a moderator in the meeting.
*/
async isModerator() {
return await this.driver.execute(() => typeof APP !== 'undefined'
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
}
@@ -300,7 +318,7 @@ export class Participant {
* Checks if the meeting supports breakout rooms.
*/
async isBreakoutRoomsSupported() {
return await this.driver.execute(() => typeof APP !== 'undefined'
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
}
@@ -308,7 +326,7 @@ export class Participant {
* Checks if the participant is in breakout room.
*/
async isInBreakoutRoom() {
return await this.driver.execute(() => typeof APP !== 'undefined'
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
}
@@ -332,11 +350,9 @@ export class Participant {
*
* @returns {Promise<void>}
*/
async waitForIceConnected(): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() =>
driver.execute(() => APP?.conference?.getConnectionState() === 'connected'), {
waitForIceConnected(): Promise<void> {
return this.driver.waitUntil(() =>
this.execute(() => APP?.conference?.getConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
});
@@ -347,11 +363,9 @@ export class Participant {
*
* @returns {Promise<void>}
*/
async waitForP2PIceConnected(): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() =>
driver.execute(() => APP?.conference?.getP2PConnectionState() === 'connected'), {
waitForP2PIceConnected(): Promise<void> {
return this.driver.waitUntil(() =>
this.execute(() => APP?.conference?.getP2PConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: `expected P2P ICE to be connected for 15s for ${this.name}`
});
@@ -377,7 +391,7 @@ export class Participant {
const lMsg = msg ?? `expected to ${
checkSend && checkReceive ? 'receive/send' : checkSend ? 'send' : 'receive'} data in 15s for ${this.name}`;
return this.driver.waitUntil(() => this.driver.execute((pCheckSend: boolean, pCheckReceive: boolean) => {
return this.driver.waitUntil(() => this.execute((pCheckSend: boolean, pCheckReceive: boolean) => {
const stats = APP?.conference?.getStats();
const bitrateMap = stats?.bitrate || {};
const rtpStats = {
@@ -398,11 +412,11 @@ export class Participant {
* @param {number} number - The number of remote streams to wait for.
* @returns {Promise<void>}
*/
waitForRemoteStreams(number: number): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() =>
driver.execute(count => (APP?.conference?.getNumberOfParticipantsWithTracks() ?? -1) >= count, number), {
async waitForRemoteStreams(number: number): Promise<void> {
return await this.driver.waitUntil(async () => await this.execute(
count => (APP?.conference?.getNumberOfParticipantsWithTracks() ?? -1) >= count,
number
), {
timeout: 15_000,
timeoutMsg: `expected number of remote streams:${number} in 15s for ${this.name}`
});
@@ -416,10 +430,8 @@ export class Participant {
* @returns {Promise<void>}
*/
waitForParticipants(number: number, msg?: string): Promise<void> {
const driver = this.driver;
return driver.waitUntil(
() => driver.execute(count => (APP?.conference?.listMembers()?.length ?? -1) === count, number),
return this.driver.waitUntil(
() => this.execute(count => (APP?.conference?.listMembers()?.length ?? -1) === count, number),
{
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
@@ -577,7 +589,7 @@ export class Participant {
}
// do a hangup, to make sure unavailable presence is sent
await this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
await this.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
// change of url
@@ -718,7 +730,7 @@ export class Participant {
async getRemoteAudioLevel(p: Participant) {
const jid = await p.getEndpointId();
return await this.driver.execute(id => {
return await this.execute(id => {
const level = APP?.conference?.getPeerSSRCAudioLevel(id);
return level ? level.toFixed(2) : null;
@@ -790,26 +802,54 @@ export class Participant {
/**
* Waits for remote video state - receiving and displayed.
* @param endpointId
* @param reverse
*/
async waitForRemoteVideo(endpointId: string) {
await this.driver.waitUntil(async () =>
await this.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
});
async waitForRemoteVideo(endpointId: string, reverse = false) {
if (reverse) {
await this.driver.waitUntil(async () =>
!await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && !await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to not be received 15s by ${this.displayName}`
});
} else {
await this.driver.waitUntil(async () =>
await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.displayName}`
});
}
}
/**
* Waits for ninja icon to be displayed.
* @param endpointId
* @param endpointId When no endpoint id is passed we check for any ninja icon.
*/
async waitForNinjaIcon(endpointId: string) {
await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000,
timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
});
async waitForNinjaIcon(endpointId?: string) {
if (endpointId) {
await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000,
timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
});
} else {
await this.driver.$('//span[contains(@class,"videocontainer")]//span[contains(@class,"connection_ninja")]')
.waitForDisplayed({
timeout: 5_000,
timeoutMsg: `expected ninja icon to be displayed in 5s by ${this.displayName}`
});
}
}
/**
* Waits for dominant speaker icon to appear in remote video of a participant.
* @param endpointId the endpoint ID of the participant whose dominant speaker icon status will be checked.
*/
waitForDominantSpeaker(endpointId: string) {
return this.driver.$(`//span[@id="participant_${endpointId}" and contains(@class, "dominant-speaker")]`)
.waitForDisplayed({ timeout: 5_000 });
}
}

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

@@ -53,6 +53,17 @@ export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptio
]);
}
/**
* Creates the first participant instance or prepares one for re-joining.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export function joinFirstParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
return joinTheModeratorAsP1(ctx, options);
}
/**
* Creates the second participant instance or prepares one for re-joining.
*
@@ -163,13 +174,14 @@ export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions
const { skipInMeetingChecks } = options;
await _joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
});
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
}),
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
]);
@@ -241,13 +253,15 @@ export async function muteAudioAndCheck(testee: Participant, observer: Participa
* @param observer
*/
export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
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);
}
/**
* Starts the video on testee and check on observer.
* Stop the video on testee and check on observer.
* @param testee
* @param observer
*/
@@ -258,6 +272,18 @@ export async function unmuteVideoAndCheck(testee: Participant, observer: Partici
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
}
/**
* Starts the video on testee and check on observer.
* @param testee
* @param observer
*/
export async function muteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickVideoMuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
}
/**
* Get a JWT token for a moderator.
*/
@@ -345,3 +371,12 @@ export async function checkForScreensharingTile(sharer: Participant, observer: P
reverse
});
}
/**
* Hangs up all participants (p1, p2, p3 and p4)
* @returns {Promise<void>}
*/
export function hangupAllParticipants() {
return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup(), ctx.p4?.hangup() ]
.map(p => p ?? Promise.resolve()));
}

View File

@@ -9,6 +9,7 @@ export type IContext = {
iframeAPI: boolean;
jwtKid: string;
jwtPrivateKeyPath: string;
keepAlive: Array<any>;
p1: Participant;
p2: Participant;
p3: Participant;

View File

@@ -62,7 +62,7 @@ export default class Filmstrip extends BasePageObject {
await remoteDisplayName.moveTo();
return await this.participant.driver.execute(eId =>
return await this.participant.execute(eId =>
document.evaluate(`//span[@id="participant_${eId}"]//video`,
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue?.srcObject?.id, endpointId);
}
@@ -71,7 +71,7 @@ export default class Filmstrip extends BasePageObject {
* Returns the local video id.
*/
getLocalVideoId() {
return this.participant.driver.execute(
return this.participant.execute(
'return document.getElementById("localVideo_container").srcObject.id');
}
@@ -80,10 +80,49 @@ export default class Filmstrip extends BasePageObject {
* @param participant The participant.
*/
async pinParticipant(participant: Participant) {
const id = participant === this.participant
? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
let videoIdToSwitchTo;
await this.participant.driver.$(`//span[@id="${id}"]`).click();
if (participant === this.participant) {
videoIdToSwitchTo = await this.getLocalVideoId();
// when looking up the element and clicking it, it doesn't work if we do it twice in a row (oneOnOne.spec)
await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
} else {
const epId = await participant.getEndpointId();
videoIdToSwitchTo = await this.getRemoteVideoId(epId);
await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
}
await this.participant.driver.waitUntil(
async () => await this.participant.getLargeVideo().getId() === videoIdToSwitchTo,
{
timeout: 3_000,
timeoutMsg: `${this.participant.displayName} did not switch the large video to ${
participant.displayName}`
}
);
}
/**
* Unpins a participant by clicking on their thumbnail.
* @param participant
*/
async unpinParticipant(participant: Participant) {
const epId = await participant.getEndpointId();
if (participant === this.participant) {
await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
} else {
await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
}
await this.participant.driver.$(`//div[ @id="pin-indicator-${epId}" ]`).waitForDisplayed({
timeout: 2_000,
timeoutMsg: `${this.participant.displayName} did not unpin ${participant.displayName}`,
reverse: true
});
}
/**
@@ -155,12 +194,19 @@ export default class Filmstrip extends BasePageObject {
return this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
}
/**
* Hover over local video.
*/
hoverOverLocalVideo() {
return this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
}
/**
* Clicks on the hide self view button from local video.
*/
async hideSelfView() {
// open local video menu
await this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
await this.hoverOverLocalVideo();
await this.participant.driver.$(LOCAL_USER_CONTROLS).moveTo();
// click Hide self view button
@@ -215,4 +261,16 @@ export default class Filmstrip extends BasePageObject {
return (await this.participant.driver.$$('//div[@id="remoteVideos"]//span[contains(@class,"videocontainer")]')
.filter(thumbnail => thumbnail.isDisplayed())).length;
}
/**
* Check if remote videos in filmstrip are visible.
*
* @param isDisplayed whether or not filmstrip remote videos should be visible
*/
verifyRemoteVideosDisplay(isDisplayed: boolean) {
return this.participant.driver.$('//div[contains(@class, "remote-videos")]/div').waitForDisplayed({
timeout: 5_000,
reverse: !isDisplayed,
});
}
}

View File

@@ -11,7 +11,7 @@ export default class IframeAPI extends BasePageObject {
* @param event
*/
getEventResult(event: string): Promise<any> {
return this.participant.driver.execute(
return this.participant.execute(
eventName => {
const result = window.jitsiAPI.test[eventName];
@@ -28,7 +28,7 @@ export default class IframeAPI extends BasePageObject {
* @param eventName The event name.
*/
addEventListener(eventName: string) {
return this.participant.driver.execute(
return this.participant.execute(
(event, prefix) => {
console.log(`${new Date().toISOString()} ${prefix} Adding listener for event: ${event}`);
window.jitsiAPI.addListener(event, evt => {
@@ -43,14 +43,14 @@ export default class IframeAPI extends BasePageObject {
* Returns an array of available rooms and details of it.
*/
getRoomsInfo() {
return this.participant.driver.execute(() => window.jitsiAPI.getRoomsInfo());
return this.participant.execute(() => window.jitsiAPI.getRoomsInfo());
}
/**
* Returns the number of participants in the conference.
*/
getNumberOfParticipants() {
return this.participant.driver.execute(() => window.jitsiAPI.getNumberOfParticipants());
return this.participant.execute(() => window.jitsiAPI.getNumberOfParticipants());
}
/**
@@ -59,7 +59,7 @@ export default class IframeAPI extends BasePageObject {
* @param args The arguments.
*/
executeCommand(command: string, ...args: any[]) {
return this.participant.driver.execute(
return this.participant.execute(
(commandName, commandArgs) =>
window.jitsiAPI.executeCommand(commandName, ...commandArgs)
, command, args);
@@ -69,13 +69,13 @@ export default class IframeAPI extends BasePageObject {
* Returns the current state of the participant's pane.
*/
isParticipantsPaneOpen() {
return this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
return this.participant.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
}
/**
* Removes the embedded Jitsi Meet conference.
*/
dispose() {
return this.participant.driver.execute(() => window.jitsiAPI.dispose());
return this.participant.execute(() => window.jitsiAPI.dispose());
}
}

View File

@@ -39,17 +39,16 @@ export default class LargeVideo extends BasePageObject {
* Returns resource part of the JID of the user who is currently displayed in the large video area.
*/
getResource() {
return this.participant.driver.execute(() => APP.UI.getLargeVideoID());
return this.participant.execute(() => APP?.UI?.getLargeVideoID());
}
/**
* Returns the source of the large video currently shown.
*/
getId() {
return this.participant.driver.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

@@ -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';
@@ -44,6 +45,20 @@ export default class Notifications extends BasePageObject {
await displayNameEl.waitForDisplayed();
}
/**
* Closes the ask to unmute notification.
*/
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);
}
/**
* Dismisses any join notifications.
*/
@@ -79,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

@@ -107,13 +107,12 @@ export default class ParticipantsPane extends BasePageObject {
}
const participantId = await participantToUnmute.getEndpointId();
const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
await participantItem.waitForExist();
await participantItem.moveTo();
await this.selectParticipant(participantToUnmute);
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

@@ -1,7 +1,10 @@
import PreMeetingScreen from './PreMeetingScreen';
const DISPLAY_NAME_ID = 'premeeting-name-input';
const ERROR_ON_JOIN = 'prejoin.errorMessage';
const JOIN_BUTTON_TEST_ID = 'prejoin.joinMeeting';
const JOIN_WITHOUT_AUDIO = 'prejoin.joinWithoutAudio';
const OPTIONS_BUTTON = 'prejoin.joinOptions';
/**
* Page object for the PreJoin screen.
@@ -28,4 +31,25 @@ export default class PreJoinScreen extends PreMeetingScreen {
return this.participant.driver.$('[data-testid="prejoin.screen"]')
.waitForDisplayed({ timeout: 3000 });
}
/**
* Returns the error message displayed on the prejoin screen.
*/
getErrorOnJoin() {
return this.participant.driver.$(`[data-testid="${ERROR_ON_JOIN}"]`);
}
/**
* Returns the join without audio button element.
*/
getJoinWithoutAudioButton() {
return this.participant.driver.$(`[data-testid="${JOIN_WITHOUT_AUDIO}"]`);
}
/**
* Returns the join options button element.
*/
getJoinOptions() {
return this.participant.driver.$(`[data-testid="${OPTIONS_BUTTON}"]`);
}
}

View File

@@ -54,7 +54,7 @@ export default abstract class PreMeetingScreen extends BasePageObject {
* Checks internally whether lobby room is joined.
*/
isLobbyRoomJoined() {
return this.participant.driver.execute(
return this.participant.execute(
() => APP?.conference?._room?.room?.getLobby()?.lobbyRoom?.joined === true);
}

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

@@ -277,7 +277,7 @@ export default class Toolbar extends BasePageObject {
* Ensures the overflow menu is not displayed.
* @private
*/
private async closeOverflowMenu() {
async closeOverflowMenu() {
if (!await this.isOverflowMenuOpen()) {
return;
}

View File

@@ -7,7 +7,7 @@ describe('DisplayName', () => {
const { p1, p2 } = ctx;
// default remote display name
const defaultDisplayName = await p1.driver.execute(() => config.defaultRemoteDisplayName);
const defaultDisplayName = await p1.execute(() => config.defaultRemoteDisplayName);
const p1EndpointId = await p1.getEndpointId();
const p2EndpointId = await p2.getEndpointId();

View File

@@ -4,7 +4,7 @@ describe('Grant moderator', () => {
it('joining the meeting', async () => {
await ensureOneParticipant(ctx);
if (await ctx.p1.driver.execute(() => typeof APP.conference._room.grantOwner !== 'function')) {
if (await ctx.p1.execute(() => typeof APP.conference._room.grantOwner !== 'function')) {
ctx.skipSuiteTests = true;
return;

View File

@@ -3,7 +3,10 @@ import {
checkForScreensharingTile,
ensureOneParticipant,
ensureTwoParticipants,
joinSecondParticipant
joinSecondParticipant,
muteAudioAndCheck,
unmuteAudioAndCheck,
unmuteVideoAndCheck
} from '../../helpers/participants';
describe('Mute', () => {
@@ -33,10 +36,7 @@ describe('Mute', () => {
it('p2 unmute after p1 mute and check', async () => {
const { p1, p2 } = ctx;
await p2.getToolbar().clickAudioUnmuteButton();
// and now check whether second participant is muted
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p2, true);
await unmuteAudioAndCheck(p2, p1);
});
it('p1 mutes before p2 joins', async () => {
@@ -73,13 +73,10 @@ async function toggleMuteAndCheck(
observer: Participant,
muted: boolean) {
if (muted) {
await testee.getToolbar().clickAudioMuteButton();
await muteAudioAndCheck(testee, observer);
} else {
await testee.getToolbar().clickAudioUnmuteButton();
await unmuteAudioAndCheck(testee, observer);
}
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, !muted);
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, !muted);
}
/**
@@ -136,7 +133,6 @@ async function muteP1BeforeP2JoinsAndScreenshare(p2p: boolean) {
await p1.getToolbar().clickStopDesktopSharingButton();
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
await p1.getToolbar().clickVideoUnmuteButton();
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
await unmuteVideoAndCheck(p1, p2);
await p2.waitForRemoteVideo(await p1.getEndpointId());
}

View File

@@ -0,0 +1,125 @@
import { ensureOneParticipant, joinFirstParticipant, joinSecondParticipant } from '../../helpers/participants';
describe('PreJoin', () => {
it('display name required', async () => {
await joinFirstParticipant(ctx, {
configOverwrite: {
prejoinConfig: {
enabled: true,
},
requireDisplayName: true
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
});
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
await p1PreJoinScreen.waitForLoading();
const joinButton = p1PreJoinScreen.getJoinButton();
await joinButton.waitForDisplayed();
await joinButton.click();
const error = p1PreJoinScreen.getErrorOnJoin();
await error.waitForDisplayed();
await ctx.p1.hangup();
});
it('without lobby', async () => {
await joinFirstParticipant(ctx, {
configOverwrite: {
prejoinConfig: {
enabled: true,
}
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
});
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
await p1PreJoinScreen.waitForLoading();
const joinButton = p1PreJoinScreen.getJoinButton();
await joinButton.waitForDisplayed();
await ctx.p1.hangup();
});
it('without audio', async () => {
await joinFirstParticipant(ctx, {
configOverwrite: {
prejoinConfig: {
enabled: true,
}
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
});
const { p1 } = ctx;
const p1PreJoinScreen = p1.getPreJoinScreen();
await p1PreJoinScreen.waitForLoading();
await p1PreJoinScreen.getJoinOptions().click();
const joinWithoutAudioBtn = p1PreJoinScreen.getJoinWithoutAudioButton();
await joinWithoutAudioBtn.waitForClickable();
await joinWithoutAudioBtn.click();
await p1.waitToJoinMUC();
await p1.driver.$('//div[contains(@class, "audio-preview")]//div[contains(@class, "toolbox-icon") '
+ 'and contains(@class, "toggled") and contains(@class, "disabled")]')
.waitForDisplayed();
await ctx.p1.hangup();
});
it('with lobby', async () => {
await ensureOneParticipant(ctx);
const { p1 } = ctx;
const p1SecurityDialog = p1.getSecurityDialog();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLobbyEnabled()).toBe(false);
await p1SecurityDialog.toggleLobby();
await p1SecurityDialog.waitForLobbyEnabled();
await joinSecondParticipant(ctx, {
configOverwrite: {
prejoinConfig: {
enabled: true,
}
},
skipDisplayName: true,
skipWaitToJoin: true,
skipInMeetingChecks: true
});
const p1PreJoinScreen = ctx.p2.getPreJoinScreen();
await p1PreJoinScreen.waitForLoading();
const joinButton = p1PreJoinScreen.getJoinButton();
await joinButton.waitForDisplayed();
});
});

View File

@@ -0,0 +1,29 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Single port', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx));
it('test', async () => {
const { p1, p2 } = ctx;
const port1 = await getRemotePort(p1);
const port2 = await getRemotePort(p2);
expect(Number.isInteger(port1)).toBe(true);
expect(Number.isInteger(port2)).toBe(true);
expect(port1).toBe(port2);
});
});
/**
* Get the remote port of the participant.
* @param participant
*/
async function getRemotePort(participant: Participant) {
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.ip);
const parts = data.split(':');
return parts.length > 1 ? parseInt(parts[1], 10) : '';
}

View File

@@ -0,0 +1,41 @@
import { ensureTwoParticipants, muteVideoAndCheck, unmuteVideoAndCheck } from '../../helpers/participants';
describe('Stop video', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx));
it('stop video and check', () => muteVideoAndCheck(ctx.p1, ctx.p2));
it('start video and check', () => unmuteVideoAndCheck(ctx.p1, ctx.p2));
it('start video and check stream', async () => {
await muteVideoAndCheck(ctx.p1, ctx.p2);
// now participant2 should be on large video
const largeVideoId = await ctx.p1.getLargeVideo().getId();
await unmuteVideoAndCheck(ctx.p1, ctx.p2);
// check if video stream from second participant is still on large video
expect(largeVideoId).toBe(await ctx.p1.getLargeVideo().getId());
});
it('stop video on participant and check', () => muteVideoAndCheck(ctx.p2, ctx.p1));
it('start video on participant and check', () => unmuteVideoAndCheck(ctx.p2, ctx.p1));
it('stop video on before second joins', async () => {
await ctx.p2.hangup();
const { p1 } = ctx;
await p1.getToolbar().clickVideoMuteButton();
await ensureTwoParticipants(ctx);
const { p2 } = ctx;
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
await unmuteVideoAndCheck(p1, p2);
});
});

View File

@@ -0,0 +1,32 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
const MY_TEST_SUBJECT = 'My Test Subject';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
describe('Subject', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx, {
configOverwrite: {
subject: MY_TEST_SUBJECT
}
}));
it('check', async () => {
await checkSubject(ctx.p1, MY_TEST_SUBJECT);
await checkSubject(ctx.p2, MY_TEST_SUBJECT);
});
});
/**
* Check was subject set.
*
* @param participant
* @param subject
*/
async function checkSubject(participant: Participant, subject: string) {
const localTile = participant.driver.$(SUBJECT_XPATH);
await localTile.moveTo();
expect(await localTile.getText()).toBe(subject);
}

View File

@@ -0,0 +1,39 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('SwitchVideo', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx));
it('p1 click on local', () => ctx.p1.getFilmstrip().pinParticipant(ctx.p1));
it('p1 click on remote', async () => {
await closeToolbarMenu();
const { p1, p2 } = ctx;
await p1.getFilmstrip().pinParticipant(p2);
});
it('p1 unpin remote', () => ctx.p1.getFilmstrip().unpinParticipant(ctx.p2));
it('p2 pin remote', () => ctx.p2.getFilmstrip().pinParticipant(ctx.p1));
it('p2 unpin remote', () => ctx.p2.getFilmstrip().unpinParticipant(ctx.p1));
it('p2 click on local', () => ctx.p2.getFilmstrip().pinParticipant(ctx.p2));
it('p2 click on remote', async () => {
await closeToolbarMenu();
const { p1, p2 } = ctx;
await p2.getFilmstrip().pinParticipant(p1);
});
});
/**
* Closes the overflow menu on both participants.
*/
async function closeToolbarMenu() {
await ctx.p1.getToolbar().closeOverflowMenu();
await ctx.p2.getToolbar().closeOverflowMenu();
}

View File

@@ -0,0 +1,26 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('UDP', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx));
it('check', async () => {
const { p1, p2 } = ctx;
// just in case wait 1500, this is the interval we use for `config.pcStatsInterval`
await p1.driver.pause(1500);
expect(await getProtocol(p1)).toBe('udp');
expect(await getProtocol(p2)).toBe('udp');
});
});
/**
* Get the remote port of the participant.
* @param participant
*/
async function getProtocol(participant: Participant) {
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.type);
return data.toLowerCase();
}

View File

@@ -2,6 +2,7 @@ import { Participant } from '../../helpers/Participant';
import {
ensureOneParticipant,
ensureThreeParticipants, ensureTwoParticipants,
hangupAllParticipants,
unmuteAudioAndCheck,
unmuteVideoAndCheck
} from '../../helpers/participants';
@@ -69,8 +70,10 @@ 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 () => {
@@ -120,7 +123,7 @@ describe('AVModeration', () => {
await moderatorParticipantsPane.getAVModerationMenu().clickStopVideoModeration();
});
it('grant moderator', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureThreeParticipants(ctx);
@@ -143,7 +146,7 @@ describe('AVModeration', () => {
await unmuteByModerator(p3, p2, false, true);
});
it('ask to unmute', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
@@ -181,7 +184,7 @@ describe('AVModeration', () => {
await tryToVideoUnmuteAndCheck(p2, p1);
});
it('join moderated', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureOneParticipant(ctx);

View File

@@ -122,9 +122,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);
@@ -173,10 +185,7 @@ describe('Avatar', () => {
await p3.hangup();
// Unmute p1's and p2's videos
await p1.getToolbar().clickVideoUnmuteButton();
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
await unmuteVideoAndCheck(p1, p2);
});
it('email persistence', async () => {

View File

@@ -1,7 +1,12 @@
import type { ChainablePromiseElement } from 'webdriverio';
import type { Participant } from '../../helpers/Participant';
import { checkSubject, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
import {
checkSubject,
ensureThreeParticipants,
ensureTwoParticipants,
hangupAllParticipants
} from '../../helpers/participants';
const MAIN_ROOM_NAME = 'Main room';
const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
@@ -309,7 +314,7 @@ describe('BreakoutRooms', () => {
});
it('send participants to breakout room', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
// because the participants rejoin so fast, the meeting is not properly ended,
// so the previous breakout rooms would still be there.

View File

@@ -1,4 +1,9 @@
import { ensureOneParticipant, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
import {
ensureOneParticipant,
ensureThreeParticipants,
ensureTwoParticipants,
hangupAllParticipants
} from '../../helpers/participants';
describe('Codec selection', () => {
it('asymmetric codecs', async () => {
@@ -20,18 +25,18 @@ describe('Codec selection', () => {
const { p1, p2 } = ctx;
// Check if media is playing on both endpoints.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Check if p1 is sending VP9 and p2 is sending VP8 as per their codec preferences.
// Except on Firefox because it doesn't support VP9 encode.
if (p1.driver.isFirefox) {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
} else {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
}
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
});
it('asymmetric codecs with AV1', async () => {
@@ -45,28 +50,28 @@ describe('Codec selection', () => {
const { p1, p2, p3 } = ctx;
// Check if media is playing on p3.
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences.
// Except on Firefox because it doesn't support AV1/VP9 encode and AV1 decode.
if (p1.driver.isFirefox) {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
} else {
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
}
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
// If there is a Firefox ep in the call, all other eps will switch to VP9.
if (p1.driver.isFirefox) {
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
} else {
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true);
}
});
it('codec switch over', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureTwoParticipants(ctx, {
configOverwrite: {
@@ -83,8 +88,8 @@ describe('Codec selection', () => {
}
// Check if p1 and p2 are encoding in VP9 which is the default codec.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true);
await ensureThreeParticipants(ctx, {
configOverwrite: {
@@ -96,22 +101,22 @@ describe('Codec selection', () => {
const { p3 } = ctx;
// Check if all three participants are encoding in VP8 now.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true);
await p3.hangup();
// Check of p1 and p2 have switched to VP9.
await p1.driver.waitUntil(
() => p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
() => p1.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'
}
);
await p2.driver.waitUntil(
() => p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
() => p2.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()),
{
timeout: 10000,
timeoutMsg: 'p1 did not switch back to VP9'

View File

@@ -1,5 +1,10 @@
import { P1_DISPLAY_NAME, P3_DISPLAY_NAME, Participant } from '../../helpers/Participant';
import { ensureOneParticipant, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
import {
ensureOneParticipant,
ensureThreeParticipants,
ensureTwoParticipants,
hangupAllParticipants
} from '../../helpers/participants';
import type { IJoinOptions } from '../../helpers/types';
import type PreMeetingScreen from '../../pageobjects/PreMeetingScreen';
@@ -7,7 +12,7 @@ describe('Lobby', () => {
it('joining the meeting', async () => {
await ensureOneParticipant(ctx);
if (!await ctx.p1.driver.execute(() => APP.conference._room.isLobbySupported())) {
if (!await ctx.p1.execute(() => APP.conference._room.isLobbySupported())) {
ctx.skipSuiteTests = true;
}
});
@@ -174,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;
@@ -191,7 +195,7 @@ describe('Lobby', () => {
});
it('change of moderators in lobby', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureTwoParticipants(ctx);
@@ -219,7 +223,7 @@ describe('Lobby', () => {
// here the important check is whether the moderator sees the knocking participant
await enterLobby(p2, false);
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
});
it('shared password', async () => {
@@ -263,8 +267,7 @@ describe('Lobby', () => {
});
it('enable with more than two participants', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureThreeParticipants(ctx);

View File

@@ -0,0 +1,81 @@
import type { Participant } from '../../helpers/Participant';
import {
ensureThreeParticipants,
ensureTwoParticipants
} from '../../helpers/participants';
const ONE_ON_ONE_CONFIG_OVERRIDES = {
configOverwrite: {
disable1On1Mode: false,
toolbarConfig: {
timeout: 500,
alwaysVisible: false
}
}
};
describe('OneOnOne', () => {
it('filmstrip hidden in 1on1', async () => {
await ensureTwoParticipants(ctx, ONE_ON_ONE_CONFIG_OVERRIDES);
const { p1, p2 } = ctx;
await configureToolbarsToHideQuickly(p1);
await configureToolbarsToHideQuickly(p2);
await p1.getFilmstrip().verifyRemoteVideosDisplay(false);
await p2.getFilmstrip().verifyRemoteVideosDisplay(false);
});
it('filmstrip visible with more than 2', async () => {
await ensureThreeParticipants(ctx, ONE_ON_ONE_CONFIG_OVERRIDES);
const { p1, p2, p3 } = ctx;
await configureToolbarsToHideQuickly(p3);
await p1.getFilmstrip().verifyRemoteVideosDisplay(true);
await p2.getFilmstrip().verifyRemoteVideosDisplay(true);
await p3.getFilmstrip().verifyRemoteVideosDisplay(true);
});
it('filmstrip display when returning to 1on1', async () => {
const { p1, p2, p3 } = ctx;
await p2.getFilmstrip().pinParticipant(p2);
await p3.hangup();
await p1.getFilmstrip().verifyRemoteVideosDisplay(false);
await p2.getFilmstrip().verifyRemoteVideosDisplay(true);
});
it('filmstrip visible on self view focus', async () => {
const { p1 } = ctx;
await p1.getFilmstrip().pinParticipant(p1);
await p1.getFilmstrip().verifyRemoteVideosDisplay(true);
await p1.getFilmstrip().unpinParticipant(p1);
await p1.getFilmstrip().verifyRemoteVideosDisplay(false);
});
it('filmstrip hover show videos', async () => {
const { p1 } = ctx;
await p1.getFilmstrip().hoverOverLocalVideo();
await p1.getFilmstrip().verifyRemoteVideosDisplay(true);
});
});
/**
* Hangs up all participants (p1, p2 and p3)
* @returns {Promise<void>}
*/
function configureToolbarsToHideQuickly(participant: Participant): Promise<void> {
return participant.execute(() => {
APP.UI.dockToolbar(false);
APP.UI.showToolbar(250);
});
}

View File

@@ -1,6 +1,7 @@
import {
ensureOneParticipant,
ensureTwoParticipants,
hangupAllParticipants,
joinSecondParticipant,
joinThirdParticipant,
unmuteVideoAndCheck
@@ -38,7 +39,6 @@ describe('StartMuted', () => {
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p1.waitForAudioMuted(p2, true);
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, true);
@@ -145,7 +145,6 @@ describe('StartMuted', () => {
const { p1, p2 } = ctx;
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
@@ -155,7 +154,6 @@ describe('StartMuted', () => {
]);
await unmuteVideoAndCheck(p2, p1);
await p1.getLargeVideo().assertPlaying();
});
@@ -236,10 +234,8 @@ describe('StartMuted', () => {
const { p3 } = ctx;
// Unmute p2 and check if its video is being received by p1 and p3.
await p2.getToolbar().clickVideoUnmuteButton();
await unmuteVideoAndCheck(p2, p3);
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
await p3.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2, true);
// Mute p2's video just before p3 leaves.
await p2.getToolbar().clickVideoMuteButton();
@@ -255,11 +251,3 @@ describe('StartMuted', () => {
await p1.getLargeVideo().assertPlaying();
});
});
/**
* Hangs up all participants (p1, p2 and p3)
* @returns {Promise<void>}
*/
function hangupAllParticipants() {
return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup() ]);
}

View File

@@ -0,0 +1,113 @@
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
/**
* The CSS selector for local video when outside of tile view. It should
* be in a container separate from remote videos so remote videos can
* scroll while local video stays docked.
*/
const FILMSTRIP_VIEW_LOCAL_VIDEO_CSS_SELECTOR = '#filmstripLocalVideo #localVideoContainer';
/**
* The CSS selector for local video tile view is enabled. It should display
* at the end of all the other remote videos, as the last tile.
*/
const TILE_VIEW_LOCAL_VIDEO_CSS_SELECTOR = '.remote-videos #localVideoContainer';
describe('TileView', () => {
it('joining the meeting', () => ensureTwoParticipants(ctx));
// TODO: implements etherpad check
it('pinning exits', async () => {
await enterTileView();
const { p1, p2 } = ctx;
await p1.getFilmstrip().pinParticipant(p2);
await p1.waitForTileViewDisplay(true);
});
it('local video display', async () => {
await enterTileView();
const { p1 } = ctx;
await p1.driver.$(TILE_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({ timeout: 3000 });
await p1.driver.$(FILMSTRIP_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({
timeout: 3000,
reverse: true
});
});
it('can exit', async () => {
const { p1 } = ctx;
await p1.getToolbar().clickExitTileViewButton();
await p1.waitForTileViewDisplay(true);
});
it('local video display independently from remote', async () => {
const { p1 } = ctx;
await p1.driver.$(TILE_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({
timeout: 3000,
reverse: true
});
await p1.driver.$(FILMSTRIP_VIEW_LOCAL_VIDEO_CSS_SELECTOR).waitForDisplayed({ timeout: 3000 });
});
it('lastN', async () => {
const { p1, p2 } = ctx;
if (p1.driver.isFirefox) {
// Firefox does not support external audio file as input.
// Not testing as second participant cannot be dominant speaker.
return;
}
await p2.getToolbar().clickAudioMuteButton();
await ensureThreeParticipants(ctx, {
configOverwrite: {
channelLastN: 1,
startWithAudioMuted: true
}
});
const { p3 } = ctx;
// one inactive icon should appear in few seconds
await p3.waitForNinjaIcon();
const p1EpId = await p1.getEndpointId();
await p3.waitForRemoteVideo(p1EpId);
const p2EpId = await p2.getEndpointId();
await p3.waitForNinjaIcon(p2EpId);
// no video for participant 2
await p3.waitForRemoteVideo(p2EpId, true);
// mute audio for participant 1
await p1.getToolbar().clickAudioMuteButton();
// unmute audio for participant 2
await p2.getToolbar().clickAudioUnmuteButton();
await p3.waitForDominantSpeaker(p2EpId);
// check video of participant 2 should be received
await p3.waitForRemoteVideo(p2EpId);
});
});
/**
* Attempts to enter tile view and verifies tile view has been entered.
*/
async function enterTileView() {
await ctx.p1.getToolbar().clickEnterTileViewButton();
await ctx.p1.waitForTileViewDisplay();
}

View File

@@ -4,7 +4,8 @@ import {
ensureFourParticipants,
ensureOneParticipant,
ensureThreeParticipants,
ensureTwoParticipants
ensureTwoParticipants,
hangupAllParticipants
} from '../../helpers/participants';
describe('Desktop sharing', () => {
@@ -26,7 +27,7 @@ describe('Desktop sharing', () => {
// Check if a local screen share tile is created on p2.
await checkForScreensharingTile(p2, p2);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
it('stop', async () => {
@@ -55,7 +56,7 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p2, p2);
await checkForScreensharingTile(p2, p2);
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
@@ -71,7 +72,7 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p2, p2);
// The video should be playing.
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// Start desktop share on p1.
await p1.getToolbar().clickDesktopSharingButton();
@@ -86,7 +87,7 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p2, p3);
// The large video should be playing on p3.
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
@@ -117,7 +118,7 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p2, p3);
// The large video should be playing on p3.
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
@@ -126,7 +127,7 @@ describe('Desktop sharing', () => {
* The call switches to jvb and then back to p2p.
*/
it('screen sharing toggle before others join', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureOneParticipant(ctx, {
configOverwrite: {
@@ -155,22 +156,22 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p1, p2);
await checkForScreensharingTile(p1, p3);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p3.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// p3 leaves the call.
await p3.hangup();
// Make sure p2 see's p1's share after the call switches back to p2p.
await checkForScreensharingTile(p1, p2);
expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
// p2 starts share when in p2p.
await p2.getToolbar().clickDesktopSharingButton();
// Makes sure p2's share is visible on p1.
await checkForScreensharingTile(p2, p1);
expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
});
/**
@@ -178,18 +179,18 @@ describe('Desktop sharing', () => {
* where only a screen share can be received. A bug fixed in jvb 0c5dd91b where the video was not received.
*/
it('audio only and non dominant screen share', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureOneParticipant(ctx);
const { p1 } = ctx;
// a workaround to directly set audio only mode without going through the rest of the settings in the UI
await p1.driver.execute(type => {
APP.store.dispatch({
await p1.execute(type => {
APP?.store?.dispatch({
type,
audioOnly: true
});
APP.conference.onToggleAudioOnly();
APP?.conference?.onToggleAudioOnly();
}, SET_AUDIO_ONLY);
await p1.getToolbar().clickAudioMuteButton();
@@ -203,7 +204,7 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p3, p2);
// the video should be playing
await p1.driver.waitUntil(() => p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
await p1.driver.waitUntil(() => p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
timeout: 5_000,
timeoutMsg: 'expected remote screen share to be on large'
});
@@ -215,7 +216,7 @@ describe('Desktop sharing', () => {
* A problem fixed in jitsi-meet 3657c19e and d6ab0a72.
*/
it('audio only and dominant screen share', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureOneParticipant(ctx, {
configOverwrite: {
@@ -226,12 +227,12 @@ describe('Desktop sharing', () => {
const { p1 } = ctx;
// a workaround to directly set audio only mode without going through the rest of the settings in the UI
await p1.driver.execute(type => {
APP.store.dispatch({
await p1.execute(type => {
APP?.store?.dispatch({
type,
audioOnly: true
});
APP.conference.onToggleAudioOnly();
APP?.conference?.onToggleAudioOnly();
}, SET_AUDIO_ONLY);
await ensureTwoParticipants(ctx, {
@@ -254,7 +255,7 @@ describe('Desktop sharing', () => {
expect(await p1.getLargeVideo().getResource()).toBe(`${await p3.getEndpointId()}-v1`);
// the video should be playing
await p1.driver.waitUntil(() => p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
await p1.driver.waitUntil(() => p1.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), {
timeout: 5_000,
timeoutMsg: 'expected remote screen share to be on large'
});
@@ -264,7 +265,7 @@ describe('Desktop sharing', () => {
* Test screensharing with lastN. We add p4 with lastN=2 and verify that it receives the expected streams.
*/
it('with lastN', async () => {
await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
await hangupAllParticipants();
await ensureThreeParticipants(ctx);
const { p1, p2, p3 } = ctx;
@@ -291,7 +292,7 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p3, p4);
// And the video should be playing
expect(await p4.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
expect(await p4.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
const p1EndpointId = await p1.getEndpointId();
const p2EndpointId = await p2.getEndpointId();

View File

@@ -16,8 +16,8 @@ describe('Lock Room with Digits only', () => {
it('lock room with digits only', async () => {
const { p1 } = ctx;
expect(await p1.driver.execute(() => APP.store.getState()['features/base/config']
.roomPasswordNumberOfDigits === 5)).toBe(true);
expect(await p1.execute(
() => APP.store.getState()['features/base/config'].roomPasswordNumberOfDigits === 5)).toBe(true);
const p1SecurityDialog = p1.getSecurityDialog();

View File

@@ -0,0 +1,27 @@
import { ensureOneParticipant } from '../../helpers/participants';
describe('Video Layout', () => {
it('join participant', () => ensureOneParticipant(ctx));
it('check', async () => {
const { p1 } = ctx;
const innerWidth = parseInt(await p1.execute('return window.innerWidth'), 10);
const innerHeight = parseInt(await p1.execute('return window.innerHeight'), 10);
const largeVideo = p1.driver.$('//div[@id="largeVideoContainer"]');
const filmstrip = p1.driver.$('//div[contains(@class, "filmstrip")]');
let filmstripWidth;
if (!await filmstrip.isExisting() || !await filmstrip.isDisplayed()) {
filmstripWidth = 0;
} else {
filmstripWidth = await filmstrip.getSize('width');
}
const largeVideoSize = await largeVideo.getSize();
expect((largeVideoSize.width === (innerWidth - filmstripWidth)) || (largeVideoSize.height === innerHeight))
.toBe(true);
});
});

View File

@@ -35,7 +35,7 @@ export async function waitForAudioFromDialInParticipant(participant: Participant
export async function cleanup(participant: Participant) {
// cleanup
if (await participant.isModerator()) {
const jigasiEndpointId = await participant.driver.execute(() => APP.conference.listMembers()[0].getId());
const jigasiEndpointId = await participant.execute(() => APP?.conference?.listMembers()[0].getId());
await participant.getFilmstrip().kickParticipant(jigasiEndpointId);
}
@@ -46,6 +46,6 @@ export async function cleanup(participant: Participant) {
* @param participant
*/
export async function isDialInEnabled(participant: Participant) {
return await participant.driver.execute(() => Boolean(
return await participant.execute(() => Boolean(
config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc));
}

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('keep-alive'));
}, 20_000));
if (bInstance.isFirefox) {
return;
@@ -188,13 +207,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
bInstance.iframePageBase = `file://${path.dirname(rpath)}`;
}));
const globalAny: any = global;
const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
globalAny.ctx = {
times: {}
} as IContext;
globalAny.ctx.roomName = roomName;
globalAny.ctx.roomName = `jitsimeettorture-${crypto.randomUUID()}`;
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
globalAny.ctx.jwtKid = process.env.JWT_KID;
},
@@ -205,6 +218,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 +331,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 +347,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
};